{
  "schema_version": "0.1.0",
  "generated_at_commit": "c8e0398e534c7d740365efc539051698d1ee0e5c",
  "nodes": {
    "boundary:arrow_seam": {
      "why": {
        "text": "Defines the Arrow IPC serialization boundary for catalog and dataplane export, including manifest schema and byte-contract specifications.",
        "cites": [
          {
            "path": "docs/STARPLATES_SPEC.md",
            "lines": "233"
          },
          {
            "path": "docs/STARPLATES_SPEC.md",
            "lines": "356"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "208-218"
          }
        ]
      },
      "copy": {
        "text": "When the reference-star catalog crosses from storage into the plate solver, its rows can be carried in the Apache Arrow columnar binary format — little-endian typed data buffers, one column per field, with a validity bitmap only on the optional columns — instead of parsed text. A one-byte per-row tag marks each row as a Gaia or a legacy bright-star record, because the two carry right ascension in different units (degrees for Gaia, hours for the legacy rows); the value is stored exactly as read and is never normalized at this boundary. Every numeric column is kept at full 64-bit precision, and the catalog identifier rides as a 64-bit floating-point value rather than a 64-bit integer, so a decoded row reproduces the original numbers and their text form exactly and downstream matching cannot tell the binary source from the older text one. This columnar path is an optional, off-by-default alternative to text parsing that returns the same materialized rows with a faster load and a smaller transfer.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/atlas_arrow_codec.ts",
            "lines": "12-72"
          },
          {
            "path": "tools/atlas/atlas_to_arrow.mjs",
            "lines": "11-46"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "208-224"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/STARPLATES_SPEC.md#3.2 Per-cell file: Arrow IPC file format + #5.2 Response bytes: Arrow IPC stream (arrow_seam byte contract: binary_layouts.ts:208-218)"
      }
    },
    "boundary:atlas_rows": {
      "why": {
        "text": "Hybrid atlas row format combining Gaia (RA in degrees) and HYG (RA in hours) data, requiring per-row discrimination for correct coordinate handling.",
        "cites": [
          {
            "path": "CLAUDE.md",
            "lines": "32-42"
          }
        ]
      },
      "copy": {
        "text": "Reference-star rows cross into the solver from stored catalog files, grouped into sky regions and written as JSON text, and the records are hybrid: rows from the Gaia survey carry right ascension in degrees while the older Hipparcos-derived rows carry it in hours. Because the two shapes share the same file, each row is classified individually — a row is read as Gaia when it carries a survey source identifier or a Gaia G magnitude — and its right ascension is divided by fifteen to reach the internal hours convention only for the Gaia shape, leaving the already-hours rows untouched. The catalog habit of tagging the Sun with a zero identifier is honored only for the legacy shape, since Gaia rows write a zero identifier on every star, so a per-row guard keeps the Gaia stars from being silently dropped. Each row is also stamped with the photometric band it reports so downstream magnitude use never mixes the two systems.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
            "lines": "710-748"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "145-188"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "JSON text · TypeScript (CPU)",
        "doc_binding": "CLAUDE.md#UNIT/FORMAT TRAPS (each cost hours once)"
      }
    },
    "boundary:fits_io": {
      "why": {
        "text": "Implements FITS file writing with astropy conformance, NaN-preservation, and proper WCS/SIP header encoding for data export.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "560-568"
          }
        ]
      },
      "copy": {
        "text": "Reading and writing FITS image files crosses a units boundary: right ascension is stored in degrees in the file header but carried in hours everywhere inside the engine, so on read the header value is divided by 15 to get hours and on write it is multiplied by 15 back to degrees, while declination stays in degrees throughout. There is deliberately no single conversion point — the ingest reader, the stacking-lane reader/writer, and the receipt export each perform the conversion independently — so every site must apply the same factor or the same number gets interpreted in the wrong units. Export writers further downstream consume the already-converted world-coordinate values and must not convert a second time. The file itself is big-endian with pixels laid out row by row and one-indexed reference pixels, mapped to the engine's zero-based convention at the boundary.",
        "cites": [
          {
            "path": "src/engine/pipeline/m1_ingestion/fits_decoder.ts",
            "lines": "1125-1127"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "159-186"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "293-318"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#Module 9: Serialization & Export (Finalization) > Export Endpoints"
      }
    },
    "boundary:libraw_mem_image": {
      "why": {
        "text": "Interleaved RGB16 memory layout contract for libraw CR2 decoding (w*h*3 dominant-channel CFA data); ~4-7% cross-leak, never one-hot; dimensions require stride coherence verification.",
        "cites": [
          {
            "path": "docs/WHITEPAPER.md",
            "lines": "82-97"
          }
        ]
      },
      "copy": {
        "text": "On the retained fallback decode path, a raw Canon frame is turned into an interleaved 16-bit RGB buffer by the bundled decoder, and a probe then samples the frame to decide how to read it. If fewer than about two percent of sampled pixels carry light in two or more channels, the frame is treated as a one-color-per-site mosaic and each pixel is collapsed to a neutral gray of its measured value, which keeps the color-filter pattern from imprinting a checkerboard on later star detection; otherwise the three channels are kept and simply normalized. A separate median channel-ratio test, tripped below about 0.15, independently flags the mosaic case so the brightness reduction can fall back to equal channel weights. When the pixel count factors into image dimensions two different ways, the ambiguity is resolved by whichever layout is most self-consistent from row to row. This arm is kept byte-for-byte identical to the pre-cutover pipeline; a separate decoder is the current default.",
        "cites": [
          {
            "path": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
            "lines": "725-758"
          },
          {
            "path": "tools/psf/decode_cr2.mjs",
            "lines": "160-175"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "118-143"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "C++→WASM (LibRaw) + TypeScript · CPU",
        "doc_binding": "docs/WHITEPAPER.md#2. Ingestion and decode > 2.2 CR2 via libraw — the mem_image contract"
      }
    },
    "boundary:rawler_cfa": {
      "why": {
        "text": "Rawler decoder demosaicing of CFA Bayer and X-Trans patterns, now the default arm (libraw retained as cold path); handles per-camera CFA geometry.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "585-613"
          }
        ]
      },
      "copy": {
        "text": "On the default decode path, a raw Canon frame is decoded into a full-sensor single-channel Bayer mosaic — optical-black borders included — and then demosaiced into full-color pixels by an integer bilinear interpolation over the 2×2 color-filter tile. The interpolation reads each pixel's filter color from its absolute sensor coordinates, so cropping to the active image area stays correctly phased, and neighbor samples are clamped at the active-area edge so the dark border pixels never leak into the science pixels. Alongside the pixels it records the per-channel black and white (saturation) levels, white-balance coefficients, and the dark-border pixels as measured metadata that is recorded but never applied — by default the pixels keep their raw black pedestal, and an optional black-level subtraction is left off so the output is bit-for-bit reproducible. This has been the default path since the mid-2026 decoder cutover; the earlier decoder is retained as a cold-path fallback.",
        "cites": [
          {
            "path": "src/engine/wasm_decode/src/lib.rs",
            "lines": "319-347"
          },
          {
            "path": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts",
            "lines": "100-111"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "325-363"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "Rust→WASM (rawler decode + integer demosaic) + TypeScript · CPU",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#Module 1: RAW Decoding & Metadata Extraction > Steps & Functions"
      }
    },
    "boundary:seam_capsule": {
      "why": {
        "text": "Per-buffer seam-capture capsule: raw little-endian .bin buffers (flat row-major, no header) + a capsule.json sidecar (sole layout authority, sha256-verified on load), snapshotted by captureSeam (env-gated CAPTURE_SEAMS=1, default OFF) for byte-level seam replay/testing.",
        "cites": [
          {
            "path": "src/engine/pipeline/seam_capture.ts",
            "lines": "392"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "457-472"
          }
        ]
      },
      "copy": {
        "text": "A test-only capture format that records the pipeline's working state to disk after each processing stage finishes, so a single stage can later be re-run in isolation and its output compared exactly against a stored reference. Each capsule pairs a JSON sidecar — holding the coordinate results and metadata inline — with separate raw little-endian binary files for the large pixel buffers; the sidecar is the sole authority on every buffer's layout and records a checksum that is re-verified when the data is reloaded. The capsule written after one stage is the input to the next, so replaying a stage loads the previous capsule, runs the real stage, and compares the result byte-for-byte. Capture is off by default: the enable flag is read once at startup, and when it is off the capture call is a dead branch that allocates nothing and reads no state, so results are identical whether or not it runs. It refuses rather than guesses — it skips when no frame identifier is supplied or the host is not little-endian, and any capture failure produces a warning and an absent or partial capsule instead of a fabricated value.",
        "cites": [
          {
            "path": "src/engine/pipeline/seam_capture.ts",
            "lines": "6-45"
          },
          {
            "path": "src/engine/pipeline/seam_capture.ts",
            "lines": "392-466"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "459-482"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU (Node)",
        "doc_binding": "src/engine/contracts/binary_layouts.ts#seam_capsule (per-buffer capture capsule, LAW 7); src/engine/pipeline/seam_capture.ts#captureSeam"
      }
    },
    "boundary:starplates_blobs": {
      "why": {
        "text": "Starplates cell data storage format and rendering; Arrow IPC files + manifest encoding for catalog distribution and on-demand sector loading.",
        "cites": [
          {
            "path": "tools/renderlab/README.md",
            "lines": "22-48"
          }
        ]
      },
      "copy": {
        "text": "The reference-star catalog is packaged for release as Apache Arrow columnar files split by position on the sky: one bright all-sky file holding the brightest stars down to a fixed magnitude cut, plus one deeper file per cell on a hierarchical equal-area grid, so a solver fetches only the cells near a frame's estimated pointing. Each row carries a star's celestial position, proper motion, a Gaia G magnitude, a colour index, and its catalog identifier, and cells are assigned by a direct bit-shift of the survey identifier rather than a recomputed sky-pixel lookup. The build is deterministic — rows are totally ordered by magnitude ascending with the identifier breaking ties, so identical input yields byte-identical output — and an accompanying manifest content-addresses every file by hash and records each cell's centre and radius, which is what lets a reader both verify integrity and resolve which cells a search cone touches. The record stride of the separate magnitude-banded blind-search index is not transcribed here and must be read from the builder before any consumer relies on it.",
        "cites": [
          {
            "path": "tools/starplates/build_release.mjs",
            "lines": "9-26"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "190-206"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "Apache Arrow IPC · Node builder (CPU)",
        "doc_binding": "tools/renderlab/README.md#Data"
      }
    },
    "boundary:tauri_native_ipc": {
      "why": null,
      "copy": {
        "text": "On the desktop build, raw-sensor demosaicing can be dispatched from the application's web layer to a native GPU backend, and the pixel data crosses that inter-process boundary as raw little-endian bytes: a single-channel 16-bit Bayer mosaic goes in and an interleaved 32-bit-float RGBA frame comes back. Before dispatch the native side validates the request — it rejects a non-binary (JSON) body, rejects zero or over-200-megapixel dimensions, and requires the byte length to be exactly two bytes per pixel for the stated width and height — returning an error rather than processing a mismatched buffer. The frame is then demosaiced on the GPU and returned as raw bytes; an earlier version encoded the same arrays as JSON number lists, so this is a transport-only change that leaves the data layout and units unchanged while removing a large copy cost. This native path is a rare fallback taken only when no explicit color-filter pattern is supplied, and its shader assumes the Canon RGGB Bayer arrangement.",
        "cites": [
          {
            "path": "src-tauri/src/lib.rs",
            "lines": "44-112"
          },
          {
            "path": "src/engine/core/NativeGpuBridge.ts",
            "lines": "50-59"
          },
          {
            "path": "src/engine/pipeline/m3_gpu_preprocess/shaders/demosaic_bayer.wgsl",
            "lines": "2-17"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript ↔ Rust → WGSL · GPU (native wgpu)"
      }
    },
    "boundary:toolchest_arrow_export": {
      "why": {
        "text": "Headless toolchest API export interface using Arrow IPC format for serialized catalog and measurement data delivery from Node environment.",
        "cites": [
          {
            "path": "tools/api/arrow_sink.apispec.ts",
            "lines": "89-116"
          },
          {
            "path": "docs/DATA_PLATFORM.md",
            "lines": "31"
          }
        ]
      },
      "copy": {
        "text": "After a run finishes, its tabular results are packaged as Apache Arrow columnar files (the little-endian Feather v2 format) for external tools and dashboards: the matched catalog stars, the detected sources, the catalog-forced confirmed photometry, and a single-row run summary carrying the solve center, pixel scale, and confirmation verdict. Every column is labelled with its physical units in the field metadata, which heads off the recurring right-ascension unit confusion — the run summary reports right ascension in hours while the matched-star table reports it in degrees, and both say so; residuals are in arcseconds and positions in pixels. The export only reads a completed run and performs no new computation, and it is honest about absence: when a product is missing it writes an empty table carrying the full column schema rather than a fabricated row. Columns that can never be null carry no validity bitmap, and the catalog identifier is written as text so a large integer id is not rounded.",
        "cites": [
          {
            "path": "packages/toolchest/src/tables.ts",
            "lines": "1-9"
          },
          {
            "path": "packages/toolchest/src/tables.ts",
            "lines": "119-146"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "419-437"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/DATA_PLATFORM.md#3. RECEIPTS / EXPORTS — measurements out (headless Arrow IPC export, gated by tools/api/arrow_sink.apispec.ts)"
      }
    },
    "boundary:wasm_typed_array": {
      "why": {
        "text": "Typed-array crossing boundary between WASM and JavaScript, defining memory layout contracts and endianness for numeric data interchange.",
        "cites": [
          {
            "path": "docs/DATA_PLATFORM.md",
            "lines": "2-13"
          }
        ]
      },
      "copy": {
        "text": "Numeric typed arrays — double- and single-precision floats and sixteen-bit integers — are copied across the boundary between the host and the compiled compute module in little-endian order, each routine using a fixed element stride. The main measured crossing is point-spread-function fitting: the caller passes a batch of star pixel stamps plus initial Gaussian parameters, and the module returns thirteen values per star — six fitted parameters, their six formal one-sigma uncertainties, and a status code. The uncertainties are returned as not-a-number with a nonzero status, rather than a fabricated value, whenever the fit has too few pixels to leave any residual degrees of freedom, the curvature matrix is singular or ill-conditioned, or a computed variance comes out non-finite or negative; the six fitted parameters themselves are unaffected. Data is copied rather than referenced in place because growing the module's memory would invalidate a direct view into it.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "245-291"
          },
          {
            "path": "src/engine/wasm_compute/src/lib.rs",
            "lines": "455-501"
          },
          {
            "path": "src/engine/wasm_compute/src/photometry.rs",
            "lines": "146-191"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM",
        "doc_binding": "docs/DATA_PLATFORM.md#SkyCruncher Data Platform — system I/O map"
      }
    },
    "boundary:wgsl_structs": {
      "why": {
        "text": "GPU shader struct layout specifications for WebGPU compute and rendering, defining alignment and memory ordering for shared GPU primitives.",
        "cites": [
          {
            "path": "docs/02-specs/WEBGPU_RENDER_PLAN.md",
            "lines": "448-486"
          }
        ]
      },
      "copy": {
        "text": "This is the layout agreement for the small parameter blocks the host code packs and hands to the graphics compute shaders. The shading language aligns each field to its own size, aligns a three-component vector to sixteen bytes, and rounds a uniform block's total size up to a multiple of sixteen — so the host that fills the buffer must insert exactly the same padding, or the shader reads every field shifted. Concrete cases follow this rule: the parameterized demosaic shader takes a forty-eight-byte block of image dimensions, colour-filter offsets, black and white levels, and white-balance gains ending in two explicit padding words, while the quad-match shader takes a sixteen-byte block of two counts, a tolerance, and a cap. One older sixteen-byte demosaic block is embedded verbatim in the native desktop build and is frozen, because a single shader is consumed under several bind layouts and any field change would ripple across all of them — new layouts are introduced beside it rather than by editing it.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "226-244"
          },
          {
            "path": "src/engine/pipeline/m3_gpu_preprocess/shaders/demosaic_bayer_param.wgsl",
            "lines": "5-34"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/gpu_quads/gpu_quad_backend.ts",
            "lines": "65-70"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "WGSL · GPU (WebGPU); TypeScript buffer packing · CPU",
        "doc_binding": "docs/02-specs/WEBGPU_RENDER_PLAN.md#7.2 The three shared primitives, spec'd once"
      }
    },
    "external:astrometry_net_oracle": {
      "why": {
        "text": "An independent ground-truth oracle is reached two ways: locally by shelling out to the astrometry.net `solve-field` binary (index files live outside the repo, path fully configurable via env, never hardcoded), and remotely by submitting frames to the public nova.astrometry.net REST API and polling job status. Both paths produce the same canonical TruthLabel record shape, independent of this repo's own solver — breaking self-label circularity for graduation.",
        "cites": [
          {
            "path": "tools/overnight/astrometry_truth.mjs",
            "lines": "6-21"
          },
          {
            "path": "tools/overnight/nova_truth.mjs",
            "lines": "6-20"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "external_dependency": true
      }
    },
    "external:gaia_catalog": {
      "why": {
        "text": "The starplates/atlas build pipeline pulls Gaia DR3 source rows (ra_deg, dec_deg, pm_ra_masyr, pm_dec_masyr, g_mag, bp_rp, source_id) directly from ESA's public Gaia Archive TAP service over HTTPS ADQL async queries, chunked by source_id range to stay under the TAP result cap.",
        "cites": [
          {
            "path": "tools/starplates/download_gaia_chunked.mjs",
            "lines": "5,18,51,151"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "external_dependency": true
      }
    },
    "external:prysm_sidecar": {
      "why": {
        "text": "A pinned Python subprocess running the `prysm` physical-optics library is spawned over newline-framed JSON-RPC to serve diffraction/Zernike-aberrated PSF predictions. On any spawn error, import failure, protocol error, or per-call timeout the caller degrades to `{available:false}` and the engine falls back cold to its closed-form 1.028*lambda/D diffraction formula — the dependency is never fatal.",
        "cites": [
          {
            "path": "tools/prysm/client.ts",
            "lines": "1-25,45-47"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "external_dependency": true
      }
    },
    "external:starplates_r2": {
      "why": {
        "text": "Built starplates/Gaia-cell release directories (Arrow cell blobs + manifest.json) are uploaded to a Cloudflare R2 bucket named 'starplates' over the S3-compatible HTTP API, using account id / access key / secret credentials read from env. This is the canonical remote distribution point for the catalog dataplane.",
        "cites": [
          {
            "path": "tools/starplates/publish_r2.mjs",
            "lines": "20-23,49,137-157"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "external_dependency": true
      }
    },
    "src/engine/core/AtmosphericManager.ts": {
      "why": {
        "text": "Centralizes how Earth's atmosphere affects light: Kasten-Young air mass, per-channel Rayleigh optical depth converted to MAGNITUDES via the 2.5/ln10 Beer-Lambert factor (a fix over the earlier tau*X mislabel that under-corrected extinction), zenith flux multipliers, and the 'Cronos check' that validates observation time/geometry against the unset-clock forensics rule. Imported by ~3 modules; its extinction magnitudes feed color/zenith correction and its Cronos gate protects against bogus-clock phantom geometry.",
        "cites": [
          {
            "path": "src/engine/core/AtmosphericManager.ts",
            "lines": "15-53"
          },
          {
            "path": "src/engine/core/AtmosphericManager.ts",
            "lines": "68-99"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/ImageProcessor.ts": {
      "why": {
        "text": "The render-lane image utility (PIXEL/RENDER ledger): full-res browser decode, magnifier crop, the STF v2 auto-stretch (subtractive background neutralization + highlight/SPCC white balance + color-preserving MTF), the default-off Oklab stretch path, the per-body camera-matrix projection, and the single SIP undistort warp (LAW 1's one warp). Honest-or-absent throughout: with no color transform / no SPCC gains / no SIP the output is byte-identical to the plain-render path. Imported by the orchestrator, ingest stage, and format registry.",
        "cites": [
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "112-145"
          },
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "354-375"
          },
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "408-443"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/SkyTransform.ts": {
      "why": {
        "text": "The spherical-trig / projection engine: gnomonic (TAN) sky<->tangent-plane, bulk pixel->sky, and WCS synthesis/fit (createWCSTransform, fitWCS, upscaleWCS, pixelToSky). It is the seam where RA-HOURS/Dec-DEGREES are converted to radians for the wasm math kernels and back, and where the WCS struct's crval is stored as [RA hours, Dec degrees]. Imported by ~12 modules; if its unit handling or the offset-free fitWCS centroid trick regressed, every plate solve's scale/rotation would be wrong and the seestar/cr2 pinned solves would break.",
        "cites": [
          {
            "path": "src/engine/core/SkyTransform.ts",
            "lines": "1-6"
          },
          {
            "path": "src/engine/core/SkyTransform.ts",
            "lines": "107-131"
          },
          {
            "path": "src/engine/core/SkyTransform.ts",
            "lines": "159-225"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/StatisticsProvider.ts": {
      "why": {
        "text": "Centralized image statistics: iterative sigma-clipped background/noise (median + sigma), basic stats, per-channel histograms, and black-point estimation. The background estimator runs in WASM with an explicit CPU MAD-based iterative-sigma-clip fallback when the wasm call throws. Imported by ~6 modules (detection, background modelling); a wrong background/sigma shifts every detection threshold.",
        "cites": [
          {
            "path": "src/engine/core/StatisticsProvider.ts",
            "lines": "12-53"
          },
          {
            "path": "src/engine/core/StatisticsProvider.ts",
            "lines": "116-138"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/TimeService.ts": {
      "why": {
        "text": "Centralized temporal engine: Julian Date, Julian centuries/years since J2000.0, GMST/LMST, precession, proper motion, and RA/Dec<->Alt/Az. The rigorous transforms (precession, apparent alt-az) delegate to apparent_place.ts (IAU 2006/2000B); the legacy IAU 1982 GMST is kept only for standalone external callers. Imported by ~8 modules; a JD or precession error would mis-place ephemeris anchors and the zenith/alt-az hint seed, and (per the timestamp trap) a bogus clock feeding it yields phantom geometry.",
        "cites": [
          {
            "path": "src/engine/core/TimeService.ts",
            "lines": "22-28"
          },
          {
            "path": "src/engine/core/TimeService.ts",
            "lines": "40-65"
          },
          {
            "path": "src/engine/core/TimeService.ts",
            "lines": "95-158"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/UnitConverter.ts": {
      "why": {
        "text": "The single centralized converter for astronomical angle units — RA hours<->degrees (x15), degrees<->radians, degrees<->arcseconds, plus arcsec/pixel scale — so magic numbers (15, 3600, PI/180) never scatter through the pipeline. It encodes the load-bearing trap that RA is carried in HOURS internally: degToHours applies the standard 0-24 astronomical wrap. Without it every coordinate/scale consumer would re-derive these factors inline and a single wrong 15 or 3600 would silently shear every solve.",
        "cites": [
          {
            "path": "src/engine/core/UnitConverter.ts",
            "lines": "5-8"
          },
          {
            "path": "src/engine/core/UnitConverter.ts",
            "lines": "18-37"
          },
          {
            "path": "src/engine/core/UnitConverter.ts",
            "lines": "75-83"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/camera_color_matrix.ts": {
      "why": {
        "text": "Holds the per-body camera-RGB -> linear-sRGB forward color matrix (dcraw/LibRaw adobe_coeff ColorMatrix2 constants) so the render can rotate a scientific camera's native primaries into sRGB before the STF stretch, instead of assuming the science RGB was already sRGB. resolveColorTransform folds the implied D65 white balance into one 3x3 and returns null for bodies with no published matrix (e.g. SeeStar/IMX462), so the render falls back to the empirical white-balance path rather than fabricating a matrix. The 60Da entry is deliberately DISTINCT from the 60D (Halpha-extended red response) to avoid mis-projecting deep-red nebula color.",
        "cites": [
          {
            "path": "src/engine/core/camera_color_matrix.ts",
            "lines": "1-28"
          },
          {
            "path": "src/engine/core/camera_color_matrix.ts",
            "lines": "48-77"
          },
          {
            "path": "src/engine/core/camera_color_matrix.ts",
            "lines": "198-232"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/colormath.ts": {
      "why": {
        "text": "Minimal color-mathematics layer: sRGB<->XYZ (with the sRGB EOTF), xy<->XYZ, XYZ->Oklab, XYZ->CIE-Lab, and the Planckian-locus generator. Imported by ~5 modules for atmospheric-extinction color work and display-ready previews. Its srgbToXYZ is the transform the camera-matrix program flags as the historical 'assumed already sRGB' gap, so its correctness bounds any color-temperature or extinction estimate built on it.",
        "cites": [
          {
            "path": "src/engine/core/colormath.ts",
            "lines": "14-39"
          },
          {
            "path": "src/engine/core/colormath.ts",
            "lines": "60-122"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/oklab.ts": {
      "why": {
        "text": "Pure branch-free Oklab/OkLCh transforms (Ottosson 2020) for the RENDER layer only (PIXEL ledger) — linear sRGB<->Oklab, Oklab<->OkLCh, hue-preserving sRGB gamut projection, and the sRGB encode. By contract it never touches the LINEAR physics upstream of the stretch, the WCS, matched_stars, or any receipt value, and any lightness read off a post-transform pixel is render-only APPROXIMATE. Consumed solely by ImageProcessor's default-off Oklab stretch path.",
        "cites": [
          {
            "path": "src/engine/core/oklab.ts",
            "lines": "1-21"
          },
          {
            "path": "src/engine/core/oklab.ts",
            "lines": "88-120"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/optics_hint_provider.ts": {
      "why": {
        "text": "The untrusted-focal-length hint seam: when a file's nominal FL is the electronics-less factory-default 50 mm and the user gave no hint, the first non-null provider supplies a labelled ASSUMPTION (assumed:true, e.g. WIDE_FIELD_FL_PRIOR_MM = 14 mm) that seeds the scale search and is recorded in the receipt. It is the future plug point for the ML hint-recommender; because hints only seed and the math verify gate is the sole arbiter, a wrong hint can fail to verify but never corrupt a verified answer.",
        "cites": [
          {
            "path": "src/engine/core/optics_hint_provider.ts",
            "lines": "1-24"
          },
          {
            "path": "src/engine/core/optics_hint_provider.ts",
            "lines": "28-59"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/core/optics_manager.ts": {
      "why": {
        "text": "Owns the physical optics/atmosphere model: Brown-Conrady distortion apply/remove (used by SkyTransform's projection), FOV math, and the trust-ordered focal-length resolution that seeds the blind scale search. Imported by ~12 modules. The heuristic FL priors are named constants (never bare literals) routed through the hint-provider seam so an assumption can never leak into the UI as a measured value; a break here mis-seeds the scale search or double-applies distortion.",
        "cites": [
          {
            "path": "src/engine/core/optics_manager.ts",
            "lines": "1-8"
          },
          {
            "path": "src/engine/core/optics_manager.ts",
            "lines": "24-70"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/events/capture_aggregate.ts": {
      "why": {
        "text": "Folds many per-run capture records into the solve-flowchart's hover statistics (per-stage reached/passed/failed and fastest-average-slowest timing over successful solves). Deduped by frame content hash per the owner mandate that repeat runs of one frame must not skew the stats; a null sha is counted distinct (never merged) and flagged. Honest-or-absent: a stage with no timing sample yields null min/avg/max, rendered NOT MEASURED. Without it the flowchart's aggregate view has no source and repeat-run skew would corrupt the pass/fail rates.",
        "cites": [
          {
            "path": "src/engine/events/capture_aggregate.ts",
            "lines": "28-61"
          },
          {
            "path": "src/engine/events/capture_aggregate.ts",
            "lines": "105-165"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/events/capture_persist.ts": {
      "why": {
        "text": "Mirrors each completed run into a versioned, bounded localStorage blob so the flowchart's local aggregate spans every run this box has seen across reloads and sessions, not just the in-memory ring. Pure consumer of the capture pipeline: it registers a sink and reads the in-memory store, never touching emission or the solve. Versioned (a schema bump invalidates a stale blob instead of crashing the aggregate), bounded (oldest runs dropped past the cap), and non-fatal (a corrupt blob degrades to an empty sample). Without it the local flowchart aggregate resets to in-memory-only runs on every reload.",
        "cites": [
          {
            "path": "src/engine/events/capture_persist.ts",
            "lines": "31-79"
          },
          {
            "path": "src/engine/events/capture_persist.ts",
            "lines": "97-110"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/events/capture_record.ts": {
      "why": {
        "text": "Turns the ephemeral event ring into a durable per-stage capture record (one CaptureEnvelope per stage per run) that the interactive flowchart and replay dashboard consume. CaptureRecorder is a non-fatal bus subscriber; on run_finished it builds the record and stores it in the bounded in-memory ring, mirrors it to a guarded window object, and invokes every registered sink. The frame_sha write-race barrier (awaitFrameSha) holds the flush until the async content-hash digest settles, so a fast run never persists a record with a null dedup key. Without it the flowchart/replay substrate and persisted run history have no per-stage data, and dedup integrity breaks on quick solves.",
        "cites": [
          {
            "path": "src/engine/events/capture_record.ts",
            "lines": "31-56"
          },
          {
            "path": "src/engine/events/capture_record.ts",
            "lines": "197-271"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/events/pipeline_events.ts": {
      "why": {
        "text": "The typed pipeline event bus and its event union (run/stage begin-end-progress, findings, warnings, provenance changes). The bus stamps each emitted event with t/seq plus the per-run runId/frameSha context and fans out to subscribers behind a per-subscriber try/catch, so a throwing subscriber can never break emission or starve others; a ring-buffer history lets late subscribers replay. Without it every downstream fold goes dark: the capture record, stage_records, stage_timings, the solve flowchart, and the UI event feed all consume this one stream.",
        "cites": [
          {
            "path": "src/engine/events/pipeline_events.ts",
            "lines": "99-124"
          },
          {
            "path": "src/engine/events/pipeline_events.ts",
            "lines": "176-213"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/events/stage_timing_summary.ts": {
      "why": {
        "text": "Pure fold over the run's event stream into one per-run timing summary: a per-stage ms map plus a total wall span computed as max(stage_end) - min(stage_start) (a SPAN basis, not the sum of stage ms, so nested stages like the calibrate umbrella wrapping psf_field/bc_rematch/forced_confirm are never double-counted and the browser and headless consumers compute an identical comparable total). It is the substrate that finally persists a clocked per-substage number (efficiency review flagged that this timing was computed every run then discarded). It is persisted to a perf JSONL sidecar by the headless/e2e consumers and also carried on the receipt as the envelope-class stage_timings block. Without it every perf claim stays structural/inferred rather than clocked.",
        "cites": [
          {
            "path": "src/engine/events/stage_timing_summary.ts",
            "lines": "45-75"
          },
          {
            "path": "src/engine/events/stage_timing_summary.ts",
            "lines": "96-129"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/export/asdf_writer.ts": {
      "why": {
        "text": "The one dependency-free ASDF serializer (desktop, browser and headless all run this same serializeAsdf): the image as a little-endian binary block plus the full receipt as YAML, plus a native astropy-interpretable GWCS transform (LINEAR + SIP, or a TPS tabular lookup when a spline is fitted). Honest-or-absent: the WCS blocks are absent when unsolved, and a present-but-non-FITTED WCS refuses loudly while an absent one is allowed (the asymmetry vs fits_writer is deliberate - ASDF is the full-receipt carrier, FITS the solved-science carrier). Both distortion carriers export the FITS/astropy sign direction (SIP via toFitsSip; the TPS tabular LUT bakes u - f using the fitter's own evalTpsField so nothing drifts). Without it there is no ASDF/GWCS export, or a backwards distortion if the u - f sign is re-broken.",
        "cites": [
          {
            "path": "src/engine/pipeline/export/asdf_writer.ts",
            "lines": "26-58"
          },
          {
            "path": "src/engine/pipeline/export/asdf_writer.ts",
            "lines": "144-165"
          },
          {
            "path": "src/engine/pipeline/export/asdf_writer.ts",
            "lines": "579-627"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/export/fits_writer.ts": {
      "why": {
        "text": "The one dependency-free FITS serializer (desktop, browser and headless all run this same serializeFits). It consumes receipt.wcs VERBATIM (already degrees, CD in deg/px, 0-based crpix per generateReceiptWcs) so it must never re-multiply CRVAL by 15 (a double conversion); the only coordinate arithmetic is CRPIX+1 to reach FITS 1-based. It enforces the export law (refuses an absent or non-FITTED WCS, and any non-finite WCS keyword), runs SIP coefficients through toFitsSip so a reader corrects toward the catalog, and preserves NaN as the FITS blank (differs from the stacker's 0-fill). Without it there is no FITS export, or a corrupt / backwards-distortion / double-scaled WCS if the unit and SIP-sign traps are re-broken.",
        "cites": [
          {
            "path": "src/engine/pipeline/export/fits_writer.ts",
            "lines": "20-54"
          },
          {
            "path": "src/engine/pipeline/export/fits_writer.ts",
            "lines": "99-153"
          },
          {
            "path": "src/engine/pipeline/export/fits_writer.ts",
            "lines": "204-234"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/export/sip_convention.ts": {
      "why": {
        "text": "The single place the engine's internal SIP convention (A_internal = OBSERVED - IDEAL, as stored on receipt.solution.astrometry.sip) is negated to the FITS-standard forward convention (A_FITS = IDEAL - OBSERVED = -A_internal). A pure coefficient negation that returns a fresh block and never mutates the receipt; imported by BOTH export serializers so neither can re-derive the sign wrong. Without it (the pre-fix M7 bug) the raw internal coefficients get emitted as FITS A_i_j and astropy applies the distortion backwards, WORSENING catalog residuals instead of improving them; in-app consumers use the internal convention end-to-end and must not be touched.",
        "cites": [
          {
            "path": "src/engine/pipeline/export/sip_convention.ts",
            "lines": "10-54"
          },
          {
            "path": "src/engine/pipeline/export/sip_convention.ts",
            "lines": "85-92"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/nebulosity_layer.ts": {
      "why": {
        "text": "Multiscale starlet (a-trous / IUWT) decomposition of a native-grid luminance frame into an additive-complete layer set (star, nebulosity, sky_gradient, residual) whose sum reconstructs the input within float epsilon. It elevates rl_deconv's single-scale diffuse stub to a first-class multiscale render layer and is honest-or-absent (nebulosity.data is null below the support floor). It is a DEFAULT-OFF render producer with no stage wired into the solve path, so the receipt nebulosity_layer block is null on every real receipt today; without it that render decomposition and its receipt block do not exist.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/nebulosity_layer.ts",
            "lines": "1-57"
          },
          {
            "path": "src/engine/pipeline/m10_psf/nebulosity_layer.ts",
            "lines": "294-440"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/psf_core.ts": {
      "why": {
        "text": "Shared PSF measurement primitives on the native pixel grid: moment-based measureStar, robustStats, pixel-noise sigma, local-maxima finder, the sigma-clipped empirical-kernel stacker, and the 3x3 region-grid helper. It is imported by psf_field, psf_stage and rl_deconv, so it is the single home for the moment maths (ported byte-for-byte from the tools/psf lane, LAW 4). Without it psf_field has no seed/fallback measure and psf_stage cannot build the RL deconvolution kernel.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_core.ts",
            "lines": "1-16"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_core.ts",
            "lines": "126-183"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/psf_field.ts": {
      "why": {
        "text": "Entry point of the PSF-field measurement: characterizePsfField fits a per-star 2D Gaussian (compiled wasm refine_stars_lm) at the SOLVED star positions on the native pixel grid and returns the FWHM / ellipticity / orientation field plus a 3x3 coma map. Without it there is no measured PSF for psf_attribution to decompose against, no frame-PSF shape reference for forced_confirm, and the receipt psf_field block is empty. It degrades honestly to a moment measure when no compiled fitter is present rather than fabricating a fit.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "1-27"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "270-313"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/psf_physics.ts": {
      "why": {
        "text": "Pure closed-form celestial-mechanics and optics predictors in engine-native arcsec (sidereal drift, diffraction floor, seeing, coma-form fit, atmospheric dispersion, and the CD-to-Jacobian pixel-to-sky shape deprojection). It reads no pixels and mutates no solve; psf_attribution compares these predictors against the measured psf_field, and stages/science reuses targetGeometry/airmassFromAltitude for SPCC airmass provenance. Without it psf_attribution cannot attribute measured anisotropy to physical systematics and SPCC loses its airmass basis.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_physics.ts",
            "lines": "1-31"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_physics.ts",
            "lines": "122-135"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/psf_stage.ts": {
      "why": {
        "text": "The optional post-solve PSF measurement + deconvolution stage (runPsfStage): a cheap measurement pass powers the text FWHM/ellipticity stats, then a default-on deconvolution lane stacks an empirical kernel and runs protected RL on bounded windows around bright/smeared targets. It is strictly optional (nothing calls it unless the user asks for PSF diagnostics) and its absence changes no solve. Without it the PSF diagnostics panel and the deconvolution strip have no producer.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_stage.ts",
            "lines": "1-25"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_stage.ts",
            "lines": "220-332"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/rl_deconv.ts": {
      "why": {
        "text": "Windowed damped Richardson-Lucy deconvolution on native-grid luminance windows, plus the nebulosity-protected variant that splits an extended diffuse floor (box blur at a scale much larger than the PSF) from the compact star residual, sharpens only the compact part, and adds the diffuse back verbatim. psf_stage's live deconv lane calls the protected variant; the raw engine and dilateMask are also reused by nebulosity_layer. Without it the (default-off) deconvolution lane and the diffuse/compact separation do not exist.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/rl_deconv.ts",
            "lines": "1-22"
          },
          {
            "path": "src/engine/pipeline/m10_psf/rl_deconv.ts",
            "lines": "143-183"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m10_psf/vignette_map.ts": {
      "why": {
        "text": "Fits a PER-BAND vignette/transmission map (gain(r)=1+a2*r^2+a4*r^4 from the frame's own sky) and evaluates gainAt(x,y,band) as a per-star flux-recovery divisor. It is the measurement-level optical-falloff correction consumed four ways (psf_field amp on luma, SPCC on r/g/b, forced photometry on luma, receipt serialization); the per-band fit is load-bearing because a single achromatic gain cancels in color ratios and would not correct color. It refuses under-constrained fits (valid=false + refusal) so a consumer skips rather than applying identity-with-noise. Default-OFF at the consumer gate, so absence is byte-identical.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/vignette_map.ts",
            "lines": "1-39"
          },
          {
            "path": "src/engine/pipeline/m10_psf/vignette_map.ts",
            "lines": "129-213"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m1_ingestion/format_registry.ts": {
      "why": {
        "text": "The single source of truth for which formats the instrument can ingest: magic-byte dispatch where the first positive sniff wins (so CR2's TIFF+'CR' signature must precede the bare-TIFF fallback), plus the science-vs-demo honesty tier every downstream surface reads. Both the pipeline and the UI derive from this one table, so there are no scattered extension checks. Without it, adding or reordering a format would require touching the front end and could mis-route a CR2 to the demo raster decoder or over-claim calibrated radiometry on an 8-bit demo frame.",
        "cites": [
          {
            "path": "src/engine/pipeline/m1_ingestion/format_registry.ts",
            "lines": "88-124"
          },
          {
            "path": "src/engine/pipeline/m1_ingestion/format_registry.ts",
            "lines": "305-311"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m1_ingestion/metadata_reaper.ts": {
      "why": {
        "text": "The ingest front door: a unified forensic EXIF/FITS autopsy that turns a raw file buffer into HardMetadata plus the sensor pixel payload, and routes the RAW decode arm. It is honest-or-absent by construction — an absent capture time becomes '' (never wall-clock now, which would silently pass the ephemeris gate) and an absent lens becomes the truthy placeholder 'Unknown Lens'. Without it nothing downstream has metadata or pixels to work on, and the honest-absent defaults are what keep phantom planets and lying optics out of the solve.",
        "cites": [
          {
            "path": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
            "lines": "40-53"
          },
          {
            "path": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
            "lines": "90-141"
          },
          {
            "path": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
            "lines": "187-207"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m1_ingestion/rawler_decoder.ts": {
      "why": {
        "text": "The DEFAULT RAW decode arm since the 2026-07-11 cutover (#14): it decodes the full-frame Bayer CFA in a wasm crate and integer-demosaics the active area into normalized Float32 RGB, keeping the value in the raw-ADU-over-65535 domain (no black-subtract or white-scale by default, LAW 2). The libraw path is retained as a cold fallback selected by VITE_DECODER_RAWLER=0. Without this module the default RAW ingest has no decoder; a wrong flag read would silently swap decode contracts, so the flag defaults to true and any read error also returns true.",
        "cites": [
          {
            "path": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts",
            "lines": "64-76"
          },
          {
            "path": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts",
            "lines": "503-595"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m2_hardware/identifier_matcher.ts": {
      "why": {
        "text": "The one canonical identifier->registry lookup (camera body / lens / rig tag), so the loose-substring bug class cannot recur: exact-first, then overlap-scored substring, then a residual-token guard that refuses a confident match when the query drops or adds a distinguishing token (R6 vs R6 II, T7 vs T7i). Ambiguity resolves to an honest null over a wrong entry. Without this shared ladder, a longer wrong body outranked an exact hit and mis-resolved 5D Mark II to the Mark III profile — a silent scale error.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/identifier_matcher.ts",
            "lines": "88-95"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/identifier_matcher.ts",
            "lines": "113-154"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m2_hardware/lens_distortion.ts": {
      "why": {
        "text": "The lens-distortion prior as COORDINATE-ledger functions: radial Brown-Conrady (k1,k2) that moves star POSITIONS so a barrel-warped frame's asterism code lines up with the rectilinear catalog before quad formation, with the native->corrected inverse solved by a 10-iteration radial fixed point; the original native pixels are kept for photometry (never an image resample). resolveLensDistortion returns a NO-OP (null) unless a lens is confidently resolved and rejects the placeholder/lying EXIF, so it must return null for the bundled CR2 whose 50mm/Unknown-Lens EXIF lies. Without that placeholder guard the prior would fire on a lying EXIF and break the byte-identical-by-construction guarantee.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
            "lines": "64-106"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
            "lines": "302-372"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m2_hardware/optics_resolver.ts": {
      "why": {
        "text": "The EXIF-derived pixel-scale entry point that kills the slow blind Tri-Lock default for DSLR frames: when the file carries no header scale but does carry an EXIF focal length and a known sensor body, the scale is plain geometry (206.265 * pitch_um / FL_mm). It returns null when the focal length is absent, the camera model is empty (an empty search would substring-match every profile), or no sensor profile exists — falling back honestly to the blind path. The focal length is nominal, so the result seeds the solver but does not earn the FITS-header trust skip.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/optics_resolver.ts",
            "lines": "54-90"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m2_hardware/scale_manager.ts": {
      "why": {
        "text": "The central coordinate/scale translator between three resolution spaces: NATIVE sensor pixels, the 2x2-binned science buffer used for detection, and the browser preview canvas. It converts both (x,y) positions and arcsec/px scales across those spaces (the science scale is roughly double the native because the buffer is half-size each axis). Without it, detected stars, overlays, and the pixel scale would be reported in the wrong resolution space and misalign with the sensor grid.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/scale_manager.ts",
            "lines": "24-39"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/scale_manager.ts",
            "lines": "78-95"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m2_hardware/sensor_db.ts": {
      "why": {
        "text": "The camera profile database: per-body pixel pitch, QE curve, RGB->XYZ color matrix, read noise, and vendor gain curves, looked up by camera-body string. The solve-critical field is pixel_size_um — a full-frame body silently defaulting to the APS-C 4.30um pitch produces roughly a 45% scale error that pins an anchored sweep off the sky, so lookups are exact-first and return null on ambiguity rather than guessing. getGainForSetting converts a ZWO gain SETTING to e-/ADU (dividing by 2^adu_bit_shift for the 12->16-bit ADC expansion). Without accurate pitch the EXIF scale rung and every photometric calibration are wrong.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/sensor_db.ts",
            "lines": "132-142"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/sensor_db.ts",
            "lines": "647-655"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/sensor_db.ts",
            "lines": "717-735"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/AtmosphericPhysics.ts": {
      "why": {
        "text": "Measures a vertical top-vs-bottom background brightness gradient and a per-star Mie halo ratio. Both are REPORTED ONLY — recorded on each star (rayleigh_index / mie_index) and on the packet (background_level_top/bottom) — and are never wired into any detection threshold. The header is explicit that the vertical slope is UNATTRIBUTED (light pollution, altitude gradient, or vignetting produce the same slope), which is why the packet deliberately omits any scattering_type field. Without it those honest display/receipt gradient fields are absent.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/AtmosphericPhysics.ts",
            "lines": "10-16"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/AtmosphericPhysics.ts",
            "lines": "23-55"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/culling_stats.ts": {
      "why": {
        "text": "Reconciles the three populations a culled star can land in for the step-3 trust surface: visible (anomalies[] + planet_candidates[], drawable by an overlay toggle) versus dropped (hard-rejected in m4 — only the assignment-time tally knows). It never invents a dropped count: a pre-tally packet or a reason missing from the tally yields dropped=0. Without it the UI per-reason counters would read only anomalies[] and under-report rejected and planet-routed stars to near zero.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/culling_stats.ts",
            "lines": "1-12"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/culling_stats.ts",
            "lines": "32-63"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/detection_cuts.ts": {
      "why": {
        "text": "Per-blob thermal-noise discriminators measured TS-side (sharpness = peak/flux, 2nd-moment FWHM and ellipticity on an 11x11 stamp) that stay honest where the WASM fwhm field saturates near ~1.5px for tiny blobs. They cut hot-pixel/dark-current junk from uncooled high-ISO DSLR frames while being calibrated OUTSIDE the clean reference frames' distributions, so they are a no-op there. A null statistic never rejects (honest-or-absent). Without it thermal spikes survive into the star list on warm DSLR frames.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_cuts.ts",
            "lines": "1-43"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_cuts.ts",
            "lines": "147-212"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/detection_flatten.ts": {
      "why": {
        "text": "Frame-measured vignette/background flattening for the DETECTION-luminance copy ONLY (fits gain(r)=1+a2 r^2+a4 r^4 against the frame's own sky, and subtracts a deg-2 background surface), returning a NEW buffer and never mutating the input. The owner two-plane law: the corrected buffer reaches detection thresholds and blob extraction only, never photometry/receipts/render — those keep native pixels. It returns null (honest-or-absent) when the frame is too sparse to fit. Both consuming flags are default-OFF, so it is byte-identical today; it exists to make the detection-side vignette lever honest and recordable.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_flatten.ts",
            "lines": "1-35"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_flatten.ts",
            "lines": "117-215"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/detection_guard.ts": {
      "why": {
        "text": "A pure fast-fail predicate that runs the instant both candidate counts are known, before the O(deep x vanguard) dedup and per-star window passes. On a pathological frame (pure noise, mis-binned mosaic, unsupported sensor) those passes grind for minutes — the ~470s Carina-class silent hang. It trips only when the deep count exceeds an absolute floor AND the per-megapixel density exceeds the pathological threshold, so any real (even dense) frame is double-protected. Without it a bad frame hangs the pipeline silently instead of failing loud and fast.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_guard.ts",
            "lines": "1-18"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_guard.ts",
            "lines": "45-70"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts": {
      "why": {
        "text": "Derives the terrain silhouette from the negative space of the star field: walking columns left to right, the lowest sky detection that has enough sky SUPPORT above it traces the horizon — working precisely where intensity segmentation is blind (pitch-black foreground). It also emits the evidence gate (coverage + below-region density contrast) that decides whether terrain exists at all; a full-sky tracked frame yields no evidence, so topography culling stays off. Without it there is no evidence-based foreground rejection and the step-3 horizon overlay has nothing to draw.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts",
            "lines": "1-17"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts",
            "lines": "51-90"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts",
            "lines": "145-165"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/hot_pixel_map.ts": {
      "why": {
        "text": "Pre-extraction single-pixel spike masking on a detection-support COPY (the science buffer that feeds photometry/PSF is never mutated). The discriminator flags a pixel only when it spikes above its 8-neighbour median AND those neighbours sit at background — a real star leaks PSF signal into its ring, a hot pixel does not. Application is gated on measured spike DENSITY (the ~200x separation between a clean DSLR's few real hot pixels and a thermal-dominated frame), and master-dark subtraction is preferred when a dark exists. Without it hot pixels seed false blobs and drag centroids.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/hot_pixel_map.ts",
            "lines": "1-30"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/hot_pixel_map.ts",
            "lines": "79-147"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/luminance_reduce.ts": {
      "why": {
        "text": "Reduces an interleaved RGB frame to the single-channel luminance the detector reads, under two weightings: REC709 perceptual weights for a genuinely demosaiced frame, and EQUAL (R+G+B)/3 ONLY for a LibRaw CFA-mosaic 'RGB' where each pixel carries one dominant CFA colour. On a CFA mosaic the perceptual weights (0.72 on green sites vs 0.07 on blue) imprint a 2px period-2 checkerboard on the detection buffer; equal weights recover the smooth per-site value. Choosing the wrong weighting for a CFA-mosaic frame corrupts detection with a Nyquist checkerboard.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/luminance_reduce.ts",
            "lines": "1-25"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/luminance_reduce.ts",
            "lines": "31-44"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/signal_processor.ts": {
      "why": {
        "text": "The detection entry point (the pipeline's 'eye'): turns a luminance buffer into categorized SignalPoints — clean stars, anomalies, planet candidates — plus the per-reason culling_tally counted at ASSIGNMENT time (so hard-rejected and planet-routed stars are counted, not just what lands in anomalies[]). It orchestrates every other m4 helper (hot-pixel masking, thermal cuts, density guard, horizon envelope, atmospheric/MW passes) and restores detections from the 2x2-binned science grid to native sensor coordinates. Without it there is no star list to solve against and no measured detection trust surface.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "1-16"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "311-339"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "277-305"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "758-780"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m4_signal_detect/source_extractor.ts": {
      "why": {
        "text": "The second detection lane: shared connected-component blob extraction + centroiding (WASM extract_blobs_shared, with a slice-API and a crude JS fallback) used by the auto path and the solver's re-extraction, plus the sigma-clipped background estimator and the RGBA->luminance converter. It applies the same thermal hot-pixel prepass and per-blob cuts as the wizard lane, then culls satellites/planets/light-pollution. Without it those non-wizard paths would have no source detection.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/source_extractor.ts",
            "lines": "46-65"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/source_extractor.ts",
            "lines": "106-263"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/confirm_status.ts": {
      "why": {
        "text": "Pure classifier that turns the already-computed deep_confirmed block into ONE four-state verdict (CONFIRMED / REFUSED / INSUFFICIENT_TARGETS / NOT_RUN) so the UI and the receipt read the same conclusion. It owns no calibrated constant (the set-gate Z is cited in from the call site). Without it a solve whose confirmation was refused or never run would still display as a bare 'solved' with a false confidence number.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/confirm_status.ts",
            "lines": "19-43"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/confirm_status.ts",
            "lines": "97-138"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/deep_verify.ts": {
      "why": {
        "text": "The forced-photometry primitives for the solver: forcedMeasure (matched-aperture flux at FIXED catalog positions), projectCatalogToPixels (HOURS sky -> pixel), the deterministic scrambled-null sampler, binomialExcessZ, and deepVerifyEscalation. It is the shared math for the ultra-wide escalation, the post-solve harvest, and the confirmation gate. Without it the sub-threshold ultra-wide rescue and every CATALOG_FORCED product lose their statistic.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
            "lines": "87-108"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
            "lines": "209-249"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
            "lines": "437-497"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/fdr_confirm.ts": {
      "why": {
        "text": "The N-invariant false-discovery-rate confirmation statistic (empirical right-tail p-values against the scrambled-null SNR pool, Benjamini-Hochberg / Benjamini-Yekutieli step-up). Since the 2026-07-15 sigma-swap it is the PRIMARY verdict driver (confirm_status reads its would_be_status when present); the legacy set-excess z is known to be proportional to sqrt(N)-malformed and is kept only as a diagnostic fallback. Without it the confirm verdict reverts to that malformed gate.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/fdr_confirm.ts",
            "lines": "77-83"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/fdr_confirm.ts",
            "lines": "105-120"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/forced_confirm.ts": {
      "why": {
        "text": "The set-level family-wise confirmation gate (confirmForcedSet + the per-star conjunction confirmForcedStar). It promotes CATALOG_FORCED candidates to CATALOG_FORCED_CONFIRMED only when the real confirmed count beats a frame-wide scrambled null at a binomial excess, otherwise the whole set collapses to zero. Without it the ~2 sigma candidate floor would be laundered into 'confirmed' across ~500 candidates and the phantom-confirmation rate per solve would be unbounded.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
            "lines": "289-332"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
            "lines": "401-475"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/solver_entry.ts": {
      "why": {
        "text": "The M6 plate-solve entry point: solvePlate (and the autoSolvePlate facade) run the whole blind/quad/ultra-wide solve orchestration, and it is imported by orchestrator_session, stages/solve.ts, stages/solve_context.ts, Mainlogic_entry, and most tools/ harnesses. Without it there is no plate solve at all — the wizard step-4/5 has no solver to call.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "229"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "3061-3062"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/standard_stars.ts": {
      "why": {
        "text": "Defines the StandardStar type (ra_hours in decimal HOURS, dec_degrees, magnitude_V, B-V color index) and the built-in photometric reference catalog, and is imported across the solver, planetary adapter, starplates provider, ultra-wide tiers, and m8 color. It is the shared row shape every catalog consumer projects and matches; without it the solve and color lanes have no common reference-star type.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/standard_stars.ts",
            "lines": "24-30"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/standard_stars.ts",
            "lines": "86"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts": {
      "why": {
        "text": "Singleton catalog access (getinstance / loadCatalog / ensureSectorLoaded / findStarsInField). It is the single place the hybrid Gaia-degrees / HYG-hours atlas rows are normalized to internal RA HOURS, and it is imported by ~15 consumers (solver_entry, uw_deep_evidence, m7 metrology, m8 color_index, and many tools). Break it and there is no reference catalog to match against, and no forced-photometry positions to project.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
            "lines": "386-396"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
            "lines": "403-418"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
            "lines": "869"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/tps_eval.ts": {
      "why": {
        "text": "The pure thin-plate-spline forward evaluator (kernel U(r)=r^2 ln r + affine + spline sum), with ZERO imports. It is shared verbatim by tps_fitter.ts (the wasm-bearing engine fit-core) and export/asdf_writer.ts (a deliberately engine-free leaf that must run under plain tsx). Keeping it here is what stops the ASDF byte path from dragging wasm onto its import chain and keeps the two consumers from forking the spline math (LAW 4).",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_eval.ts",
            "lines": "1-11"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_eval.ts",
            "lines": "26-33"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/tps_fitter.ts": {
      "why": {
        "text": "Fits a regularized thin-plate spline to the matched-star residual field and appends it to solution.astrometry.tps — the non-polynomial companion to the SIP fitter and the representation ASDF/GWCS carries as a lookup table. It is a coordinate-space fit over already-solved pairs: it never resamples pixels and never mutates WCS / matched_stars / confidence, so both sacred solves stay byte-identical. Without it there is no spline distortion carry for frames the SIP polynomial cannot follow.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
            "lines": "1-19"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
            "lines": "21-39"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts": {
      "why": {
        "text": "The ultra-wide fit-tight-reverify escalation tier: the wide-net gate saturates below the +5 sigma bar even for TRUE solutions on huge fields because the radius-inflated net lets chance track matches, so this tier FITS Brown-Conrady distortion on provisional matches then RE-VERIFIES through a tight net (with an inner-region pinhole fallback). buildMatch also carries the load-bearing HOURS->DEGREES conversion (catalog.ra = ra_hours*15). Without it true ultra-wide solutions are rejected at the wide gate.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
            "lines": "1-37"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
            "lines": "112-131"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
            "lines": "317-388"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/orchestrator_session.ts": {
      "why": {
        "text": "The single live wizard orchestrator (the auto-path orchestrator.ts was deleted). It holds per-run session state and sequences every shared post-C1 stage in order — decode, detect, metrology, hint+solve, calibrate, SPCC, PSF characterize/attribute, package — feeding each stage's output into the next, and it owns buffer lifetimes (it nulls the raw buffer after ingest, which is why step-1 re-entry is forbidden by design). Without it no wizard solve runs and no receipt is assembled.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "35-62"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "880-1471"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/calibrate.ts": {
      "why": {
        "text": "Post-solve COORDINATE calibration: runs the M7 residual analysis + SIP fit (landed on solution.astrometry, admitted only through an out-of-sample CV gate), the TPS distortion fit (same fire gate then its own CV gate), and the hardware / spectral-forensics profile built from the matched stars' peeked per-channel samples. Non-fatal by contract — any failure warns and returns null, never killing the pipeline. Without it the receipt carries no residual RMS, distortion model, or hardware profile.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "60-127"
          },
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "150-183"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/detect.ts": {
      "why": {
        "text": "Runs star extraction, culling and curation, producing the SignalPacket (clean_stars / anomalies / culling_tally) that seeds metrology and the solver. It chooses native-Bayer (2x2-binned Uint16) vs luminance detection via the exported isNativeBayer predicate and forwards the curated float-precision detections to the solver on the one existingStars parameter (landmine #3) so the solver does not re-extract from an 8-bit buffer. Without it the solver has no stars to match.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "143-196"
          },
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "99-104"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/ingest.ts": {
      "why": {
        "text": "Decodes the source bitstream into science buffers (the rawSensor payload plus the interleaved linear RGB frame) — the sole producer of the pixels every later stage measures. It selects the decode arm (rawler default / libraw cold path / browser JPEG / FITS), builds real demosaic params from the decoded header instead of the Canon 14-bit RGGB default, and honestly refuses a phase-wrong color demosaic (grayscale, RGB NOT MEASURED) when the Bayer phase is unknown. Without it there are no pixels to detect, solve, or photometer.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "200-329"
          },
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "107-153"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/metrology.ts": {
      "why": {
        "text": "Locks the pixel scale (arcsec/px) through the trust ladder FITS-header optics -> EXIF optics + sensor-DB pitch -> blind Tri-Lock, and builds the ephemeris guest list gated on BOTH a trusted clock and a real observer site. The scale it produces seeds the solver's scale prior; a wrong or missing scale poisons the blind solve. On the EXIF rung it mutates metadata.pixel_scale / pixel_pitch_um so downstream stages see a real scale instead of a blind default.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/metrology.ts",
            "lines": "80-128"
          },
          {
            "path": "src/engine/pipeline/stages/metrology.ts",
            "lines": "135-147"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/package.ts": {
      "why": {
        "text": "Assembles the wizard receipt (the Toolchest API product) from both ledgers while mutating neither: the FITTED WCS (never a re-synthesized approximation), matched stars with 2D residual vectors, per-star photometry, SPCC / PSF / hardware blocks, pipeline provenance, and the reproducibility envelope. It emits RECEIPT_SCHEMA_VERSION rather than hand-pinning a number, and every block is honest-or-absent. Without it a solved run produces no serialized output.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "137-195"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "443-558"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/psf_attribution.ts": {
      "why": {
        "text": "Decomposes the MEASURED PSF field into physically-explained components (sidereal drift, diffraction, seeing, differential refraction, coma) plus an unexplained residual, labeling each by epistemic tier (CALCULATED / CONFIRMED / FITTED / APPROXIMATE / INFERRED / NOT_MEASURED). Physics informs and guides but never overrides the measurement; the stage never mutates psf_field, matched_stars, the WCS, or the solve, so it is purely additive. Without it the elongation in the PSF field is unattributed.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "1-32"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "72-116"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/psf_characterize.ts": {
      "why": {
        "text": "Measures the spatially-varying PSF (per-star 2D-Gaussian Levenberg-Marquardt) at the solved matched-star positions on the science buffer's OWN grid — native, or 2x2-binned when the Bayer-native path produced a binned buffer — never resampling before measurement (PIXEL ledger). It returns null on honest absence (no solve / no buffer / incoherent dims) so the receipt records psf_field:null rather than a fabricated map. Without it the receipt has no measured FWHM/ellipticity field.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_characterize.ts",
            "lines": "43-78"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/schema_versions.ts": {
      "why": {
        "text": "The single home for RECEIPT_SCHEMA_VERSION — the wizard receipt / Toolchest API contract version consumed as v2.x by downstream shape-discovery. Three product versions circulate in the codebase and are deliberately NOT unified to one number; this file documents that and warns that bumping the receipt version without a real schema change is dishonest versioning. Without this single source, the emitted receipt version could drift across producers.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/schema_versions.ts",
            "lines": "1-25"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/science.ts": {
      "why": {
        "text": "Runs SPCC spectrophotometric calibration — aperture photometry on the retained linear science RGB regressed against catalog color/magnitude — plus per-star calibrated magnitudes and the honest field-center airmass. It is honest-or-absent throughout: it refuses gray/mono frames (REFUSED_GRAY / UNCALIBRATED_CFA), marks <8-usable-star fits UNCALIBRATED, and records airmass absent rather than the legacy fabricated 1.0. Without it the receipt has no color / zero-point calibration.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "284-360"
          },
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "58-77"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/solve.ts": {
      "why": {
        "text": "Resolves the wizard's search hints through the shared ladder (CONFIG -> FITS_HEADER -> ZENITH -> BLIND, RA in HOURS / Dec in DEGREES, azimuth sentinel guarded out) and is the single entry point wrapping autoSolvePlate for both pipelines, forwarding curated detections on the one existingStars parameter. It also emits the per-branch flowchart events read back from the solver's own forensics. Without it there is no plate solution (WCS + matched_stars).",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve.ts",
            "lines": "80-149"
          },
          {
            "path": "src/engine/pipeline/stages/solve.ts",
            "lines": "235-244"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/pipeline/stages/solve_context.ts": {
      "why": {
        "text": "Builds the canonical SolveContext consumed by autoSolvePlate — one option contract for both pipelines that can no longer fork — and computes the narrow-field verifyWCS relaxation (field diagonal < 6 deg -> 4 anchor matches @ 0.5 confidence). Callers may not invent extra keys. A drifted or hand-rolled context would silently change solver gates, so centralizing it here keeps the two pipelines' solve behavior identical.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve_context.ts",
            "lines": "57-71"
          },
          {
            "path": "src/engine/pipeline/stages/solve_context.ts",
            "lines": "134-152"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/PipelineWizard.tsx": {
      "why": {
        "text": "The 7-step wizard modal — the app's only interactive solve surface. It creates/drives the OrchestratorSession through usePipelineFSM, owns the step-2 metadata form (USER-provenance optics + UTC time-trust parse), and hosts the DEFAULT-OFF render-plane toggles (Oklab, corrected-view) that never touch the solve. Without it there is no interactive path to run a solve or edit observation metadata.",
        "cites": [
          {
            "path": "src/engine/ui/PipelineWizard.tsx",
            "lines": "161-194"
          },
          {
            "path": "src/engine/ui/PipelineWizard.tsx",
            "lines": "216-257"
          },
          {
            "path": "src/engine/ui/PipelineWizard.tsx",
            "lines": "750-804"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/calibration/chart_math.ts": {
      "why": {
        "text": "Pure, React-free math for the step-6 instrument charts: nice-tick placement, honest coefficient formatting (fmtCoef returns em-dash for absent, never a fake 0), the Brown-Conrady radial-shift model in pixels, the vignette relative-illumination model, and the labeled quiver magnification. Without it the distortion/vignette curves and residual quiver scale cannot be drawn from measured values.",
        "cites": [
          {
            "path": "src/engine/ui/calibration/chart_math.ts",
            "lines": "9-72"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/calibration/quiver_model.ts": {
      "why": {
        "text": "Data prep for the step-6 residual vector field: projects every verified matched star through the LINEAR WCS (the same ResidualAnalyzer convention M7 used) and derives honest display stats. Both the live-solution builder and the receipt-fed builder finalize through ONE stats tail (LAW 4), so the step-6 quiver and the Widget-Shelf quiver are statistically identical, and both return null below a 15-arrow floor rather than a sparse fake field. Without it the residual field cannot be shown or kept consistent across the two surfaces.",
        "cites": [
          {
            "path": "src/engine/ui/calibration/quiver_model.ts",
            "lines": "33-76"
          },
          {
            "path": "src/engine/ui/calibration/quiver_model.ts",
            "lines": "90-112"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/corrected_view.ts": {
      "why": {
        "text": "Pure (React/DOM-free) render-plane availability probe for the corrected-view toggle, shared by the wizard pill and the session's warp method. Only a fitted SIP polynomial (both a[][] and b[][] present) can drive the preview warp; absent one it reports NOT AVAILABLE — an honest disabled state, never a fabricated correction. It carries the convention trap: engine-internal SIP is self-consistent and must NOT get the FITS-boundary negation applied here. Without it the toggle can't honestly report absence and could double-negate the distortion.",
        "cites": [
          {
            "path": "src/engine/ui/corrected_view.ts",
            "lines": "10-26"
          },
          {
            "path": "src/engine/ui/corrected_view.ts",
            "lines": "42-64"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/dashboard/replay/replay_state.ts": {
      "why": {
        "text": "Pure scrub math for the replay dashboard: given a run's CaptureEnvelope[] and a scrub time t it derives per-stage phase (pending/active/complete), elapsed ms, verdicts-so-far and the time-filtered record slice — no React, no Date.now. Honest-or-absent: a stage not yet reached is pending with a null verdict; durations and counts are never interpolated. Without it the time-slider and replay-aware widgets have no state-at-t to render.",
        "cites": [
          {
            "path": "src/engine/ui/dashboard/replay/replay_state.ts",
            "lines": "16-25"
          },
          {
            "path": "src/engine/ui/dashboard/replay/replay_state.ts",
            "lines": "142-197"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/diag_prefs.ts": {
      "why": {
        "text": "Perf-gating for expensive diagnostic visuals (step-6 calibration charts, residual quiver, PSF strips): the manual wizard flow computes them lazily at render, AUTO runs skip them unless the persisted full-diagnostics opt-in is set. Without it AUTO runs pay for chart/quiver/PSF geometry the owner performance directive says to skip.",
        "cites": [
          {
            "path": "src/engine/ui/diag_prefs.ts",
            "lines": "1-31"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/format/sexagesimal.ts": {
      "why": {
        "text": "Display-only sexagesimal coordinate formatting: RA is HOURS internally everywhere in the engine, Dec is degrees, and this converts to compact H/M/S and D/M/S strings for UI copy only, returning 'NOT MEASURED' on non-finite input. It explicitly never feeds a measurement, receipt, solver or WCS path. Without it either coordinates render as raw decimals or a formatter leaks the RA-hours unit trap into a value path.",
        "cites": [
          {
            "path": "src/engine/ui/format/sexagesimal.ts",
            "lines": "5-33"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/persist_migration.ts": {
      "why": {
        "text": "One-time, copy-on-read migration of legacy 'astrologic.*' persisted keys to the 'skycruncher.*' namespace (LAW 6 rename). It copies each legacy key whose new counterpart is absent, never deletes the old key (downgrade-safe, idempotent), and runs at boot before any component reads a preference; every access is try/caught for headless safety. Without it the renamed build silently loses every persisted user preference.",
        "cites": [
          {
            "path": "src/engine/ui/persist_migration.ts",
            "lines": "1-46"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/render_prefs.ts": {
      "why": {
        "text": "Persisted render-layer opt-in flags (Oklab stretch, corrected view) — PIXEL/RENDER ledger, DEFAULT OFF, and byte-identical to the pre-flag output when off. Every accessor is try/caught for headless safety. Without it the render toggles lose persistence and the default-off / byte-identical-off guarantee has no single anchor.",
        "cites": [
          {
            "path": "src/engine/ui/render_prefs.ts",
            "lines": "1-43"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/utils/save_export.ts": {
      "why": {
        "text": "The unified export dispatcher + availability matrix — the one place that knows how each format (receipt/FITS/ASDF/Arrow) is written and returns an honest disabled row + human reason for any format the current run cannot produce (no run, no fitted WCS, no science frame; PNG/C2PA declared-coming, never faked). It also picks the runtime sink (Tauri native save-dialog vs browser Blob download). Without it the step-7 selector and desktop AnalysisPanel lose exports and the fitted-WCS-only export law loses its UI-side gate.",
        "cites": [
          {
            "path": "src/engine/ui/utils/save_export.ts",
            "lines": "10-20"
          },
          {
            "path": "src/engine/ui/utils/save_export.ts",
            "lines": "58-100"
          },
          {
            "path": "src/engine/ui/utils/save_export.ts",
            "lines": "150-212"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/widgets/registry.ts": {
      "why": {
        "text": "The widget registry: data collection is DECOUPLED from display — a widget's dataSelector is a pure read over the already-collected receipt/event bus (never collects, mutates, or fabricates) and weightTier gates render cost ONLY. Imported by the dock, shelf and tests. Without it no widget can be discovered or selected for render, and the 'a widget never emits a placeholder number' law loses its enforcement point.",
        "cites": [
          {
            "path": "src/engine/ui/widgets/registry.ts",
            "lines": "6-21"
          },
          {
            "path": "src/engine/ui/widgets/registry.ts",
            "lines": "103-137"
          },
          {
            "path": "src/engine/ui/widgets/registry.ts",
            "lines": "331-337"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/widgets/widget_persist.ts": {
      "why": {
        "text": "The one versioned localStorage surface for every widget-dock and replay-dashboard preference: a {v,data} envelope so a schema change can migrate or cleanly INVALIDATE a stale blob (return null, caller falls back to default) instead of crashing on it. Every access is try/caught, so it is headless/Node-safe. Without it a persistence-format change would throw on old blobs and each surface would re-invent its own storage guard.",
        "cites": [
          {
            "path": "src/engine/ui/widgets/widget_persist.ts",
            "lines": "8-21"
          },
          {
            "path": "src/engine/ui/widgets/widget_persist.ts",
            "lines": "40-68"
          }
        ]
      },
      "walked_by": []
    },
    "src/engine/ui/wizard_steps.ts": {
      "why": {
        "text": "Single source of truth for wizard step copy (STEP_META) and the visual-step-to-session-method offset, read by PipelineWizard and every step component via getStepMeta. Without it the step headers, progress pips and titles lose their labels and the visual-step vs session-method mapping is undocumented.",
        "cites": [
          {
            "path": "src/engine/ui/wizard_steps.ts",
            "lines": "1-56"
          }
        ]
      },
      "walked_by": []
    },
    "stage:bc_measure": {
      "why": {
        "text": "Measures Brown-Conrady lens distortion (radial k1/k2 only) in a single, side-effect-free pass over the solved match set (measureBrownConradyFromSolution), recording lens_distortion_measured; the solve stays byte-identical.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1746"
          },
          {
            "path": "docs/02-specs/ITERATIVE_BC_SPEC.md",
            "lines": "1-3"
          }
        ]
      },
      "copy": {
        "text": "After the plate solve, this step fits a Brown-Conrady lens-distortion model to the frame's own solver-verified star matches, comparing each matched star's detected position against where the linear plate solution predicts it. It always fits the primary radial term (k1), but admits the higher-order radial terms (k2, k3) only when the matched stars actually reach the frame corners, and the tangential decentering terms only when the stars fill enough of the azimuth around the center — otherwise those terms are refused rather than fit from a coverage hole. The fit is a pure measurement: it reads the world coordinate system and matched stars but changes none of them, so the astrometric result is identical whether or not it runs. It reports honestly instead of guessing — no result when there is no usable coordinate solution, and a \"not measured\" record when fewer than ten matched pairs are available or the fit is degenerate; where the azimuthal coverage is too thin to separate lens distortion from a decentered optic, it still reports k1 but flags the magnitude as upper-bound-honest and the sign as tentative. It also records the residual scatter before and after correction and, when the higher-order terms are available, whether the radial profile changes sign across the field.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1731-1752"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion_refit.ts",
            "lines": "302-324"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion_refit.ts",
            "lines": "390-419"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "receipt_write": "lens_distortion_measured",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "src/engine/pipeline/orchestrator_session.ts#bc_measure withStage (single-pass measureBrownConradyFromSolution, radial k1/k2); ITERATIVE_BC_SPEC.md = ROADMAP-DRAFT (the iterative/atmosphere-corrected design, unbuilt)"
      }
    },
    "stage:bc_rematch": {
      "why": {
        "text": "Re-matches stars against the catalog after Brown-Conrady distortion correction is applied, refining the astrometric solution with improved coordinate accuracy.",
        "cites": [
          {
            "path": "docs/02-specs/RENDER_LOOP_CLOSURE_PLAN.md",
            "lines": "5-10"
          }
        ]
      },
      "copy": {
        "text": "This step recovers stars near the frame edges that the initial plate solve missed because its matching had not yet accounted for lens distortion. It applies the measured distortion model to the catalog star positions, re-matches the full detection list against them, re-runs the astrometric (SIP) refinement on the enlarged set, and keeps a recovered star only if its residual after that refit lands inside the same acceptance envelope the original matches already cleared; when a native science image is present it additionally requires forced photometry to confirm real flux at the predicted position, and records that flux check as not-measured when no such image is available. The denser match set replaces the original solution only when it clears two guards together: a never-worse structural check (strictly more matched stars and residual error no worse than before) and a wrong-sign control that confirms the recovery beats a rerun with the distortion sign reversed. If either guard fails, the original solution is kept unchanged and left byte-identical — a well-corrected narrow field recovers nothing and keeps — so this step can never degrade a good solve. The match counts before and after, the residuals, the recovered stars, and the keep-or-replace decision are recorded.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1786-1797"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion_rematch_pass.ts",
            "lines": "282-337"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion_rematch.ts",
            "lines": "369-397"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/02-specs/RENDER_LOOP_CLOSURE_PLAN.md#Increment I1 — Unify the warp (SIP∪TPS∪BC → one displacement, one resample)"
      }
    },
    "stage:calibrate": {
      "why": null,
      "copy": {
        "text": "After the plate is solved, this step refines the astrometry and profiles the instrument without altering the solved coordinates. It measures the leftover position error between detected stars and catalog stars projected through the solution — a residual scatter in arcseconds — and when that scatter exceeds about 1.2 arcseconds over more than twenty matched stars it fits two optical-distortion models to the residuals, a polynomial (SIP) and a thin-plate spline. Each model is admitted onto the solution only if out-of-sample cross-validation shows it predicts held-out star positions better than applying no correction; a model that fits the sparse, center-weighted star set well in-sample but extrapolates badly toward the frame edges is refused and left absent, with the gate's verdict recorded either way. It also derives a hardware profile — focal length from the pixel scale, approximate radial-distortion and vignetting terms, and per-channel color bias — from the matched stars, and attaches every distortion product alongside the world coordinate system rather than folding it in, so the reported solve numbers stay unchanged.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "60-127"
          },
          {
            "path": "src/engine/pipeline/m7_astrometry/sip_gate.ts",
            "lines": "353-358"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/hardware_profiler.ts",
            "lines": "155-235"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ]
      }
    },
    "stage:calibrate.hardware_profile": {
      "why": {
        "text": "Generates the forensic hardware-profile report (HardwareProfiler.generateReport) from the solved plate + metadata + signal statistics + per-star spectral measurements (M2 spectral forensics). Produces a report — not a persistent per-rig store deposit (that is the separate workbench_deposit node).",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "172-183"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1831-1834"
          }
        ]
      },
      "copy": {
        "text": "This sub-stage runs after the plate solution and star matching are final and produces a forensic instrument profile. From the solved pixel scale it infers a focal length, and from the matched stars — each carrying per-channel brightness samples and a catalog color — it fits approximate radial-distortion and vignetting terms, a per-channel color bias, and flags for likely filters or sensor modifications. The distortion and vignetting fit runs only when at least ten clean matched stars survive after planetary markers and gross-residual outliers are excluded; below that it falls back to a coarse single-coefficient estimate, and every distortion value is labeled approximate rather than a calibrated lens model. The profile is written to the receipt's hardware block — null when there is no solution to profile — and carries a label tied to the final match generation, so a profile based on an outdated match set would be flagged rather than silently trusted.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/hardware_profiler.ts",
            "lines": "155-235"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/hardware_profiler.ts",
            "lines": "277-293"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1490-1493"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "src/engine/pipeline/stages/calibrate.ts#generateHardwareProfile (forensic report; distinct from the workbench_deposit per-rig store)"
      }
    },
    "stage:calibrate.m7_analyze": {
      "why": {
        "text": "Module 7 analyzes and refines astrometric calibration through polynomial fitting and matrix solving to achieve sub-pixel precision in coordinate solutions.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "437-442"
          }
        ]
      },
      "copy": {
        "text": "This sub-stage measures how well the solved linear world coordinate system reproduces the observed star positions. It projects each matched catalog star back to a predicted pixel location, combines the detected-minus-predicted offsets into a residual scatter (RMS in arcseconds) plus a mean shift vector, and — when that scatter exceeds about 1.2 arcseconds over more than twenty matched stars — fits a candidate order-3 distortion polynomial by least squares. The residual scatter and a distortion-detected flag are written to the solution's astrometry record, but the candidate polynomial is kept downstream only if a separate cross-validation gate accepts it. With fewer than fifteen usable matches — planetary-verification markers and gross outliers are excluded — it reports zero residual and no distortion rather than guessing, and a rank-deficient star layout makes the polynomial fit abstain instead of emitting non-finite coefficients.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "60-105"
          },
          {
            "path": "src/engine/pipeline/m7_astrometry/residual_analyzer.ts",
            "lines": "52-118"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#Module 7: Astrometric Refinement (Precision Calibration)"
      }
    },
    "stage:calibrate.sip_gate": {
      "why": {
        "text": "Fits Simple Imaging Polynomial distortion correction to encode the local coordinate transformation, ensuring plate solution accuracy across the image field.",
        "cites": [
          {
            "path": "docs/reference/CARD_ASTROMETRY_WCS.md",
            "lines": "20-25"
          }
        ]
      },
      "copy": {
        "text": "This sub-stage decides whether the fitted distortion polynomial (SIP) is worth keeping. It takes the candidate polynomial fit to the matched-star position residuals and runs deterministic k-fold out-of-sample cross-validation, comparing the polynomial's held-out accuracy against two baselines: applying no correction at all (the linear residual over the matched stars) and a plain affine tilt plane scored on the same held-out stars. The polynomial is admitted onto the solution only if, out of sample, it beats the no-correction residual and does no worse than the affine plane; otherwise it is refused with a specific reason — too few points to test, a singular fit, worse than no correction, or an overfit the affine plane already matches — because a low-order polynomial from a sparse, center-weighted star set extrapolates badly toward the frame corners. Either way it records a verdict carrying the cross-validation residuals and the corner-versus-hull extrapolation reach, attaches the polynomial only when admitted, and never touches the world coordinate system, matched stars, or confidence.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "88-94"
          },
          {
            "path": "src/engine/pipeline/m7_astrometry/sip_gate.ts",
            "lines": "298-359"
          },
          {
            "path": "src/engine/pipeline/m7_astrometry/sip_gate.ts",
            "lines": "72-77"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/reference/CARD_ASTROMETRY_WCS.md#3. SIP (Simple Imaging Polynomial) distortion fit"
      }
    },
    "stage:calibrate.tps_gate": {
      "why": {
        "text": "Produces Thin-Plate Spline (TPS) distortion surface as an alternative high-fidelity astrometric calibration for export and remapping.",
        "cites": [
          {
            "path": "docs/01-canonical/processing_flow.md",
            "lines": "138-152"
          }
        ]
      },
      "copy": {
        "text": "After the plate solution is fixed, this sub-stage fits a thin-plate spline to the leftover position errors between matched catalog stars and the linear solution, modeling residual optical distortion; it runs only when distortion was flagged (residual scatter above about 1.2 arcseconds) and more than twenty stars are matched. Generalized cross-validation selects the spline's smoothing strength over a grid, and the spline is admitted only if it clears every arm of a four-part gate: its out-of-sample error must stay within a set multiple of its in-sample error, must beat applying no correction, and its peak displacement must stay under a physical refraction budget scaled to the field span — with too-few or lopsided control points, or a singular system, refused up front. The fitted model (or an explicit null) and the full verdict — the measured out-of-sample and in-sample residuals, the smoothing value chosen, and a machine-readable reason — are written to the solution's astrometry record. It only observes and annotates and never alters the world coordinate system, matched stars, or confidence, so the reported solve values are identical whether the spline is admitted or refused.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "111-121"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
            "lines": "410-439"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
            "lines": "103-109"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/01-canonical/processing_flow.md#Export surfaces (step-6-adjacent, not all in the browser download)"
      }
    },
    "stage:extract": {
      "why": null,
      "copy": {
        "text": "Turns the loaded raw or FITS frame into a list of detected stars. It decodes and demosaics the frame into linear RGB, reduces that to a luminance science buffer, runs multi-pass star detection with morphological filtering and terrain culling, and samples each detected star's color before releasing the raw buffer. It produces the signal packet — the clean detections plus flagged anomalies — that later stages solve against, and records the decode calibration (white balance, black and white levels, sensor color pattern) and the status of any supplied master dark or flat frame. It also vets the capture timestamp and observer location: a timestamp that predates the camera format, sits in the future, or matches a dead-battery reset signature is treated as an unset clock, and a missing or out-of-range location as absent — in either case only the planet and ephemeris overlays are disabled and the run says so, since the plate solve itself does not depend on the clock or the observing site.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "880-911"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "947-990"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1126-1148"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU; Rust→WASM decode + blob extraction; WGSL/GPU demosaic, CPU fallback",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0"
      }
    },
    "stage:extract.decode": {
      "why": {
        "text": "Decodes raw image formats (CR2, RAW) using the rawler decoder (default) or libraw (cold path), extracting pixel data and metadata for downstream processing.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "1849-1852"
          }
        ]
      },
      "copy": {
        "text": "Decodes the loaded raw or FITS file into pixels at the start of the extraction phase. It first checks a content-fingerprint cache and reuses a prior decode of the same frame; on a miss it decodes the sensor bitstream — camera raw through the default raw decoder (or a byte-identical fallback decoder for formats the default cannot handle), FITS directly, and an already-rendered JPEG or TIFF through the browser image decoder — and demosaics a Bayer color-filter mosaic into a full-resolution linear RGB frame. It emits both the untouched sensor buffer and the demosaiced RGB with the image dimensions, and records which decoder actually ran along with the sensor's black and white levels and filter pattern. If the file cannot be decoded it stops with an error rather than continuing on empty data; if a frame is already demosaiced it passes through without a second demosaic; and if the Bayer phase is unknown it refuses to guess a color demosaic — which would imprint a checkerboard artifact — and instead block-averages the mosaic into a phase-agnostic grayscale that keeps the geometry solvable while marking color as not measured.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "880-911"
          },
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "207-250"
          },
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "289-317"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU orchestration; Rust→WASM decode; WGSL/GPU (WebGPU) demosaic, CPU fallback",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#O.1 Module 1 — RAW decoder cutover (rawler default, LibRaw cold path)"
      }
    },
    "stage:extract.detect": {
      "why": {
        "text": "Performs morphological signal detection to extract star candidates from the luminance plane using thresholding and connectivity analysis.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "334-339"
          }
        ]
      },
      "copy": {
        "text": "Detects stars in the extracted frame. When an un-demosaiced 16-bit Bayer sensor buffer is available it detects on that native data with 2×2 binning; otherwise it detects on the single-channel luminance buffer, and the same condition drives the status text so the reported path cannot drift from the one actually taken. Source extraction runs with multi-pass background masking, morphological filtering to reject non-stellar blobs, and foreground/horizon culling to drop terrain, yielding clean detections with sub-pixel centroids plus, on raw frames, a color-filter-array verdict. These are recorded to the receipt's signal block; if signal analysis returns no result the step raises an error and halts rather than continuing downstream.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "143-177"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1130-1158"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1029-1038"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM (blob extraction)",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#Module 4: Signal Detection (Morphological Extraction)"
      }
    },
    "stage:extract.luminance": {
      "why": null,
      "copy": {
        "text": "Reduces the decoded three-channel RGB frame to a single-channel luminance image — one value per pixel — producing the science buffer that later stages read for star detection, point-spread measurement, and forced-photometry confirmation. By default it combines the channels with the standard Rec. 709 luminance weights; when the frame is a single-color-per-site Bayer mosaic rather than true RGB, a guard switches to equal channel weights so the reduction does not imprint a two-pixel checkerboard on detection, and that guard is off by default and engages only on the flagged mosaic case. It scans the result for non-finite pixels and reports their count rather than substituting made-up values. It writes no receipt block — it hands the buffer downstream and records only its own timing.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "2507-2531"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "995-998"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0"
      }
    },
    "stage:extract.spectral_peek": {
      "why": {
        "text": "Analyzes spectral / color manifolds to provide early information about color content and spectral properties for downstream color calibration.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "178-179"
          }
        ]
      },
      "copy": {
        "text": "Just before the full-resolution decoded image is released from memory, this sub-step records the color of each detected star. For every star in the detection list it reads the interleaved red, green, and blue values at that star's center pixel and stores them on the star, along with a rough blue-over-green color index. A star whose center falls outside the image bounds is left without color values rather than given fabricated ones, and the color index is computed only where the green channel is above zero. It writes no receipt block of its own; the per-star colors it attaches are carried forward and read later by the hardware-profile report's spectral analysis.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1652-1672"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1167-1177"
          },
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "144-168"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#§1.3 Color Spaces (Spectral Manifolds)"
      }
    },
    "stage:forced_confirm": {
      "why": {
        "text": "Performs forced photometry at catalog positions with deep verification and family-wise statistical escalation to confirm detections and measure deep-confirmed photometry.",
        "cites": [
          {
            "path": "docs/WHITEPAPER.md",
            "lines": "226-233"
          }
        ]
      },
      "copy": {
        "text": "This step independently checks whether the plate solution is real by testing for the catalog stars it predicts rather than trusting the fit. It projects reference-catalog positions through the fitted world-coordinate solution — re-projecting through the active lens-distortion model when one is present so the probes land on native pixels — measures the brightness at each predicted position on the untouched full-resolution luminance frame, and applies a single set-level significance test that compares the real set's confirmation rate against repeated scrambled on-frame draws made through the identical per-star test; the set passes only when its excess clears the calibrated threshold and more stars confirm than the null would yield. The outcome — how many positions were examined and confirmed, the set-level significance, a pass-or-fail flag, and the grid label — is written to a separate confirmation record and later summarized as confirmed, refused, insufficient-targets, or not-run, while the coordinate solution, its scale, matched stars, and confidence are never altered. When there is no valid world-coordinate solution, no coherent science frame, or no in-frame catalog stars, it records an honest not-measured marker instead of a number, and the per-color check is likewise reported as not measured because only combined luminance is available at this stage.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "2928-2966"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "2987-3011"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "2069-2084"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/WHITEPAPER.md#5. Beyond quads: other identification machinery > 5.3 Deep-verification and forced confirmation"
      }
    },
    "stage:load": {
      "why": null,
      "copy": {
        "text": "Reads the frame's metadata before any pixels are decoded: it identifies the container format from the file's magic bytes, then pulls instrument and capture fields — camera and lens model, focal length, aperture, ISO, exposure time, capture time, and observer coordinates — from either the FITS header or the EXIF/TIFF tags of a camera raw file, and derives a Julian Date from the capture-time claim. It is deliberate about honesty: when no capture time is present it leaves the timestamp empty rather than substituting the current clock (which would silently misplace later ephemeris work), a zero-vector or camera-default position is recorded as absent rather than as a real fix, and an EXIF time whose primary field is unparseable is either rescued from a secondary date field at a lower hint tier or dropped entirely. An already-rendered 8-bit input such as a phone JPEG or TIFF is flagged as approximate photometry, and the step refuses to run at all if the image buffer was released beforehand. It writes no receipt block of its own — the fields it reads and any degradation notices flow into the receipt's provenance and warnings at packaging time.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "633-697"
          },
          {
            "path": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
            "lines": "225-356"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0"
      }
    },
    "stage:m7_refine": {
      "why": {
        "text": "Performs precision astrometric calibration refinement using normal equations and matrix solving to achieve final plate-solution accuracy.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "437-442"
          }
        ]
      },
      "copy": {
        "text": "After the plate is solved, this stage measures how well the fitted world coordinate system matches the star catalog, computing the residual scatter (RMS, in arcseconds) between each matched star's catalog position and its measured position on the frame. When distortion is flagged and more than twenty stars are matched, it also fits two optical-distortion models — a polynomial (SIP) and a thin-plate spline — but emits each one only if out-of-sample cross-validation shows it generalizes better than applying no correction, with the polynomial additionally required to beat a plain affine tilt plane; otherwise the model is left absent with a recorded reason. The results — residual scatter, a distortion-detected flag, and any admitted model — are written to the solution's astrometry record. This is a read-only measurement layered on top of the solve: it never alters the world coordinate system, the matched stars, or the confidence score, and any internal failure leaves the record absent rather than halting the pipeline.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "60-121"
          },
          {
            "path": "src/engine/pipeline/m7_astrometry/sip_gate.ts",
            "lines": "353-356"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
            "lines": "433-436"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "receipt_write": "astrometry",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#Module 7: Astrometric Refinement (Precision Calibration)"
      }
    },
    "stage:metrology": {
      "why": null,
      "copy": {
        "text": "Resolves the image's angular pixel scale through a trust ladder: FITS header optics are tried first, then the EXIF focal length combined with a sensor-database pixel pitch, and finally a blind triangulation from the brightest detected stars if neither is available. It records which rung produced the lock and emits any focal length it had to assume as a separately labelled finding — never folded into the scale as if it were measured — so the receipt shows the chain of trust; the header rung consults no focal length and so carries no assumption. It also builds the ephemeris guest list of planets, moons, and satellites, but only when both the capture timestamp and an observer location are trusted, since a default clock or a missing site would compute a guest list for the wrong sky; otherwise the list is left empty.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/metrology.ts",
            "lines": "89-127"
          },
          {
            "path": "src/engine/pipeline/stages/metrology.ts",
            "lines": "135-146"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0"
      }
    },
    "stage:package": {
      "why": null,
      "copy": {
        "text": "Assembles every computed pipeline product into a single, schema-versioned document, emitting the version from the one shared schema-version constant rather than a hand-written number. It prefers the real fitted world coordinate solution — the matrix that actually verified the matched stars — and only falls back to a re-synthesized approximation, explicitly labelled as such, when no fitted matrix exists. The solution summary filters sentinel values out of the residuals (dropping planet-verification markers and non-finite entries) and reports the real mean star width from the matched stars, falling back to the forensic estimate only when no width samples survive and to null when even that is absent. It reads both the coordinate and pixel ledgers but mutates neither; the heavy pixel arrays are dropped later, at serialization time, so the assembled object still carries live references.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1166-1187"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "146-194"
          },
          {
            "path": "src/engine/pipeline/stages/receipt_serializer.ts",
            "lines": "20-44"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU"
      }
    },
    "stage:psf_attribution": {
      "why": {
        "text": "Reads the measured PSF field (copies, never mutates) and decomposes the measured stellar anisotropy into physically-calculable systematics {drift / diffraction / seeing / differential-refraction / coma} plus a residual — an APPROXIMATE, report-only attribution. It performs attribution, not deconvolution.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "312-334"
          }
        ]
      },
      "copy": {
        "text": "After the point-spread function has been measured, this step explains the measured star shapes by predicting the physical effects that broaden or elongate them and comparing each prediction against the measurement, which stays the sole arbiter; it changes nothing it reads and is purely additive. It computes the expected star trail from an untracked mount using exposure time, declination, and pixel scale, then tests it: a predicted trail below about half a pixel is marked negligible, and otherwise it is confirmed only when the measured elongation agrees in direction to within about twenty degrees and in size to within about a third — a confirmed trail is then trusted as exact for its share and the rest reported as an unexplained core. It also reports the diffraction limit per color channel, an approximate seeing estimate scaled by air mass from an assumed zenith value, and — only when both the observation time and observer location are trusted — an approximate atmospheric-refraction stretch and chromatic-dispersion elongation, which are predictors only and are never fed back into the solve; when the clock or location is untrusted these are recorded as not measured with the reason stated. From the trail test it infers whether the mount tracked, did not track, or is indeterminate, and every quantity is tagged by how it was obtained (calculated, confirmed, fitted, approximate, or inferred) so a fabricated number is never presented as a measurement.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "358-400"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "466-505"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "625-643"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "src/engine/pipeline/stages/psf_attribution.ts#runPsfAttribution (systematics attribution, report-only; deconvolution is psf_characterize/rl_deconv)"
      }
    },
    "stage:psf_field": {
      "why": {
        "text": "Characterizes the point-spread function field across the image by fitting 2D Gaussians per-star via Levenberg-Marquardt, producing FWHM and ellipticity maps.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "1678-1686"
          }
        ]
      },
      "copy": {
        "text": "After the plate solve, this stage measures how star images are shaped across the frame (the point-spread function). Using the solved, catalog-matched star positions, it fits a two-dimensional Gaussian to each star on the image's own luminance pixel grid — native or 2×2-binned, never resampled before measurement — producing per-star full-width-at-half-maximum, ellipticity, and orientation, summarized as a three-by-three map across the field. It reads only the luminance buffer and never alters the world-coordinate solution, the matched-star list, or the solve confidence. Results are written to the psf_field receipt block; if there is no solution, no image buffer, or the buffer dimensions are inconsistent, the stage records psf_field as null instead of a fabricated map.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1990-1996"
          },
          {
            "path": "src/engine/pipeline/stages/psf_characterize.ts",
            "lines": "43-77"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "5-10"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#L.4 Module 10 — PSF-characterization post-solve stage + Richardson-Lucy deconv"
      }
    },
    "stage:receipt_serializer": {
      "why": null,
      "copy": {
        "text": "Converts the assembled receipt object into JSON text through a replacer that drops a fixed set of heavy typed-array fields — the science image buffer, segmentation masks, the horizon vector, and grid arrays — which JSON encoding would otherwise expand into hundreds of megabytes of index-keyed text. The serialization is pure, with no browser download or document machinery, so a command-line consumer produces receipts byte-identical to the browser's. A companion helper names the output file from the solution's spatial hash, falling back to a timestamp when no solve produced one.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/receipt_serializer.ts",
            "lines": "20-35"
          },
          {
            "path": "src/engine/pipeline/stages/receipt_serializer.ts",
            "lines": "42-49"
          },
          {
            "path": "src/engine/pipeline/stages/receipt_serializer.ts",
            "lines": "6-12"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU"
      }
    },
    "stage:render_apply_sip": {
      "why": {
        "text": "Applies the fitted SIP inverse to re-warp the DISPLAY preview only (ImageProcessor.applySipUndistort rewrites previewUrl) — RENDER-plane, DEFAULT OFF (RENDER_APPLY_SIP), byte-identical when off; never touches WCS/matched_stars and does not feed export.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1848-1878"
          }
        ]
      },
      "copy": {
        "text": "This display-only step redraws the on-screen preview with the fitted lens distortion removed: it reads the preview image, the fitted distortion polynomial (SIP), and the reference pixel, then applies a single inverse warp so the distortion the solve measured is taken out of the pixels the viewer sees. It rewrites only the preview image and never touches the world coordinate system, the matched stars, or the solve numbers. It is turned off by default and reports a skip when the feature flag is off, when the solve carried no distortion polynomial, or when preview rendering is disabled; it reports applied only when it actually re-warps the preview, and the warp is a provable no-op that returns the image untouched when no distortion is present. It writes no receipt entry, and on any rendering error it leaves the preview unchanged.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1856-1885"
          },
          {
            "path": "src/engine/pipeline/constants/pipeline_config.ts",
            "lines": "595-602"
          },
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "434-443"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "src/engine/pipeline/orchestrator_session.ts#render_apply_sip withStage (RENDER-plane, default-off, previewUrl only — LAW 1; export SIP is the separate fits_writer/asdf_writer path)"
      }
    },
    "stage:schema_versions": {
      "why": {
        "text": "Manages and declares receipt schema versions across the pipeline, tracking evolution and backward-compatibility guarantees for data serialization.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "1799-1806"
          }
        ]
      },
      "copy": {
        "text": "Holds, in one place, the schema version numbers stamped onto the pipeline's serialized products. It defines two deliberately separate constants — the receipt schema version, which is the versioned contract for the exported solve receipt returned to callers, and an independent science-packet version that tracks a separate compact export format — each bumped only on a genuine schema change so a version reliably distinguishes products of different shapes. A version number is never advanced without a real structural change, which would be dishonest versioning. A third version, used by an in-memory result wrapper, deliberately lives elsewhere and is not centralized here.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/schema_versions.ts",
            "lines": "6-25"
          },
          {
            "path": "src/engine/pipeline/stages/schema_versions.ts",
            "lines": "442-445"
          },
          {
            "path": "src/engine/types/manifest.ts",
            "lines": "261-266"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#N.3 Receipt schema — 2.5.0 → 2.6.0 → 2.7.0 lineage"
      }
    },
    "stage:solve": {
      "why": {
        "text": "Orchestrates plate solving via pattern recognition, combining quad matching, catalog cross-matching, and astrometric refinement to produce a World Coordinate System solution.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "248-253"
          }
        ]
      },
      "copy": {
        "text": "First resolves a search hint through a fixed ladder — an explicit user target, then a telescope pointing read from the FITS header, then a zenith estimate, then a full-sky blind search. The zenith rung is used only when both the observer location and the capture clock are trusted, because a zenith hint from a defaulted location or an unset camera clock points at the wrong part of the sky and would push the true center outside a radius-limited search; a user target or header pointing is marked trusted while zenith and blind are not, and when no rung fires the search falls back to the full sky (180-degree radius). It then hands the detected stars and the chosen hint to the plate solver, which forms four-star geometric groups from the brightest stars, matches their rotation- and scale-invariant shapes against a catalog index, fits the world coordinate system, and verifies the fit by checking that detected stars fall on cataloged positions. It returns the image-center coordinates (right ascension in hours, declination in degrees), rotation, and pixel scale together with the matched stars and a confidence score, and reports failure rather than a guessed pose when no candidate verifies.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve.ts",
            "lines": "80-149"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "25-36"
          },
          {
            "path": "src/engine/types/Main_types.ts",
            "lines": "218-242"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#Module 6: Plate Solving (Pattern Recognition)"
      }
    },
    "stage:solve.quad_wasm": {
      "why": {
        "text": "Executes quad-hash based blind plate solving in WASM, matching detected star quads against catalog patterns to determine initial astrometric solution.",
        "cites": [
          {
            "path": "docs/reference/CARD_ASTROMETRY_WCS.md",
            "lines": "7-12"
          }
        ]
      },
      "copy": {
        "text": "The primary plate-solving branch for normal and narrow fields. It takes the detected star positions and a magnitude-ranked subset of nearby catalog stars — capped at thirty and one hundred respectively by default — and first drops any detection whose coordinates are non-finite, which would otherwise crash the compiled matcher; it then forms four-star geometric groups, hashes each into a rotation- and scale-invariant descriptor, matches those descriptors against the catalog, and for every candidate correspondence fits a world coordinate system and checks that the detected stars land on cataloged positions. If the matcher returns no candidate quads, or if every fitted candidate fails that verification, this branch reports no lock and the solver falls through to its wider-field fallbacks; the first candidate that verifies becomes the recorded solution. Either way it logs its own elapsed time and attempt count to the receipt's per-branch solve-timing block, which is left absent — never a fabricated zero — for any branch that was not attempted.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "1539-1541"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "1880-1992"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1040-1054"
          }
        ]
      },
      "walked_by": [
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU + Rust→WASM · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "receipt_write": "solution",
        "doc_binding": "docs/reference/CARD_ASTROMETRY_WCS.md#1. Quad-hash blind plate solving"
      }
    },
    "stage:solve_context": {
      "why": null,
      "copy": {
        "text": "Builds the plate solver's option contract from a single shared builder so the two pipelines can no longer drift apart: base pixel scale, focal length, sensor pixel pitch, lens-distortion model, a trust-gated observation time, observer location, a blind-search wall-clock budget (90 seconds by default), and any search hints. For a narrow field — frame diagonal under 6 degrees — it relaxes the verification gate to accept a lock on four anchor-star matches at 0.5 confidence, because the wide-field thresholds reject genuine deep-stack solves whose match confidence is honestly lower; wider fields keep the caller's defaults. The observation time must already be trust-gated by the caller, so an untrusted camera clock is passed as absent and planetary anchors are never placed from a default date, and the observer site is omitted when the location was only a default.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve_context.ts",
            "lines": "57-71"
          },
          {
            "path": "src/engine/pipeline/stages/solve_context.ts",
            "lines": "134-152"
          },
          {
            "path": "src/engine/pipeline/stages/solve_context.ts",
            "lines": "36-48"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU"
      }
    },
    "stage:solve_provenance": {
      "why": {
        "text": "Records the lean success provenance solved_via in {blind | assisted:user | assisted:metadata | assisted:tool}, derived from the hint-resolution rung (a hinted solve is not a lesser solve; acceptance never consults the hint). No confidence levels; no wall-clock enters any field.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "1859"
          },
          {
            "path": "src/engine/pipeline/stages/schema_versions.ts",
            "lines": "115-118"
          }
        ]
      },
      "copy": {
        "text": "Records which category of search prior was active when the solve locked — blind, user-assisted, metadata-assisted, or tool-assisted — by mapping the winning hint rung (blind to blind, an explicit user target to user-assisted, a FITS header pointing or a zenith estimate to metadata-assisted). It returns nothing rather than a guessed label when the hint source is not honestly known, never defaulting to blind; the tool-assisted category is defined but never emitted because no autonomous tool feeds the solve today; and a failed-attempts list exists in the shape but is attached only when an earlier attempt genuinely failed before a later one recovered, carrying no wall-clock timing so the block stays deterministic. A hinted solve is not a lesser solve — acceptance never consults the hint — so this is denominator bookkeeping for detection-improvement analysis, not a quality tag.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve_provenance.ts",
            "lines": "73-81"
          },
          {
            "path": "src/engine/pipeline/stages/solve_provenance.ts",
            "lines": "92-108"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1375-1377"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#O.2 Receipt schema 2.11.0 — solve_provenance / solved_via (hint rung, not confidence)"
      }
    },
    "stage:spcc": {
      "why": {
        "text": "Performs SpectroPhotometric Color Calibration (SPCC): regresses instrumental color against catalog Gaia BP-RP to measure the color transform (slope/intercept/r2) plus a zero-point.",
        "cites": [
          {
            "path": "docs/reference/CARD_COLOR_PHOTOCAL.md",
            "lines": "2-11"
          }
        ]
      },
      "copy": {
        "text": "Spectrophotometric color calibration. After the plate solve, measure each matched star's brightness in the red, green, and blue channels at its catalog-matched position on the retained full-resolution linear frame, then fit the instrument's color response — a straight line relating instrumental blue-minus-red color to catalog Gaia BP-RP color — plus a photometric zero point that transfers instrumental magnitude onto the Gaia G scale. The color fit is invariant to sensor gain, but the absolute zero point rides an approximate gain model and is demoted to a hint whenever no exposure time is present; below eight usable stars the color fit is left uncalibrated while a zero point may still be recorded. Stars whose catalog band is not Gaia G are kept out of the fit rather than blended across two photometric systems, and air mass is recorded as absent rather than fabricated when no trusted clock or location is available. The stage refuses honestly with a null-valued block for a monochrome sensor, an unknown or unsupported color-filter layout, or a downscaled solve whose full-resolution coordinates were not restored, and it works only in pixel space, never modifying the coordinate solution.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "315-371"
          },
          {
            "path": "src/engine/pipeline/m8_photometry/spcc_calibrator.ts",
            "lines": "673-712"
          },
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "383-388"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "receipt_write": "spcc",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/reference/CARD_COLOR_PHOTOCAL.md#1. SPCC color regression (SpectroPhotometric Color Calibration, PixInsight lineage)"
      }
    },
    "stage:spcc_render_gains": {
      "why": {
        "text": "Computes channel-gain TLS/EIV estimators from SPCC and applies white-balance gains in the render lane for photometric display correction.",
        "cites": [
          {
            "path": "docs/reference/CARD_COLOR_PHOTOCAL.md",
            "lines": "55-61"
          }
        ]
      },
      "copy": {
        "text": "A display-only step that redraws the on-screen preview using the per-channel white-balance gains the color-calibration step derived from catalog star colors, replacing the earlier estimate taken from the average color of the bright stars in the frame. The gains are anchored to the green channel and clamped to a safe range, and the sky background level is left untouched so the background stays neutral. It applies only if those gains passed their quality check and previews are enabled; otherwise it makes no change and reports a skip, leaving the preview on its existing empirical white balance. Because the plate-solve, point-spread, and forced-photometry measurements all read the original unscaled linear data, no measured value changes, and any rendering error is caught and leaves the preview as it was.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1962-1981"
          },
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "197-205"
          }
        ]
      },
      "walked_by": [
        "cr2",
        "fits"
      ],
      "badges": {
        "substrate": "TypeScript · CPU",
        "observed_in": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
        "receipt_version": "2.27.0",
        "merge_notes": [
          "observed_in=test_results/dag_receipts/cr2_2026-07-17T01-29-43-816Z.receipt.json from cr2 walk"
        ],
        "doc_binding": "docs/reference/CARD_COLOR_PHOTOCAL.md#8. SPCC channel-gains TLS/EIV estimator + render-lane white-balance application"
      }
    },
    "stage:user_annotations": {
      "why": {
        "text": "Captures observer-supplied free-text TESTIMONY in the receipt (schema 2.12.0): description / location / bortle / rig-notes / session-issues (all STRINGS + provenance), NEVER parsed into the solve; null when the observer supplied nothing.",
        "cites": [
          {
            "path": "docs/01-canonical/SkyCruncher_Architecture.md",
            "lines": "1860"
          }
        ]
      },
      "copy": {
        "text": "Captures the observer's free-text testimony — a target description, location text, a sky-quality note, rig notes, and any session issues — as strings only, never parsed into a number, a knob, or any solve input. These annotations are kept structurally separate from the metadata that feeds the physics model, so testimony can never be laundered into evidence. When every field is empty the block is recorded as null rather than an empty skeleton, which keeps the pinned reference solves byte-identical since their receipts carry a null block. Provenance is recorded as either typed directly by the observer or drafted by an assistant and then explicitly confirmed in the export interface before being applied — the assistant tool never writes a session on its own.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/user_annotations.ts",
            "lines": "84-107"
          },
          {
            "path": "src/engine/pipeline/stages/user_annotations.ts",
            "lines": "5-27"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/01-canonical/SkyCruncher_Architecture.md#O.2 Receipt schema 2.12.0 — user_annotations (free-text testimony, never parsed into the solve)"
      }
    },
    "stage:user_target_hint": {
      "why": {
        "text": "Receives and records user-provided target hints (location, optics class) to assist the solver with prior information.",
        "cites": [
          {
            "path": "docs/TEST_SUITE_PLAN.md",
            "lines": "441-462"
          }
        ]
      },
      "copy": {
        "text": "Records the structured target hint a user supplied on upload — a display label plus a sky pointing, right ascension in hours and declination in degrees — but only when that hint actually seeded the winning search. The block is present only for a user-assisted solve and is null on a blind one or when no hint was given; the azimuth-mode sentinel and any non-finite coordinate are screened out with the same guard the hint resolver applies, so a hint that could not have seeded the solve never appears. A hint is treated as a search prior, always flagged assumed: it only narrows the search and never affects whether the solution is accepted, which the verification gate alone decides. Field of view is not part of the supplied hint, so it is reported null rather than fabricated.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/user_target_hint.ts",
            "lines": "48-62"
          },
          {
            "path": "src/engine/pipeline/stages/user_target_hint.ts",
            "lines": "5-23"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/TEST_SUITE_PLAN.md#Stage 9 — Receipt trains 2.14.0: `rawler_calibration` + `user_target_hint`"
      }
    },
    "stage:workbench_deposit": {
      "why": {
        "text": "Deposits per-rig optical calibration profiles into the Optical Workbench persistent store, keyed by user identity and lens, never fatal if absent.",
        "cites": [
          {
            "path": "docs/OPTICAL_WORKBENCH_SCHEMA.md",
            "lines": "1-6"
          }
        ]
      },
      "copy": {
        "text": "After packaging completes, writes a compact side-channel record — keyed by the camera body and lens model — that copies scalar receipt fields (pixel scale, matched-star count, measured lens-distortion and point-spread summaries, photometric zero point) for later per-rig pooling. The whole hook is wrapped so it can never fail a solve or alter the receipt: the extractor only reads the receipt, and any storage failure, synchronous or asynchronous, is swallowed. It is on by default, but a row persists only when a storage backend is available — an injected file-backed store on the command line, or the browser's own persistent storage — and otherwise is a silent no-op, which is why the headless smoke test stays byte-identical. It returns nothing when the receipt has no solution, since there is nothing to pool, and otherwise assigns a version epoch against the rig's prior deposits before appending.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/workbench_deposit.ts",
            "lines": "85-110"
          },
          {
            "path": "src/engine/pipeline/stages/workbench_deposit.ts",
            "lines": "64-78"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/workbench_store.ts",
            "lines": "272-373"
          }
        ]
      },
      "walked_by": [],
      "badges": {
        "substrate": "TypeScript · CPU",
        "doc_binding": "docs/OPTICAL_WORKBENCH_SCHEMA.md#Optical Workbench — Per-Rig Profile Store (REFERENCE, owner-ratified 2026-07-09)"
      }
    },
    "tools/api/a5_summarize.mjs": {
      "why": {
        "text": "Mechanical corpus-pass summarizer: folds the per-frame a5_results.jsonl into a5_summary.json with honest-or-absent accounting (frames enumerated in the manifest but absent from the jsonl are reported under not_run, never counted as solved/failed), computing per-source/per-lane/per-rig crack rates, CR2 blind-vs-hinted provenance, a failure taxonomy, and truth-graded TP/FP counts. Editorial fields are left empty for the orchestrator relay.",
        "cites": [
          {
            "path": "tools/api/a5_summarize.mjs",
            "lines": "1-13"
          },
          {
            "path": "tools/api/a5_summarize.mjs",
            "lines": "97-129"
          }
        ]
      },
      "walked_by": []
    },
    "tools/api/api_harness.config.ts": {
      "why": {
        "text": "Isolated vitest config for the Toolchest API headless harness (the api-smoke gate): scopes include to *.apispec.ts so the sacred `npx vitest run` gate never collects it, CLEARS the base setupFiles after merge so it drives the REAL compiled wasm instead of the suite's pure-JS wasm mock, and sets fileParallelism:false so parallel spec files can't starve the 90s blind-solve wall-clock budget. Without this discipline the api-smoke gate would run against the mock and/or false-no-lock.",
        "cites": [
          {
            "path": "tools/api/api_harness.config.ts",
            "lines": "1-44"
          }
        ]
      },
      "walked_by": []
    },
    "tools/api/headless_driver.ts": {
      "why": {
        "text": "The single headless entry point: runWizardPipeline boots the real compiled wasm, installs an fs-backed atlas loader, and drives the real OrchestratorSession step1_Load->step6_Integrate in Node, returning a receipt value-identical to the browser wizard export. Imported by every apispec/runspec/previewspec and by run.mjs's runspec. Without it there is no headless pipeline execution and the CR2/SeeStar sacred api-gates, the Arrow sink, and the community push sink have nothing to run.",
        "cites": [
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "1-33"
          },
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "399-506"
          }
        ]
      },
      "walked_by": []
    },
    "tools/api/run.config.ts": {
      "why": {
        "text": "Isolated vitest config for run.mjs's *.runspec.ts, mirroring api_harness.config: it clears the base wasm-mock setup after the merge so the CLI drives the real narrow wizard solve, and uses the distinct *.runspec.ts include suffix so this spec is collected by NEITHER the sacred `npx vitest run` gate nor the api-smoke gate nor the CR2/FITS rails — it never pollutes any standing gate's pass/skip count.",
        "cites": [
          {
            "path": "tools/api/run.config.ts",
            "lines": "1-35"
          }
        ]
      },
      "walked_by": []
    },
    "tools/api/run.mjs": {
      "why": {
        "text": "Thin CLI projection wrapper over runWizardPipeline: a plain .mjs cannot resolve the engine @/ alias or boot the compiled wasm, so it forks vitest against run.config.ts (env-passing the FITS path + receipt-out path + optional knob overrides), dumps the FULL canonical receipt to disk, and prints ONLY a --select dot-path projection to stdout with honest-or-absent MISSING for absent paths. Exit 0 on solve, 2 on no-solve, 1 on error. Entry point for agents and the overnight FITS driver. FITS lane only.",
        "cites": [
          {
            "path": "tools/api/run.mjs",
            "lines": "1-33"
          },
          {
            "path": "tools/api/run.mjs",
            "lines": "84-138"
          }
        ]
      },
      "walked_by": []
    },
    "tools/api/solve_to_receipt.runspec.ts": {
      "why": {
        "text": "The vitest-hosted half of run.mjs: runs the real calibrated FITS wizard on the env-supplied buffer, applies optional runtime PIPELINE_CONSTANTS overrides pre-solve (config-as-argument seam), injects the headless JSONL workbench store so the post-package deposit hook persists a per-rig row, and writes canonical receipt bytes via serializeReceipt (byte-identical to the browser download) to API_RUN_RECEIPT_OUT. Without it run.mjs has no engine-capable child to fork.",
        "cites": [
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "1-71"
          }
        ]
      },
      "walked_by": []
    },
    "tools/api/stage_replay.replayspec.ts": {
      "why": {
        "text": "The vitest-hosted seam-replay executor (SEAM_CONTRACT v1): loads a sha256-verified input capsule (loud fail on mismatch), boots the real compiled wasm, runs exactly ONE real engine stage (m7_refine/spcc/psf_field/psf_attribution/bc_measure/psf/integrate) with the exact orchestrator_session wiring, and writes the replayed capsule; all IEEE-exact/byte comparison lives in the .mjs executor. Without it a single real stage cannot be re-run over a frozen capsule for divergence localization.",
        "cites": [
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "1-32"
          },
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "174-421"
          }
        ]
      },
      "walked_by": []
    },
    "tools/atlas/merge_hyg_sectors.mjs": {
      "why": {
        "text": "The HYG gap-fill merge — the atlas chain link that was a one-off never committed. It appends STRICT mag>6.8 HYG rows (ra in HOURS) wholesale after each sector's Gaia block (ra in DEGREES), byte-exact to the shipped minified sector format (one object per line, LF, no dedup). It is additive only (never retires rows) and writes to a caller-supplied out dir, never over live sectors; imported as a library by verify_atlas_repro. Without it the frozen deep-catalog sectors cannot be reproduced.",
        "cites": [
          {
            "path": "tools/atlas/merge_hyg_sectors.mjs",
            "lines": "1-37"
          },
          {
            "path": "tools/atlas/merge_hyg_sectors.mjs",
            "lines": "43-55"
          }
        ]
      },
      "walked_by": []
    },
    "tools/atlas/rebucket_sectors.mjs": {
      "why": {
        "text": "Re-buckets the deep Gaia level-3 sector files so their numeric ids match star_catalog_adapter.getSectorId (raIndex=floor(ra_hours/4), decIndex=floor((dec+90)/30), id=decIndex*6+raIndex), deduping by source_id. Gaia rows carry ra in DEGREES (raH=ra/15). It is the middle link of the generate_star_atlas -> rebucket -> merge_hyg chain, emitting Gaia-only sectors that merge_hyg then HYG-fills. Without it the mixed-generation numeric sectors have inconsistent, non-adapter ids.",
        "cites": [
          {
            "path": "tools/atlas/rebucket_sectors.mjs",
            "lines": "1-3"
          },
          {
            "path": "tools/atlas/rebucket_sectors.mjs",
            "lines": "19-41"
          }
        ]
      },
      "walked_by": []
    },
    "tools/atlas/verify_atlas_repro.mjs": {
      "why": {
        "text": "The standing byte-proof gate that the shipped, gitignored, irreplaceable deep catalog (L1/L2 anchors + 36 level-3 sectors) is byte-for-byte regenerable from committed inputs and tooling. It carries an INDEPENDENT reference re-implementation of the generator+rebucket steps (a stronger claim than re-running the script) and imports the HYG-merge rule from the tool under proof. It documents that the frozen atlas was built with legacy buggy (ra-degrees-as-hours) bucketing, so byte-repro must replicate that. Without it, atlas regenerability is unverifiable.",
        "cites": [
          {
            "path": "tools/atlas/verify_atlas_repro.mjs",
            "lines": "1-47"
          }
        ]
      },
      "walked_by": []
    },
    "tools/corpus/ledger.mjs": {
      "why": {
        "text": "Append-only cumulative record of every corpus-frame solve outcome across runs, preserving the history that run_corpus's overwritten corpus_report.json throws away. Each JSONL line ties a result to the git commit, so a fix flips a frame failed->solved as MEASURED improvement; it also backs the 2-solve churn purge and the pooled-failure labeled dataset. run_corpus imports and calls ingestReport at the end of every sweep; without it, improvement across commits could only be asserted.",
        "cites": [
          {
            "path": "tools/corpus/ledger.mjs",
            "lines": "1-21"
          },
          {
            "path": "tools/corpus/ledger.mjs",
            "lines": "37-60"
          }
        ]
      },
      "walked_by": []
    },
    "tools/corpus/run_corpus.mjs": {
      "why": {
        "text": "Headless triage sweep over every FITS (and CR2) under the corpus dir plus the M66 control: real WASM extraction + matcher + verification against the real atlas, emitting a per-file verdict table. It runs its OWN solve loop (decode -> extractStars -> solve_planar_local -> fitWCS -> scale-gate -> verify -> consensus), a known duplication of the wizard/engine path kept as fast triage tooling. Without it there is no headless breadth check that new frames still solve and no feed into the cumulative corpus ledger.",
        "cites": [
          {
            "path": "tools/corpus/run_corpus.mjs",
            "lines": "1-19"
          },
          {
            "path": "tools/corpus/run_corpus.mjs",
            "lines": "647-800"
          }
        ]
      },
      "walked_by": []
    },
    "tools/e2e/lib.mjs": {
      "why": {
        "text": "The shared Playwright harness (createRun) for every wizard e2e scenario: it spawns-or-attaches a vite dev server (refusing to spawn or kill the owner's reserved port 3005), launches system Chrome, blocks all non-same-origin requests for a hermetic run, and exposes the step/assert/sessionSnapshot/finish helpers plus the artifact + per-stage-timing sidecar. Imported by run_wizard_seestar, run_wizard_cr2, run_wizard_cr2_export, run_wizard_carina, and capture_ui_wave. Without it no wizard e2e runs and the two pinned reference solves lose their driver.",
        "cites": [
          {
            "path": "tools/e2e/lib.mjs",
            "lines": "42-74"
          },
          {
            "path": "tools/e2e/lib.mjs",
            "lines": "191-318"
          }
        ]
      },
      "walked_by": []
    },
    "tools/e2e/run_wizard_cr2.mjs": {
      "why": {
        "text": "The bundled Canon CR2 DSLR pinned reference on the same harness as SeeStar: steps 1-4 must genuinely work (decode, honest badges, detection, instant EXIF scale lock at the 14mm-override 63.35 arcsec/px), step 5 is the ultra-wide BLIND solve (honest outcome either way for arbitrary files; bundled sample pins scale/matched), and step 6 pins the post-solve bc_rematch outcome (KEPT_ORIGINAL, matched 56->56, rms 630.8214->616.8437, wrong-sign guard holding).",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_cr2.mjs",
            "lines": "1-15"
          },
          {
            "path": "tools/e2e/run_wizard_cr2.mjs",
            "lines": "156-183"
          }
        ]
      },
      "walked_by": []
    },
    "tools/e2e/run_wizard_seestar.mjs": {
      "why": {
        "text": "The SeeStar M66 FITS pinned reference solve driven through all 7 wizard steps plus the Glass-Pipeline inspector and the step-7 receipt export/download. It asserts the sacred blind-solve geometry (RA about 11.336h, scale about 3.68 arcsec/px, matched >= 5, confidence >= 0.6, unit parity); a break here is a revert trigger.",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_seestar.mjs",
            "lines": "1-8"
          },
          {
            "path": "tools/e2e/run_wizard_seestar.mjs",
            "lines": "106-120"
          }
        ]
      },
      "walked_by": []
    },
    "tools/psf/corrections.mjs": {
      "why": {
        "text": "Lens-corrections + solve-socket module: vignette is a PIXEL-lane multiplicative radial gain applied first; distortion is a COORDINATE FUNCTION (Brown-Conrady k1/k2 on star positions, not an image warp). Its `--astrometry <file.json>` seam swaps the APPROXIMATE_PROFILE prior for measured corrections through the same getCorrections() path. Its makeBrownConrady is the fast-iteration incubator twin of the live engine's makeBrownConradyDistortion and must stay in sync when the radial model changes.",
        "cites": [
          {
            "path": "tools/psf/corrections.mjs",
            "lines": "1-25"
          },
          {
            "path": "tools/psf/corrections.mjs",
            "lines": "48-57"
          }
        ]
      },
      "walked_by": []
    },
    "tools/psf/decode_cr2.mjs": {
      "why": {
        "text": "Shared CR2 decode lane: libraw-wasm (browser-Worker bridged onto worker_threads via worker_shim) returns an active-area interleaved RGB16 DOMINANT-CHANNEL mem_image (G sites pure [0,G,0]; R/B carry ~4-7% deterministic cross-leak). It infers true (w,h) from the element count with a stride-coherence tie-break (the 60Da near-tied-dims shear guard), repairs hot pixels at the CFA level, and bilinear-demosaics the dominant channels so data stays linear and camera-native. Imported by measure_and_clean/refit_distortion; without the tie-break a near-miss factorization shears the frame diagonally.",
        "cites": [
          {
            "path": "tools/psf/decode_cr2.mjs",
            "lines": "1-14"
          },
          {
            "path": "tools/psf/decode_cr2.mjs",
            "lines": "56-60"
          }
        ]
      },
      "walked_by": []
    },
    "tools/psf/solution_to_astrometry.mjs": {
      "why": {
        "text": "Bridges the wizard plate-solve receipt (or an e2e summary.json) to the measured `--astrometry` JSON the PSF cleanup lane consumes, closing the solve->cleanup loop. It navigates this codebase's unit land mines explicitly: the engine-internal fitted WCS stores crval[0] in HOURS while the packet FITS-style wcs block stores CRVAL1 in DEGREES, and it emits DEGREES per the socket contract. It fits Brown-Conrady k1/k2 about the FRAME CENTER with an extra linear term to absorb WCS scale degeneracy. Without it, measured corrections cannot reach corrections.mjs.",
        "cites": [
          {
            "path": "tools/psf/solution_to_astrometry.mjs",
            "lines": "1-34"
          },
          {
            "path": "tools/psf/solution_to_astrometry.mjs",
            "lines": "36-60"
          }
        ]
      },
      "walked_by": []
    },
    "tools/stack/fits_io.mjs": {
      "why": {
        "text": "Plane-level FITS reader in ORIGINAL units plus the minimal writer for the stack lane. Reads without normalization because per-frame background subtraction and cross-frame flux normalization must happen in raw values, and treats non-finite AND exact-0.0 float samples as out-of-footprint NaN markers. Its wcsCards writer is one of the two HOURS->DEGREES (x15) + CRPIX+1 conversion boundaries and eats an engine-internal (HOURS) WCS — never a receipt WCS. Without it the stacker cannot read frames or emit output FITS.",
        "cites": [
          {
            "path": "tools/stack/fits_io.mjs",
            "lines": "1-13"
          },
          {
            "path": "tools/stack/fits_io.mjs",
            "lines": "190-224"
          }
        ]
      },
      "walked_by": []
    },
    "tools/stack/solve_lib.mjs": {
      "why": {
        "text": "The stack lane's solve machinery, ported from the proven corpus-runner patterns: hybrid Gaia/HYG atlas loading, spherical/tangent-plane math, hinted quad solve, and N-point WCS refinement against matched stars. It fixes the coordinate-ledger conventions (crval[0] in HOURS internally, cd in deg/px, 0-based pixel centers) that stack.mjs relies on. Without it stack.mjs cannot register frames.",
        "cites": [
          {
            "path": "tools/stack/solve_lib.mjs",
            "lines": "1-11"
          },
          {
            "path": "tools/stack/solve_lib.mjs",
            "lines": "27-52"
          }
        ]
      },
      "walked_by": []
    },
    "tools/stack/stack.mjs": {
      "why": {
        "text": "Entry point of the v1 solve-first stacker/drizzler (headless, tools-lane only): inventory -> prep -> solve -> registration -> stack/drizzle -> validation -> report+PNG+float32 FITS. It enforces the coordinate/pixel ledger separation (registration is pure coordinate math; no pixel is touched until the single resample) and wires together fits_io, solve_lib, and the psf-lane imaging writer. Without it there is no headless N-frame stack product.",
        "cites": [
          {
            "path": "tools/stack/stack.mjs",
            "lines": "1-40"
          },
          {
            "path": "tools/stack/stack.mjs",
            "lines": "46-51"
          }
        ]
      },
      "walked_by": []
    }
  },
  "edges": [
    {
      "from": "boundary:arrow_seam",
      "to": "src/engine/core/ArrowMemory.ts",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:214",
      "verification": "VERIFIED",
      "how": {
        "what": "little-endian Arrow IPC buffers (equatorial RA/Dec per column, COORDINATE ledger)",
        "transform": "in-memory Arrow view over the seam",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:214 'in-memory view: ArrowMemory.ts'; coord:218 COORDINATE ledger. Confirmed."
    },
    {
      "from": "boundary:arrow_seam",
      "to": "src/engine/pipeline/m6_plate_solve/atlas_arrow_codec.ts",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:213",
      "verification": "VERIFIED",
      "how": {
        "what": "Arrow column buffers per spec; RA-unit hybridity inherited from atlas_rows",
        "transform": "atlas_arrow_codec decodes the columnar seam between catalog store and solver",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:213 names atlas_arrow_codec.ts as the codec; RA-unit hybridity inherited (:217). Confirmed."
    },
    {
      "from": "boundary:arrow_seam",
      "to": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts:590-601",
      "verification": "VERIFIED",
      "how": {
        "what": "per-sector Apache Arrow IPC blob /atlas/sectors/level_3_sector_<N>.arrow (little-endian columnar), the value-equivalent binary TWIN of the canonical JSON sector rows",
        "transform": "tryLoadArrowSector fetches the .arrow via atlasLoader and decodeArrowSector (atlas_arrow_codec) -> StandardStar[]; flag-gated by VITE_ATLAS_BINARY (default ON), with a PER-SECTOR fallback to the JSON path when the .arrow is absent or fails to decode",
        "lossy": false
      },
      "why": {
        "text": "The flag-gated binary read arm of the atlas — a third arrow_seam consumer distinct from wave-1's ArrowMemory / atlas_arrow_codec edges. The .arrow sector twins carry the SAME hybrid rows as the JSON sectors (RA-unit hybridity inherited from atlas_rows), so the downstream match/solve is unchanged; the per-sector JSON fallback keeps machines without shipped twins byte-identical.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
            "lines": "185-195"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "154-160"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "boundary:atlas_rows",
      "to": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts:719",
      "verification": "VERIFIED",
      "how": {
        "what": "HYBRID rows: Gaia rows carry ra in DEGREES, legacy HYG rows carry ra in HOURS (stored AS-IS, never normalized on disk)",
        "transform": "isGaiaFormat = (source_id||mag_g defined); ra_hours = ra_deg!==undefined ? ra_deg/15 : (isGaiaFormat ? ra/15 : ra) -> everything normalized to internal HOURS",
        "lossy": false
      },
      "why": {
        "text": "A per-row discrimination, not a per-file one: the same sector file mixes Gaia-degrees and HYG-hours rows. Reading a Gaia row's ra as hours (or an HYG row's ra as degrees) mislocates the star by a 15x factor and poisons the match/solve. isGaiaFormat is the load-bearing discriminator.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
            "lines": "719-730"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "161-165"
          }
        ]
      },
      "review_note": "star_catalog_adapter.ts:719-728 exact: isGaiaFormat=(source_id||mag_g), ra_hours = ra_deg/15 else isGaia?ra/15:ra. Per-row deg/hours discriminator confirmed."
    },
    {
      "from": "boundary:atlas_rows",
      "to": "tools/atlas/verify_atlas_repro.mjs",
      "type": "evidence",
      "cite": "tools/atlas/verify_atlas_repro.mjs:46",
      "verification": "VERIFIED",
      "how": {
        "what": "the 38 shipped atlas files (2 anchors + 36 sectors) regenerated byte-for-byte from gaia_vanguard_dr3.csv + the named HYG cells",
        "transform": "an INDEPENDENT reference reimplementation of generate_star_atlas + rebucket (pinned to ATLAS_LEGACY_BUCKETING) reproduces the frozen bytes; exit 0 iff 38/38 byte-EQUAL; per-file SHA-256 + aggregate md5 fc2c4482 recorded in atlas_repro_manifest.json",
        "lossy": false
      },
      "why": {
        "text": "This is atlas_rows' golden-vector gate. The frozen ~338MB catalog is gitignored and irreplaceable, so byte-exact regenerability from committed inputs is the only standing evidence the layout reproduces. It also pins the load-bearing byte-order finding: the rebucketer's first-seen order makes the generator's legacy (buggy) shard bucketing byte-order-load-bearing, so the verifier MUST replicate it to hit the frozen bytes.",
        "cites": [
          {
            "path": "tools/atlas/verify_atlas_repro.mjs",
            "lines": "25-47"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "169-178"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "boundary:fits_io",
      "to": "src/engine/pipeline/m1_ingestion/fits_decoder.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/m1_ingestion/fits_decoder.ts:1127",
      "verification": "VERIFIED",
      "how": {
        "what": "in: FITS header RA in DEGREES (big-endian file, CRPIX 1-indexed); out: internal hard.ra_hint in HOURS",
        "transform": "hard.ra_hint = ra / 15 (engine ingest site; one of THREE non-chokepoint conversion sites)",
        "lossy": false
      },
      "why": {
        "text": "crval[0]/catalog RA is HOURS internally; degrees only at the FITS boundary. A rewire that trusts a 'single chokepoint' would miss fits_decoder.ts:1127 (and stack + package.ts) — exactly the units bug LAW 7 exists to prevent.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "297-317"
          }
        ]
      },
      "review_note": "fits_decoder.ts:1127 hard.ra_hint = ra/15, comment:1125 'header RA is in DEGREES, hint contract is HOURS'. deg->hours confirmed (schema's own :403 ref is stale)."
    },
    {
      "from": "boundary:libraw_mem_image",
      "to": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:118",
      "verification": "VERIFIED",
      "how": {
        "what": "uint16 interleaved RGB16, length w*h*3, pixel (x,y) ch c at (y*w+x)*3+c; ADU per-channel 16-bit linear; image-space y-down; dominant-channel with ~4-7% cross-leak (NOT one-hot, NOT CFA)",
        "transform": "convertMemImageToRgb detects dominant-channel-vs-demosaiced (multi-channel-lit + median 2nd/max ratio) and routes; consumers do not branch on format",
        "lossy": true
      },
      "review_note": "binary_layouts.ts:118-135 strideRule/units/coord match how (interleaved RGB16 w*h*3, ~4-7% cross-leak NOT one-hot, convertMemImageToRgb detect+route); metadata_reaper gating referenced at :124."
    },
    {
      "from": "boundary:libraw_mem_image",
      "to": "tools/color/rgb_to_xyz.mjs",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:139",
      "verification": "VERIFIED",
      "how": {
        "what": "same interleaved dominant-channel RGB16 mem_image",
        "transform": "secondary EXACT-meta-match dims recovery / tie-break (schema notes this lane currently hosts the tie-break rule, not the shared psf lane)",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:139 notes 'tie-break rule currently lives in tools/color/rgb_to_xyz.mjs' — secondary exact-meta recovery, matches how."
    },
    {
      "from": "boundary:libraw_mem_image",
      "to": "tools/psf/decode_cr2.mjs",
      "type": "unit-conversion",
      "cite": "tools/psf/decode_cr2.mjs:91",
      "verification": "VERIFIED",
      "how": {
        "what": "in: interleaved dominant-channel RGB16 mem_image whose w*h can factor TWO ways; out: three linear Float32 planes [0,1]",
        "transform": "candidateDims enumerates near-meta factorizations, strideCoherence tie-break (mean |v(y)-v(y+2)|, period-2 Bayer parity) picks the true stride so a 5196x3468 near-miss cannot beat exact 5202x3464; then demosaicBilinear",
        "lossy": true
      },
      "why": {
        "text": "Without the strideCoherence tie-break a near-tied dims factorization SHEARS the frame diagonally (the 60Da 413.3-vs-411.1 case picked 3468x5196 over the exact 3464x5202). Proximity to meta.width alone cannot disambiguate because 5196 is nearer meta's 5184 than the true 5202.",
        "cites": [
          {
            "path": "tools/psf/decode_cr2.mjs",
            "lines": "113-134"
          },
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "130-132"
          }
        ]
      },
      "review_note": "decode_cr2.mjs:91 candidateDims + strideCoherence:121-134 confirm the 60Da tie-break (5196x3468 vs true 5202x3464 shear); ADU->Float32[0,1] normalization is the unit conversion. Type slightly loose (also a decode) but defensible."
    },
    {
      "from": "boundary:rawler_cfa",
      "to": "boundary:libraw_mem_image",
      "type": "fallback",
      "cite": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts:64",
      "verification": "VERIFIED",
      "how": {
        "what": "primary = rawler_cfa (isRawlerDecoderEnabled default TRUE); fallback = libraw_mem_image COLD PATH",
        "transform": "VITE_DECODER_RAWLER=0 (or 'false') selects the retained libraw cold path (byte-identical pre-cutover pins); unset/truthy => rawler default arm",
        "lossy": false
      },
      "why": {
        "text": "Decoder cutover #14 made rawler the default RAW arm but the owner ruling KEPT libraw as a never-delete cold path; the two boundaries carry DIFFERENT contracts (rawler = full single-channel CFA + OB borders; libraw = interleaved dominant-channel RGB16), so the switch is a real fork, not a drop-in.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "320-324"
          }
        ]
      },
      "review_note": "rawler_decoder.ts:64-74 isRawlerDecoderEnabled: v==='0'||'false' => false (libraw cold), else true (rawler default), catch=>true. Fallback confirmed."
    },
    {
      "from": "boundary:rawler_cfa",
      "to": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:325",
      "verification": "VERIFIED",
      "how": {
        "what": "uint16 single-channel Bayer CFA mosaic (cpp=1), FULL sensor frame incl. optical-black borders, length width*height, photosite (x,y) at y*width+x; color = 2x2 CFA tile at phase (y&1)*2+(x&1); raw ADU with per-frame black pedestal (NOT subtracted); LE wasm boundary; y-down full-frame coords",
        "transform": "decodeCfaContract / decodeRawlerForPipeline (integer-bilinear demosaic of ACTIVE area, phase-correct via absolute coords, OB clamped out of science pixels; blacklevel/white/WB ride the contract, blacklevel varies PER FRAME)",
        "lossy": false
      },
      "why": {
        "text": "The OB borders are the per-frame dark anchor (DARK_CALIBRATION_POLICY) so consumers must NOT pre-trim before measurement; and blacklevel is per-frame (2046 vs 2045 same camera), so a hardcoded pedestal biases photometry. This is the DEFAULT RAW arm since the 2026-07-11 cutover.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "328-342"
          }
        ]
      },
      "review_note": "binary_layouts.ts:327-345 single-channel CFA cpp=1 full-frame incl OB borders, phase (y&1)*2+(x&1), per-frame blacklevel 2046 vs 2045. Confirmed default arm."
    },
    {
      "from": "boundary:rawler_cfa",
      "to": "stage:extract.decode",
      "type": "evidence",
      "cite": "tools/api/solve_cr2.apispec.ts:118-123",
      "verification": "VERIFIED",
      "how": {
        "what": "expect(receipt.pipeline_provenance.decoder_arm).toBe('rawler') on the bundled CR2 — proves the raw buffer crossed the rawler_cfa decode boundary (default arm) before extract.decode produced pixels",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_cr2.apispec.ts:122 expect(receipt.pipeline_provenance.decoder_arm).toBe('rawler') on bundled CR2 — proves the raw buffer crossed the rawler default-arm decode. Opened, exact."
    },
    {
      "from": "boundary:tauri_native_ipc",
      "to": "src/engine/core/NativeGpuBridge.ts",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:372",
      "verification": "VERIFIED",
      "how": {
        "what": "INPUT Bayer CFA uint16 (length w*h, y*w+x); OUTPUT interleaved RGBA float32 (length w*h*4, (y*w+x)*4+c); raw little-endian bytes BOTH directions; PIXEL ledger y-down; CFA phase RGGB/Canon-hardcoded in the native shader",
        "transform": "NativeGpuBridge.demosaic consumes Uint8Array body + width/height headers -> ArrayBuffer; TRANSPORT-only binary IPC (retired the 300-700MB Array.from JSON round-trip); data layout identical to prior JSON path",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:366-389 tauri_native_ipc entry (Bayer u16 in / RGBA f32 out, raw LE both directions, RGGB native shader, transport-only) + NativeGpuBridge.ts:50-54 demosaic() builds a Uint8Array body over the Uint16Array, invoke('demosaic_native', bytes, {width/height headers}) -> ArrayBuffer->Float32Array. Both ends confirmed."
    },
    {
      "from": "boundary:wasm_typed_array",
      "to": "src/engine/pipeline/m10_psf/psf_field.ts",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:250",
      "verification": "VERIFIED",
      "how": {
        "what": "refine_stars_lm OUTPUT = 13 f64/star [A,cx,cy,sx,sy,theta, sigmaA,sigmaCx,sigmaCy,sigmaSx,sigmaSy,sigmaTheta, covStatus]; A=luma ADU, cx..sy=pixels, theta=radians; native pixel grid y-down; little-endian wasm linear memory",
        "transform": "psf_field consumes with stride AUTO-DETECT (6-vs-13); sigmas ADDITIVE (all NaN when covStatus!=0); values COPIED (a zero-copy view is invalidated by any wasm realloc)",
        "lossy": false
      },
      "why": {
        "text": "The output stride graduated 6->13/star (JTJ covariance surgery). A consumer that assumed the old 6-wide stride would read sigmas as refined params and shear the whole per-star product. The auto-detect is the compatibility seam (psf_field.ts:393 `stride = out.length >= N*13 ? 13 : 6`, :394 `hasCov = stride === 13`); the first 6 stay BIT-IDENTICAL to the pre-covariance output.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "253-261"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "393-394"
          }
        ]
      },
      "review_note": "binary_layouts.ts:254-268 refine_stars_lm OUTPUT 13 f64/star, first 6 bit-identical, sigmas NaN when covStatus!=0, values COPIED. Schema-faithful; the 6-vs-13 auto-detect is a psf_field consumer claim not code-checked here."
    },
    {
      "from": "boundary:wasm_typed_array",
      "to": "stage:solve.quad_wasm",
      "type": "evidence",
      "cite": "tools/api/solve_seestar.apispec.ts:93-101",
      "verification": "VERIFIED",
      "how": {
        "what": "receipt.reproducibility.wasm.compute_pkg is asserted to match the pattern 'wasm-compute <digit>...' — a real runtime-measured identity_version() string from the compiled wasm-compute crate, proving the wasm_typed_array crossing is the real module, not a stub",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_seestar.apispec.ts:100 expect(receipt.reproducibility.wasm.compute_pkg).toMatch(/^wasm-compute [0-9]/) — real runtime identity_version() of the compiled wasm-compute crate (the quad solver's module). Confirmed; indirect but valid evidence the crossing is the real module."
    },
    {
      "from": "external:astrometry_net_oracle",
      "to": "tools/overnight/astrometry_truth.mjs",
      "type": "data-flow",
      "cite": "tools/overnight/astrometry_truth.mjs:6-16",
      "verification": "VERIFIED",
      "how": {
        "what": "a fitted WCS solution (.wcs FITS header, NAXIS=0) produced by the external solve-field binary",
        "transform": "parsed via readFitsHeaderFd and adapted to a canonical TruthLabel record (fromAstrometryNetWcs)",
        "lossy": false
      },
      "review_note": "astrometry_truth.mjs:6-21 shells out to external solve-field, parses .wcs via readFitsHeaderFd -> canonical fromAstrometryNetWcs TruthLabel; index files external, paths configurable never hardcoded. Confirmed."
    },
    {
      "from": "external:astrometry_net_oracle",
      "to": "tools/overnight/nova_truth.mjs",
      "type": "data-flow",
      "cite": "tools/overnight/nova_truth.mjs:9-20",
      "verification": "VERIFIED",
      "how": {
        "what": "a solved job result (WCS + calibration fields) polled from the nova.astrometry.net cloud API after an xylist/image submission",
        "transform": "job result adapted to a GOLD TruthLabel written to labels.json, tagged with a nova provenance block distinguishing it from the local oracle",
        "lossy": false
      },
      "review_note": "nova_truth.mjs:6-21 pushes no-GOLD frames to nova.astrometry.net (xylist default, image fallback), emits the SAME TruthLabel record shape as the local oracle + a nova provenance block, writes GOLD to labels.json on success, no-op without key. Confirmed."
    },
    {
      "from": "external:gaia_catalog",
      "to": "tools/starplates/download_gaia_chunked.mjs",
      "type": "data-flow",
      "cite": "tools/starplates/download_gaia_chunked.mjs:51,148-190",
      "verification": "VERIFIED",
      "how": {
        "what": "Gaia DR3 rows (source_id, ra_deg, dec_deg, pmra, pmdec, phot_g_mean_mag, bp_rp) as ADQL CSV over ESA TAP async job polling",
        "transform": "CSV rows parsed and spilled to per-cell buffer files for the downstream Arrow cell builder",
        "lossy": false
      },
      "review_note": "download_gaia_chunked.mjs:51-53 TAP_BASE gea.esac.esa.int/tap-server/tap + gaiadr3.gaia_source + 7 REQUIRED_COLUMNS (source_id/ra_deg/dec_deg/pm/g_mag/bp_rp). Confirmed. MINOR: transform 'spilled to per-cell buffer files' is loose — this tool spills to raw CHUNK CSVs (:56); the per-cell spill is build_gaia_cells.mjs. Edge direction is correct."
    },
    {
      "from": "external:starplates_r2",
      "to": "tools/starplates/publish_r2.mjs",
      "type": "data-flow",
      "cite": "tools/starplates/publish_r2.mjs:137-157",
      "verification": "VERIFIED",
      "how": {
        "what": "built release directory objects (manifest.json + Arrow cell blobs) PUT to the R2 bucket over the S3-compatible signed HTTP API",
        "transform": "raw file bytes uploaded as-is, no re-encoding",
        "lossy": false
      },
      "review_note": "publish_r2.mjs:49 const BUCKET = ...||'starplates'; :136-157 s3Env reads R2_ACCOUNT_ID/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY, s3Request SigV4-signs and fetch-PUTs to <account>.r2.cloudflarestorage.com/<bucket>/<key>, body as-is. Confirmed."
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:arrow_seam",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:208",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the atlas->solver Arrow seam: Apache Arrow IPC columnar (validity bitmap + typed data buffer), little-endian (Arrow mandates LE). RA-unit hybridity is INHERITED from atlas_rows, so the same Gaia-deg/HYG-hours per-row discrimination applies through the columnar seam. Distinct from toolchest_arrow_export (the receipt->consumer export).",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "208-225"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:atlas_rows",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:145",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the shipped catalog rows: canonical on-disk form is JSON TEXT (not Arrow), rows are a PER-ROW hybrid (Gaia rows RA in DEGREES, HYG rows RA in HOURS, never normalized on disk), sector id = decIndex*6+raIndex. Golden fc2c4482 is byte-exact but pinned to the legacy shard bucketing (byte-order-load-bearing). The per-row RA-unit hybridity is the repeat 15x trap this entry exists to pin.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "145-188"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:fits_io",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:293",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the FITS file boundary and THE canonical hours-cost trap: CRVAL RA/Dec are DEGREES at the file but crval[0]/catalog RA is HOURS internally, CRPIX is 1-indexed at the file (crpix[0]/crpix[1] internal, never CRPIX1). There is NO single chokepoint — three live conversion sites (stack fits_io, engine fits_decoder, receipt package.ts). A rewire that trusts a single chokepoint reintroduces exactly the units bug LAW 7 guards.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "293-318"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:libraw_mem_image",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:118",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the libraw decode boundary: interleaved uint16 RGB16 (w*h*3), BAYER mode = dominant-channel with ~4-7% cross-leak (NOT one-hot, NOT CFA), dims factor TWO ways so a near-tied stride-coherence must prefer exact-meta dims (the 60Da shear). This is the retained COLD PATH since decoder cutover #14. Without a same-commit edit here, a stride/dims/units change on this boundary drifts silently past the compile gate.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "117-143"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:rawler_cfa",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:325",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the DEFAULT RAW arm since the 2026-07-11 cutover: single-channel Bayer CFA mosaic (cpp=1), FULL sensor frame INCLUDING optical-black borders (the per-frame dark anchor — do NOT pre-trim before measurement), CFA phase = (y&1)*2+(x&1). Black level varies PER FRAME (2046 vs 2045 same camera), so a hardcoded pedestal biases photometry. Golden 968381f8 (measured). Supersedes libraw_mem_image, which is the cold path.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "325-364"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:seam_capsule",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:457",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the on-disk per-stage session-state capsule (raw LE .bin per typed array + capsule.json sidecar as the SOLE layout authority; NO header in the .bin), sha256-verified on load, y-down PIXEL ledger. Env-gated (CAPTURE_SEAMS=1, default OFF). The sidecar re-embeds RECEIPT_SCHEMA_VERSION and BINARY_LAYOUTS_VERSION (generative, never mirrored). Golden NOT MEASURED until the first frozen capsule bank.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "457-482"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:starplates_blobs",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:190",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the starplates dataplane (t0/t1 blobs + 4-band lost-in-space index, ~40.6MB). Deliberately UNVERIFIED-heavy: dtype/stride/endianness/RA-unit are all marked UNVERIFIED, so the byte layout MUST be transcribed from tools/starplates/build_release.mjs before any consumer rewire. The entry is the standing reminder that this boundary is not yet safe to rewire.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "190-206"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:tauri_native_ipc",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:372",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the JS<->Tauri-native (Rust/wgpu) demosaic_native crossing: Bayer uint16 in (length w*h), interleaved RGBA float32 out (length w*h*4), raw little-endian bytes both directions. TRANSPORT-ONLY change (retired the 300-700MB Array.from JSON round-trip) — dtype/stride/units are identical to the prior JSON path, so the surface version was NOT bumped. Golden NOT MEASURED (needs a packaged Tauri+wgpu runtime).",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "372-410"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:toolchest_arrow_export",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:417",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the receipt->external Arrow export (Feather v2, per-column typed buffers; non-nullable columns carry no validity bitmap). THE units trap: run_summary.ra_hours is HOURS while matched_stars.ra_deg is DEGREES — both carried, both labelled in per-Field Arrow metadata. Distinct from arrow_seam (the atlas->solver catalog store). Golden null here because the committed .arrow fixtures are the de-facto goldens pinned by interop tests.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "417-450"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:wasm_typed_array",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:250",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the JS<->wasm(Rust) typed-array crossing. refine_stars_lm OUTPUT graduated 6->13 f64/star (JtJ covariance surgery): the first 6 refined params stay BIT-IDENTICAL to the pre-covariance output, sigmas[6..11] are all-NaN when covStatus!=0, values are COPIED (a zero-copy view dies on any wasm realloc). Golden 8ed7ac19. Any Rust signature change requires wasm-pack build + owner sign-off (LAW 5), and pkg/ is gitignored.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "250-291"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/contracts/binary_layouts.ts",
      "to": "boundary:wgsl_structs",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:227",
      "verification": "VERIFIED",
      "why": {
        "text": "LAW-7 declaration for the JS->WebGPU struct crossing: WGSL std140/std430-style member alignment (vec3 aligns to 16 bytes, struct size rounded to the largest member alignment), little-endian, PIXEL ledger. SHARED-WGSL HAZARD: one shader consumed by multiple bind layouts, so a struct-member change ripples across every bind group — the reason this crossing is an enumerated boundary.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "227-244"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/AtmosphericManager.ts",
      "to": "src/engine/core/TimeService.ts",
      "type": "data-flow",
      "cite": "src/engine/core/AtmosphericManager.ts:90",
      "verification": "VERIFIED",
      "how": {
        "what": "target RA in HOURS, Dec in DEGREES, observer lat/lon in DEGREES, and an observation Date",
        "transform": "validateObservationTime converts the Date to JD via TimeService.toJulianDate, then TimeService.computeAltAz to get the target altitude for the below-horizon check",
        "lossy": false
      },
      "why": {
        "text": "The Cronos horizon check consumes TimeService's JD + alt-az; a target more than 2 degrees below the horizon fails validation as TARGET_BELOW_HORIZON.",
        "cites": [
          {
            "path": "src/engine/core/AtmosphericManager.ts",
            "lines": "90-98"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/ImageProcessor.ts",
      "to": "src/engine/core/oklab.ts",
      "type": "data-flow",
      "cite": "src/engine/core/ImageProcessor.ts:336",
      "verification": "VERIFIED",
      "how": {
        "what": "per-pixel calibrated LINEAR sRGB triplet [r,g,b] (float, background-neutralized + WB-gained)",
        "transform": "renderOklabStretch runs linearSrgbToOklab -> oklabToOklch, applies a lightness-only MTF stretch with a chroma-space desat guard, gamutClipPreserveHue, then oklchToLinearSrgb -> encodeSrgb; hue is held exactly, output is Uint8",
        "lossy": true
      },
      "why": {
        "text": "The Oklab path is render-only and DEFAULT-OFF (renderOpts.oklab), branching after all shared LINEAR calibration so the flag-off render is byte-identical; every post-transform readout is APPROXIMATE (Oklab L is not photometric luminance) and Math.cbrt is only per-platform deterministic.",
        "cites": [
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "244-249"
          },
          {
            "path": "src/engine/core/oklab.ts",
            "lines": "30-58"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/ImageProcessor.ts",
      "to": "src/engine/pipeline/m2_hardware/scale_manager.ts",
      "type": "unit-conversion",
      "cite": "src/engine/core/ImageProcessor.ts:81",
      "verification": "VERIFIED",
      "how": {
        "what": "magnifier center (centerX, centerY) in NATIVE sensor pixel coordinates",
        "transform": "when the science buffer is the binned size (scienceW*scienceH), getCrop calls scales.nativeToscience(centerX, centerY) to map native -> science(binned) pixel space before indexing the buffer",
        "lossy": false
      },
      "why": {
        "text": "A native-vs-binned pixel-space mismatch would index the wrong region; the conversion is applied only when the buffer length equals the binned dimensions, otherwise the native coords are used as-is.",
        "cites": [
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "77-93"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/SkyTransform.ts",
      "to": "boundary:wasm_typed_array",
      "type": "unit-conversion",
      "cite": "src/engine/core/SkyTransform.ts:113",
      "verification": "VERIFIED",
      "how": {
        "what": "WCS scalars (crval[0] RA in HOURS, crval[1] Dec in DEGREES, CD matrix in DEGREES) plus a Float64Array of pixel (x,y) coords for bulkPixelsToSky / gnomonic / fit_wcs kernels",
        "transform": "every angle is converted to RADIANS before crossing into the wasm_compute kernel (hoursToRad for RA, degToRad for Dec and CD terms); the returned radians are converted back with radToHours (RA) / radToDeg (Dec) on the way out",
        "lossy": false
      },
      "why": {
        "text": "This is the LOUD hours<->degrees<->radians seam: the JS side speaks RA-hours/Dec-degrees, the wasm math speaks radians, and the round-trip conversion lives entirely in SkyTransform. Getting hoursToRad vs degToRad wrong on RA would shear the whole solution by the 15x hours/degrees factor.",
        "cites": [
          {
            "path": "src/engine/core/SkyTransform.ts",
            "lines": "46-72"
          },
          {
            "path": "src/engine/core/SkyTransform.ts",
            "lines": "107-131"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/SkyTransform.ts",
      "to": "src/engine/core/optics_manager.ts",
      "type": "data-flow",
      "cite": "src/engine/core/SkyTransform.ts:66",
      "verification": "VERIFIED",
      "how": {
        "what": "tangent-plane coordinates (xi, eta) in DEGREES plus a DistortionModel (Brown-Conrady k1/k2/k3/p1/p2)",
        "transform": "gnomonicProject calls OpticsManager.applyDistortion(xi, eta, ...) to push ideal->observed on the forward projection; inverseGnomonic calls OpticsManager.removeDistortion to undo it before de-projecting to sky",
        "lossy": false
      },
      "why": {
        "text": "Distortion is applied in tangent-plane degrees, symmetric between the forward and inverse projections; only invoked when a distortion profile is supplied, so the undistorted path is unchanged.",
        "cites": [
          {
            "path": "src/engine/core/SkyTransform.ts",
            "lines": "46-102"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/StatisticsProvider.ts",
      "to": "boundary:wasm_typed_array",
      "type": "data-flow",
      "cite": "src/engine/core/StatisticsProvider.ts:24",
      "verification": "VERIFIED",
      "how": {
        "what": "a Float32Array pixel buffer plus iterations and sigma_clip scalars",
        "transform": "estimateBackground calls wasm.estimate_background_wasm(data, iterations, sigma_clip) which returns a 2-element [median, sigma] summary from iterative sigma-clipping",
        "lossy": true
      },
      "why": {
        "text": "The full pixel buffer crosses into wasm and only a 2-scalar background/noise summary returns; this median/sigma is the reference every detection threshold is measured against.",
        "cites": [
          {
            "path": "src/engine/core/StatisticsProvider.ts",
            "lines": "18-28"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/StatisticsProvider.ts",
      "to": "boundary:wasm_typed_array",
      "type": "fallback",
      "cite": "src/engine/core/StatisticsProvider.ts:29",
      "verification": "VERIFIED",
      "how": {
        "what": "the same Float32Array buffer, on the wasm-failure arm",
        "transform": "if wasm.estimate_background_wasm throws, a try/catch falls back to a pure-CPU iterative sigma-clip (MAD*1.4826 sigma) computing the same {median, sigma}, logging a PERFORMANCE CRITICAL warning",
        "lossy": true
      },
      "why": {
        "text": "Honest degradation path: the CPU arm produces the same summary contract as the wasm arm so consumers never see a gap, only a slower run when wasm is unavailable.",
        "cites": [
          {
            "path": "src/engine/core/StatisticsProvider.ts",
            "lines": "29-52"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/camera_color_matrix.ts",
      "to": "src/engine/core/ImageProcessor.ts",
      "type": "data-flow",
      "cite": "src/engine/core/ImageProcessor.ts:157",
      "verification": "VERIFIED",
      "how": {
        "what": "ColorTransform.matrix — a 3x3 camera-raw-RGB -> linear-sRGB rotation (D65 preMul folded column-wise), row-major number[][], produced by resolveColorTransform from the per-body ColorMatrix2",
        "transform": "float32ToImageDataAutoStretch calls applyColorMatrixLinear(src, w, h, colorTransform.matrix) to project each interleaved RGB pixel into linear sRGB BEFORE the STF calibration/stretch",
        "lossy": true
      },
      "why": {
        "text": "The matrix is the sole per-body chromatic-rotation input to the render; out-of-gamut negatives are clamped to 0 (deep-red/blue rotations lost), and when resolveColorTransform returns null the render aliases the input and is byte-identical to the pre-color path.",
        "cites": [
          {
            "path": "src/engine/core/camera_color_matrix.ts",
            "lines": "205-221"
          },
          {
            "path": "src/engine/core/ImageProcessor.ts",
            "lines": "361-375"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/core/optics_manager.ts",
      "to": "src/engine/core/optics_hint_provider.ts",
      "type": "fallback",
      "cite": "src/engine/core/optics_manager.ts:12",
      "verification": "VERIFIED",
      "how": {
        "what": "a FocalLengthHintContext (untrusted EXIF focal length, lens model string, optional explicit user hint)",
        "transform": "when the nominal focal length is not trustworthy (electronics-less factory-default 50 mm), getEffectiveFocalLength queries queryFocalLengthHintProviders; the first non-null provider returns a labelled OpticsHint (assumed:true, e.g. WIDE_FIELD_FL_PRIOR_MM = 14) that REPLACES the untrusted FL as a scale-search seed",
        "lossy": false
      },
      "why": {
        "text": "Last-resort degradation arm (honest-or-absent): the hint is an ASSUMPTION recorded in the receipt (optics_hints), null when the FL is trusted evidence so nothing is assumed; because hints only seed and the math verify gate arbitrates, a wrong hint can only fail to verify, never corrupt a verified solve.",
        "cites": [
          {
            "path": "src/engine/core/optics_manager.ts",
            "lines": "10-27"
          },
          {
            "path": "src/engine/core/optics_hint_provider.ts",
            "lines": "8-24"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/capture_persist.ts",
      "to": "src/engine/ui/widgets/widget_persist.ts",
      "type": "data-flow",
      "cite": "src/engine/events/capture_persist.ts:78",
      "verification": "VERIFIED",
      "how": {
        "what": "a versioned PersistedRunMap (runId -> CaptureEnvelope[]) under key 'skycruncher.capture.runs' at CAPTURE_PERSIST_VERSION=1, bounded to PERSIST_RUNS_CAP=60 runs (oldest dropped first)",
        "transform": "persistRun writes the map via writeVersioned; loadPersistedRunMap reads it via readVersioned with a structural guard, degrading a stale-version or corrupt blob to {} (non-fatal, honest-absent) rather than throwing",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/capture_record.ts",
      "to": "src/engine/events/capture_aggregate.ts",
      "type": "data-flow",
      "cite": "src/engine/events/capture_aggregate.ts:116",
      "verification": "VERIFIED",
      "how": {
        "what": "CaptureEnvelope[][] — one inner array per run (run_id, frame_sha, stage_id, seq, t_start, t_end, ms, ok, verdict, counts, warnings, payload_ref)",
        "transform": "aggregateCaptureRuns dedups by frame content hash (latest run of a frame wins; a null sha never merges) then rolls up per-stage reached/passed/failed and min/avg/max ms over successful-solve frames only",
        "lossy": true
      },
      "why": {
        "text": "The dedup key is the OWNER-mandated frame_sha: multiple runs of one frame must collapse to its latest run so the flowchart's pass/fail and timing stats are not skewed by re-runs. Lossy by design — many envelopes reduce to per-stage aggregates.",
        "cites": [
          {
            "path": "src/engine/events/capture_aggregate.ts",
            "lines": "10-22"
          },
          {
            "path": "src/engine/events/capture_aggregate.ts",
            "lines": "116-124"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/capture_record.ts",
      "to": "src/engine/events/capture_persist.ts",
      "type": "data-flow",
      "cite": "src/engine/events/capture_record.ts:312",
      "verification": "VERIFIED",
      "how": {
        "what": "(runId: string, envelopes: CaptureEnvelope[], rawEvents) delivered through the CaptureSink callback registered by installCapturePersistSink",
        "transform": "CaptureRecorder.flush invokes every registered sink after storing/mirroring; capture_persist's sink calls persistRun to mirror the envelopes to localStorage. Each sink runs inside its own try/catch so one sink's failure never breaks the run",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/pipeline_events.ts",
      "to": "src/engine/events/capture_record.ts",
      "type": "control",
      "cite": "src/engine/events/capture_record.ts:255",
      "verification": "VERIFIED",
      "why": {
        "text": "run_finished is the FLUSH TRIGGER (it snapshots the buffered events and builds/stores/sinks the capture record). The frame_sha write-race barrier orders that flush: awaitFrameSha holds the flush until the async content-hash digest settles (on either fate), so a run that reaches run_finished before the digest resolves does not persist a record with a null dedup key. The barrier defaults settled, so an unarmed run (raw-bus tests, pre-hashed path) flushes synchronously, byte-identical to the pre-fix behavior.",
        "cites": [
          {
            "path": "src/engine/events/capture_record.ts",
            "lines": "236-266"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/pipeline_events.ts",
      "to": "src/engine/events/capture_record.ts",
      "type": "data-flow",
      "cite": "src/engine/events/capture_record.ts:224",
      "verification": "VERIFIED",
      "how": {
        "what": "the stamped PipelineEvent stream (run_started/stage_started/stage_finished{ok,ms,verdict?,counts?,payloadRef?}/warning/run_finished, each carrying EventStamp {t, seq, runId, frameSha})",
        "transform": "CaptureRecorder subscribes to the bus and buffers events per run (reset on run_started), then on run_finished builds the per-stage envelopes; the buffer is a raw copy of the emitted events",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/pipeline_events.ts",
      "to": "src/engine/events/stage_timing_summary.ts",
      "type": "data-flow",
      "cite": "src/engine/events/stage_timing_summary.ts:117",
      "verification": "VERIFIED",
      "how": {
        "what": "the PipelineEvent stream — run_started (source_format), run_finished (ok), and every stage_finished {stage, ms, t}",
        "transform": "summarizeStageTimings does one linear pass filling stages[stage]=ms and tracking span endpoints (start = t - ms); total_ms = max(end) - min(start), a SPAN basis (not the sum of stage ms) so nested stages are not double-counted and browser vs headless compute an identical total",
        "lossy": true
      },
      "why": {
        "text": "The span basis (max end minus min start) rather than summing per-stage ms is the load-bearing choice: the calibrate umbrella wraps psf_field/bc_rematch/forced_confirm, so summing would double-count. Using t - ms as each stage's start lets the fold reconstruct the wall span from durations alone, keeping the block free of nondeterministic epoch timestamps.",
        "cites": [
          {
            "path": "src/engine/events/stage_timing_summary.ts",
            "lines": "60-68"
          },
          {
            "path": "src/engine/events/stage_timing_summary.ts",
            "lines": "117-129"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/events/stage_timing_summary.ts",
      "to": "src/engine/pipeline/stages/package.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/package.ts:1091",
      "verification": "VERIFIED",
      "how": {
        "what": "StageTimingSummary {v, run_id, frame_sha, source_format, decoder_arm, ok, n_stages, total_ms, stages{}} supplied as packageInput.stageTimings",
        "transform": "the receipt builders assign receipt.stage_timings = i.stageTimings on BOTH the solved and no-solve receipts — an ENVELOPE-class block (wall-clock ms), outside the byte-identity contract",
        "lossy": false
      },
      "why": {
        "text": "CONTRADICTS the stage_timing_summary.ts docstring claim 'RECEIPT-INERT ... never folded into the receipt' (see fragment DRIFT note): package.ts DOES carry the summary on the receipt as the envelope-class stage_timings field (declared on NoSolveReceipt :845, assigned in the builders). It is additionally persisted to a perf JSONL sidecar by the headless/e2e consumers; the docstring should say envelope-class-on-receipt (like stage_records.ts), not receipt-inert.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "845"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1091"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/export/asdf_writer.ts",
      "to": "src/engine/pipeline/m6_plate_solve/tps_eval.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/export/asdf_writer.ts:612",
      "verification": "VERIFIED",
      "how": {
        "what": "in: fitted TPS block (control_points [u,v] pairs, weights_x, weights_y, scale, affine.dx/dy) + frame dims + receipt crpix; out: two 24x24 float64 Tabular2D lookup tables of corrected pixel-offset coordinates on a frame-spanning (u,v) grid",
        "transform": "buildTpsCorrection samples the LIVE fitted spline via evalTpsField (the IDENTICAL evaluator the fitter used, shared through tps_eval so nothing drifts) at each grid node, baking u' = u - f_dx(u/scale,v/scale), v' = v - f_dy(...) — the SUBTRACTION is the sign fix (the direct Tabular2D output IS the coordinate fed to CD, so it must hold the IDEAL offset)",
        "lossy": true
      },
      "why": {
        "text": "SIGN (same fix as SIP): the fitted field f reproduces the internal residual dx = OBSERVED - IDEAL (tps_fitter convention). Because the gwcs Tabular2D is a direct lookup with no implicit add, the table must hold IDEAL = OBSERVED - (OBSERVED - IDEAL) = u - f; emitting u + f applied the distortion backwards and worsened the astropy-applied residuals. Lossy: the smooth spline is discretized to a 24x24 grid (exact AT the nodes, where the fidelity gate evaluates).",
        "cites": [
          {
            "path": "src/engine/pipeline/export/asdf_writer.ts",
            "lines": "556-577"
          },
          {
            "path": "src/engine/pipeline/export/asdf_writer.ts",
            "lines": "603-620"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/export/asdf_writer.ts",
      "to": "src/engine/pipeline/stages/receipt_serializer.ts",
      "type": "contract",
      "cite": "src/engine/pipeline/export/asdf_writer.ts:60",
      "verification": "VERIFIED",
      "how": {
        "what": "the DROPPED_KEYS Set (heavy typed-array receipt keys the JSON serializer strips)",
        "transform": "asdf_writer's sanitize() skips any key in DROPPED_KEYS while normalizing the receipt into YAML-safe values, so the ASDF YAML tree and the JSON receipt agree on exactly which heavy keys are stripped",
        "lossy": false
      },
      "why": {
        "text": "Sharing DROPPED_KEYS from the JSON serializer is what keeps the two receipt serializations consistent: a key added to the JSON drop-list is automatically stripped from the ASDF YAML too, so the ASDF metadata tree never re-embeds a heavy typed array the JSON receipt intentionally omitted.",
        "cites": [
          {
            "path": "src/engine/pipeline/export/asdf_writer.ts",
            "lines": "737-757"
          },
          {
            "path": "src/engine/pipeline/stages/receipt_serializer.ts",
            "lines": "20-34"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/export/fits_writer.ts",
      "to": "boundary:fits_io",
      "type": "control",
      "cite": "src/engine/pipeline/export/fits_writer.ts:101",
      "verification": "VERIFIED",
      "why": {
        "text": "EXPORT LAW gate: serializeFits THROWS (writes no bytes across the FITS boundary) unless receipt.wcs is present AND receipt.wcs.SOURCE === 'FITTED' — a SYNTHESIZED approximation is never emitted as if it verified stars. It further refuses a non-finite WCS keyword or a shape/length mismatch, so a corrupt or WCS-less file is never produced. This is the honest-or-absent gate deciding whether the fits_io boundary is crossed at all; the sibling taken edge (loud_edges_c) is the data-flow WHEN it is crossed.",
        "cites": [
          {
            "path": "src/engine/pipeline/export/fits_writer.ts",
            "lines": "99-123"
          },
          {
            "path": "src/engine/pipeline/export/fits_writer.ts",
            "lines": "368-373"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/export/fits_writer.ts",
      "to": "boundary:fits_io",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:303",
      "verification": "VERIFIED",
      "how": {
        "what": "consumes receipt.wcs whose CRVAL1 is ALREADY in DEGREES; writes big-endian FITS image + 80-byte header cards",
        "transform": "emits CRVAL verbatim, adds only CRPIX+1; must NOT re-multiply by 15",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:303-305 downstream writers consume already-converted receipt.wcs, must NOT re-x15, only CRPIX+1 (confirmed at fits_writer.ts:209). Cite :302 lands mid-sentence, block is :296-304."
    },
    {
      "from": "src/engine/pipeline/export/sip_convention.ts",
      "to": "src/engine/pipeline/export/asdf_writer.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/export/asdf_writer.ts:346",
      "verification": "VERIFIED",
      "how": {
        "what": "in: internal SIP; out: FITS-standard forward SIP for BOTH the asdf wcs_fits keyword fallback AND the native gwcs polynomial node",
        "transform": "toFitsSip negation (same bridge) so the two exports are byte-consistent and both FITS-conventional; called at asdf_writer.ts:346 (wcs_fits keyword) and :495 (gwcs polynomial node)",
        "lossy": false
      },
      "review_note": "cite :80 is a doc-comment line, but asdf_writer.ts:62 imports toFitsSip and calls it at :346 (wcs_fits keyword) and :495 (gwcs node) — the BOTH-exports claim in how is confirmed by two call sites."
    },
    {
      "from": "src/engine/pipeline/export/sip_convention.ts",
      "to": "src/engine/pipeline/export/fits_writer.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/export/sip_convention.ts:85",
      "verification": "VERIFIED",
      "how": {
        "what": "in: internal SIP (A_internal = OBSERVED-IDEAL); out: FITS-standard forward SIP (A_FITS = IDEAL-OBSERVED)",
        "transform": "toFitsSip negates A and B (A_FITS = -A_internal, orders unchanged); pure coefficient negation, returns a NEW block",
        "lossy": false
      },
      "why": {
        "text": "Emitting the internal coefficients verbatim as FITS A_i_j (the pre-fix M7 bug) makes astropy apply the distortion in the WRONG direction — it WORSENS catalog residuals instead of improving them. In-app consumers use convention (1) end to end and must NOT be touched; the wrong sign only ever existed at the FITS/ASDF serialization boundary.",
        "cites": [
          {
            "path": "src/engine/pipeline/export/sip_convention.ts",
            "lines": "27-54"
          }
        ]
      },
      "review_note": "sip_convention.ts:85 toFitsSip negates A/B (A_FITS=-A_internal); fits_writer.ts:56 imports + :226 calls toFitsSip(sip). Pure negation confirmed."
    },
    {
      "from": "src/engine/pipeline/m10_psf/nebulosity_layer.ts",
      "to": "src/engine/pipeline/stages/package.ts",
      "type": "fallback",
      "cite": "src/engine/pipeline/stages/package.ts:1489",
      "verification": "VERIFIED",
      "how": {
        "what": "NebulosityLayerReceipt | null from buildNebulosityLayerReceipt(i.nebulosityDecomposition ?? null): per-layer present/integrated_flux/support_frac/snr/scale_band/significance plus method, sigma_noise and reconstruction_max_abs_err",
        "transform": "buildReceipt calls buildNebulosityLayerReceipt with the (default-absent) decomposition; a null decomposition yields a null block (honest producer-gap) and the byte-identical solve/receipt path is unchanged",
        "lossy": false
      },
      "why": {
        "text": "The producer (decomposeNebulosityLayers) is a DEFAULT-OFF render tool with no stage wired into the solve path, so nebulosityDecomposition is always absent today and the receipt serializes nebulosity_layer: null (the widget shows DECOMPOSITION NOT RUN). This is an honest producer-gap, not a bug; wiring a producer stage lights the block up with zero further receipt wiring.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/nebulosity_layer.ts",
            "lines": "460-517"
          },
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1483-1489"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_core.ts",
      "to": "src/engine/pipeline/m10_psf/psf_stage.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m10_psf/psf_stage.ts:323",
      "verification": "VERIFIED",
      "how": {
        "what": "PsfKernel { k Float64 (flux-normalized to sum 1), size, nStars } from buildEmpiricalKernel, plus PsfStarMeasure moment measures (FWHM/ellipticity/peakAboveBg) from measureStar and the peak/neighbor helpers",
        "transform": "psf_stage stacks the FWHM-consistent measured stars into an empirical kernel, truncates it (truncateKernel), and uses it as the RL deconvolution kernel; the moment measures also drive target selection (brightest / worst-FWHM) and the text stats",
        "lossy": false
      },
      "why": {
        "text": "The empirical PSF kernel that RL deconvolves with is stacked by psf_core.buildEmpiricalKernel from the sharpest measured stars; it returns null below 5 registerable stars and psf_stage then skips the deconv lane honestly. Without psf_core there is no kernel and no measurement pass.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_core.ts",
            "lines": "191-242"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_stage.ts",
            "lines": "319-332"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_field.ts",
      "to": "src/engine/pipeline/m10_psf/psf_core.ts",
      "type": "fallback",
      "cite": "src/engine/pipeline/m10_psf/psf_field.ts:425",
      "verification": "VERIFIED",
      "why": {
        "text": "When the compiled wasm LM fitter is absent (a strict ESM mock throws on the undefined export, or refine_stars_lm is not a function) resolveDefaultFitter returns null and the whole run degrades to the moment measure (psf_core.measureStar / fitFromMoment), labelling the report MOMENT_FALLBACK. Per-star, an LM fit rejected as non-physical (NaN/negative sigma, off-center, wider than the stamp) also falls back to the moment measure rather than being dropped. It never fabricates a Gaussian fit it did not run (honest-or-absent).",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "184-197"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "416-429"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_field.ts",
      "to": "src/engine/pipeline/stages/package.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/package.ts:1291",
      "verification": "VERIFIED",
      "how": {
        "what": "PsfFieldReport (as i.psfField) including the internal match_epoch signature the field was measured under",
        "transform": "buildReceipt serializes it via serializePsfFieldBlock and derives psf_field.stale_pre_rematch = isStalePreRematch(solution.matchEpoch, report.match_epoch); null passes straight through (honest-or-absent), and the raw per-star fits stay a diagnostic detail not serialized",
        "lossy": true
      },
      "why": {
        "text": "The receipt psf_field block carries only the compact aggregate map and covariance summary, and a stale_pre_rematch flag when the PSF was measured against the pre-rematch match set. Without the match_epoch carry the receipt could silently present a PSF field measured at positions that a later rematch moved.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "144-148"
          },
          {
            "path": "src/engine/pipeline/stages/psf_characterize.ts",
            "lines": "85-109"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_field.ts",
      "to": "src/engine/pipeline/stages/psf_characterize.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/psf_characterize.ts:70",
      "verification": "VERIFIED",
      "how": {
        "what": "PsfFieldReport: method (WASM_LM_GAUSSIAN | MOMENT_FALLBACK | NOT_MEASURED), fwhmMedianMajPx/MinPx (px, null when unmeasured), ellipticityMedian, orientationMedianDeg, per-star fits[] (native-grid x/y px, sigmas, ellipticity, optional JtJ paramSigmas), nCovOk, and the 3x3 regions[] coma map",
        "transform": "runPsfCharacterization calls characterizePsfField with the science luminance buffer, the catalog-matched star positions (planet_ sentinels filtered), and an options block; it then stamps report.grid = SCIENCE_NATIVE | SCIENCE_BINNED2X from its own native-vs-2x-binned dimension check",
        "lossy": false
      },
      "why": {
        "text": "psf_characterize is the stage wrapper that feeds characterizePsfField the solved matched-star positions and tags the grid the FWHM numbers live in. The grid tag is load-bearing: the same fwhm number means different sky angles on a native vs a 2x-binned buffer, so a mis-tag would mis-scale every downstream arcsec conversion.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_characterize.ts",
            "lines": "43-77"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_physics.ts",
      "to": "src/engine/pipeline/stages/psf_attribution.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/psf_attribution.ts:405",
      "verification": "VERIFIED",
      "how": {
        "what": "grid-independent ARCSEC predictors: per-channel diffractionFwhmArcsec (1.028*lambda/D), siderealTrailArcsec (15.041*cos(Dec)*t), seeingArcsec, and the RA-axis drift line PA (deg)",
        "transform": "psf_attribution divides each arcsec predictor by the science-grid pixel_scale to reach px and compares against the MEASURED psf_field FWHM/orientation; the predictors are re-derived in engine-native arcsec so the comparison is binning-safe",
        "lossy": false
      },
      "why": {
        "text": "psf_physics returns everything in arcsec on purpose: arcsec is grid-independent, so the same predictor is valid on a native or 2x-binned buffer and only the consumer's divide-by-pixel_scale changes. The diffraction floor is per-channel (chromatic) and the sidereal drift is exact given Dec and exposure (immutable celestial mechanics).",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_physics.ts",
            "lines": "37-53"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "355-407"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_physics.ts",
      "to": "src/engine/pipeline/stages/psf_attribution.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/stages/psf_attribution.ts:556",
      "verification": "VERIFIED",
      "how": {
        "what": "in: a measured PIXEL-space PSF shape (fwhmMajPx, fwhmMinPx, orientationDeg in image space) + a CD matrix (deg/px); out: a SkyShape (fwhmMajArcsec, fwhmMinArcsec, orientationDeg in the tangent (xi,eta) frame, ellipticity)",
        "transform": "cdToJacobianArcsec scales CD by 3600 to a Jacobian J (arcsec/px), then deprojectShapeByJacobian propagates the shape covariance Sigma_sky = J*Sigma_px*J^T and re-extracts major/minor FWHM + PA in arcsec — the honest anisotropic conversion the scalar pixel_scale (which assumes isotropic square pixels) approximates",
        "lossy": false
      },
      "why": {
        "text": "This is the load-bearing pixel-to-sky shape conversion: a scalar pixel_scale throws away skew, rotation and non-square anisotropy of the local plate, which matters exactly where PSF elongation is being attributed. Propagating the covariance through the CD Jacobian is the only honest way to turn a measured pixel ellipse into a sky-angle ellipse.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_physics.ts",
            "lines": "312-379"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "545-557"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_physics.ts",
      "to": "src/engine/pipeline/stages/science.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/science.ts:72",
      "verification": "VERIFIED",
      "how": {
        "what": "field-center altitude (deg) and plane-parallel airmass sec(z) from targetGeometry(raHours, decDeg, lat, lon, date) + airmassFromAltitude(altDeg)",
        "transform": "science computes the SPCC extinction-provenance airmass at the plate center; the same alt-az apparent-place path psf_attribution uses is reused verbatim (altDeg rounded to 3dp then airmass to 3dp) so the SPCC and attribution airmass agree; below ~3 deg the airmass diverges and is refused (honest-or-absent)",
        "lossy": false
      },
      "why": {
        "text": "UNIT NOTE: targetGeometry takes RA in HOURS (raHours), the engine-internal convention — feeding degrees here would mislocate the star by 15x and poison the airmass. psf_physics is the single home for this apparent-place geometry, so SPCC and psf_attribution get bit-consistent airmass for the same field center and instant.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_physics.ts",
            "lines": "144-161"
          },
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "40-76"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/psf_stage.ts",
      "to": "src/engine/pipeline/orchestrator_session.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2246",
      "verification": "VERIFIED",
      "how": {
        "what": "PsfReport { pedestal, sigmaPixel, satLevel, nMeasured, fwhmMedianPx, ellipticityMedian, regionFwhm 3x3, kernel|null, deconv PsfDeconvReport|null, approximate[], timings } with grid tag",
        "transform": "orchestrator wraps runPsfStage in withStage('psf'), stamps report.grid = SCIENCE_NATIVE | SCIENCE_BINNED2X from its own dimension check, and stores it on this.psfReport for the PSF panel",
        "lossy": false
      },
      "why": {
        "text": "runPsfStage is invoked only on an explicit PSF-diagnostics request (strictly optional stage); the report is held on the session for the UI panel and never gates or mutates the solve, so its absence changes no scenario.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "2246-2259"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/rl_deconv.ts",
      "to": "src/engine/pipeline/m10_psf/nebulosity_layer.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m10_psf/nebulosity_layer.ts:59",
      "verification": "VERIFIED",
      "how": {
        "what": "dilateMask(mask, w, h, r): Chebyshev (square) dilation of a 0/1 Uint8Array mask by r px via separable max",
        "transform": "nebulosity_layer dilates the compact star mask by starDilatePx to form the star FOOTPRINT that is excluded from the nebulosity band, so star halos are not mislabelled as diffuse emission",
        "lossy": false
      },
      "why": {
        "text": "The star footprint used to carve stars (and their halos) out of the nebulosity band is exactly rl_deconv.dilateMask, the same dilation the RL saturated-core freeze uses. Without the dilation, bright-star halos would leak into the nebulosity layer and fabricate diffuse structure around every star.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/nebulosity_layer.ts",
            "lines": "349-350"
          },
          {
            "path": "src/engine/pipeline/m10_psf/rl_deconv.ts",
            "lines": "50-72"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/rl_deconv.ts",
      "to": "src/engine/pipeline/m10_psf/psf_stage.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m10_psf/psf_stage.ts:379",
      "verification": "VERIFIED",
      "how": {
        "what": "RlProtectedResult { estimate Float32 (deconvolved window, same w*h), snapshots[], itersRun, diffuse Float32 (preserved extended component), diffuseRadius }",
        "transform": "psf_stage passes a plane-flattened window + the truncated empirical kernel + a saturated-core freeze mask to richardsonLucyWindowProtected; it re-measures FWHM on the returned estimate and builds the before/after tiles and the iteration strip",
        "lossy": false
      },
      "why": {
        "text": "psf_stage's live deconv lane calls the PROTECTED variant (not the raw RL): the diffuse floor (Ha/OIII/dust) is split out via a box blur at a scale much larger than the PSF, only the compact star residual is sharpened, and the diffuse is added back verbatim so real nebulosity is never flattened or ringed. Deconv remains default-off.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/rl_deconv.ts",
            "lines": "143-183"
          },
          {
            "path": "src/engine/pipeline/m10_psf/psf_stage.ts",
            "lines": "370-385"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/vignette_map.ts",
      "to": "src/engine/pipeline/m10_psf/psf_field.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m10_psf/psf_field.ts:436",
      "verification": "VERIFIED",
      "how": {
        "what": "scalar luminance-band gain g = gainAt(map, x, y, 'luma') >= 1 (dimensionless flux-recovery factor) evaluated at each fit's native position",
        "transform": "each PsfFieldFit gets vignetteGain = g and ampVignetteCorrected = amp * g reported ALONGSIDE the raw amp; the extracted amp is divided by the transmission at the star position, never a buffer pre-warp (MULTILAYER doctrine)",
        "lossy": false
      },
      "why": {
        "text": "This is the CELL-2 measurement-level flux correction on the luminance band: the star amp is corrected by the map value at its position, the native pixels are never warped. It is applied only when the caller supplies a map (flag on), so absence is byte-identical and the raw amp is always preserved.",
        "cites": [
          {
            "path": "src/engine/pipeline/m10_psf/psf_field.ts",
            "lines": "432-443"
          },
          {
            "path": "src/engine/pipeline/m10_psf/vignette_map.ts",
            "lines": "274-287"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/vignette_map.ts",
      "to": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/deep_verify.ts:195",
      "verification": "VERIFIED",
      "how": {
        "what": "scalar luminance-band gain gainAt(map, p.x, p.y, 'luma') (dimensionless) at each forced-photometry probe position",
        "transform": "each forced result gets flux_vignette_corrected = flux * gainAt(...,'luma') reported alongside the raw flux; set only when a vignette map is supplied",
        "lossy": false
      },
      "why": {
        "text": "Forced photometry (catalog-position confirmation) is a luminance-band flux consumer, so it uses the luma gain to recover optical-falloff-corrected flux at the probe position without touching the pixels. Additive and default-off: the raw flux and confirmation verdict are unchanged when no map is present.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
            "lines": "67-100"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m10_psf/vignette_map.ts",
      "to": "src/engine/pipeline/m8_photometry/spcc_calibrator.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m8_photometry/spcc_calibrator.ts:663",
      "verification": "VERIFIED",
      "how": {
        "what": "three PER-BAND gains gainAt(map, x, y, 'r'|'g'|'b') (dimensionless), plus serializeVignetteMap(map) for the receipt",
        "transform": "each matched star's forced fluxes are multiplied per channel: fR*=gainAt(r), fG*=gainAt(g), fB*=gainAt(b) at the star's native position before the color fit; the serialized per-band a2/a4/fit_rms/valid ride the spcc receipt block",
        "lossy": false
      },
      "why": {
        "text": "SPCC is the COLOR consumer, so it must use the PER-BAND gains: an achromatic single gain cancels in the flux_b/flux_r ratio and would not correct color at all. Applying r/g/b independently is the whole reason vignette_map fits per channel. Gated by PSF_FLUX_VIGNETTE_CORRECT (default off) so the color fit is byte-identical when absent.",
        "cites": [
          {
            "path": "src/engine/pipeline/m8_photometry/spcc_calibrator.ts",
            "lines": "604-665"
          },
          {
            "path": "src/engine/pipeline/m10_psf/vignette_map.ts",
            "lines": "17-23"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m1_ingestion/format_registry.ts",
      "to": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "type": "control",
      "cite": "src/engine/pipeline/m1_ingestion/format_registry.ts:305",
      "verification": "VERIFIED",
      "how": {
        "what": "magic-byte format dispatch (RegistryFormatId | null) + honesty tier that gates the isRaw / science-vs-demo arm",
        "transform": "sniffFormatId iterates FORMAT_REGISTRY and returns the FIRST descriptor whose magic-byte sniff matches (CR2's TIFF II|MM + 'CR' marker before the bare-TIFF fallback); metadata_reaper.detectMagicFormatSync delegates its FITS/CR2 verdict here, then routes RAW/FITS down the sensor-data arm and demo formats to the raster decoder",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "to": "src/engine/pipeline/m1_ingestion/fits_decoder.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts:418",
      "verification": "VERIFIED",
      "how": {
        "what": "FITS byte buffer -> parsed header cards + HardMetadata mapping + decoded image + wide-sample value domain + NaN mask",
        "transform": "parseFitsMetadata calls parseFitsHeader -> fitsHeaderToHardMetadata (header cards -> HardMetadata: RA degrees->hours, DATE-OBS->timestamp) and computeFitsValueDomain (retains physical domain for BITPIX +/-32 wide-sample frames as recording only; the normalized [0,1] pixels are untouched, null on the int16 SeeStar path)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "to": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts",
      "type": "control",
      "cite": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts:549",
      "verification": "VERIFIED",
      "how": {
        "what": "flag-and-format gated decode-arm selection inside extractRawSensorData",
        "transform": "RAF (Fuji X-Trans) sets forceLibrawArm and bypasses rawler unconditionally (no X-Trans on the rawler wasm rail); otherwise isRawlerDecoderEnabled() (default TRUE) routes to decodeRawlerForPipeline, and VITE_DECODER_RAWLER=0/'false' selects the retained libraw cold path (byte-identical pre-cutover)",
        "lossy": false
      },
      "why": {
        "text": "This is the decoder-cutover #14 fork at the module seam: rawler is the default RAW arm but libraw is a never-delete cold path, and RAF must force libraw because the rawler crate hardcodes a 2x2 Bayer CFA and cannot decode the 6x6 X-Trans mosaic. The two arms carry different contracts, so the selection is a real fork, not a drop-in.",
        "cites": [
          {
            "path": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
            "lines": "541-559"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "to": "src/engine/pipeline/m2_hardware/sensor_db.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts:450",
      "verification": "VERIFIED",
      "how": {
        "what": "camera_model string + FITS GAIN header card (ZWO gain SETTING, integer) -> SensorProfile + gain_e_adu (e-/ADU) seeded onto the FrameContext",
        "transform": "parseFitsMetadata: findSensorByCamera(camera_model) then getGainForSetting(sensor, GAIN) = piecewise-linear over gain_curve, divided by 2^adu_bit_shift for the 12->16-bit ADC expansion; falls back to gain_e_adu 0.05 when no gain curve resolves (honest default)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts",
      "to": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m1_ingestion/rawler_decoder.ts:503",
      "verification": "VERIFIED",
      "how": {
        "what": "decode payload { data: Float32 interleaved RGB (length active_w*active_h*3, values [0,1]), width/height (active area), stride, isDemosaiced:true, arrowTable, rawler: RawlerCfaRecord } returned to extractRawSensorData",
        "transform": "decodeRawlerForPipeline: wasm full-frame CFA -> integer-bilinear demosaic of the ACTIVE area -> rgb16 * (1/65535) in the raw-ADU-pedestal domain (NO black-subtract/white-scale by default, LAW 2); the additive rawler record (OB areas, per-channel levels, WB, active/crop rects) is carried honest-or-absent (undefined on the libraw path)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
      "to": "src/engine/pipeline/m2_hardware/lens_profiles.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m2_hardware/lens_distortion.ts:138",
      "verification": "VERIFIED",
      "how": {
        "what": "LensProfile.distortion table (keyed by sampled focal lengths) + a chosen focal length (mm) -> DistortionCoeffs { k1, k2, k3?, p1, p2 }",
        "transform": "findLensByModel/LENS_DB then interpolateDistortion: exact-match passthrough, clamp to the sampled bounds, else linear blend of the two nearest sampled focal lengths; sorts a COPY of profile.focal_lengths so the shared LENS_DB is never mutated (radial k1/k2 only are applied downstream; p1/p2 carried but not applied)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
      "to": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m2_hardware/lens_distortion.ts:302",
      "verification": "VERIFIED",
      "how": {
        "what": "LensDistortionResolution { k1, k2, coeffs, provenance, lensKey, focalLength } -> undistorted MATCHING coordinates (native distorted -> corrected)",
        "transform": "resolveLensDistortion picks the prior (rung-0 measured identity > USER_HINT > EXIF_TRUSTED), then makeBrownConradyDistortion(k1,k2,w,h).toCorrected undistorts detection POSITIONS via a 10-iteration radial fixed point (r normalized to the half-diagonal, center=frame center) BEFORE quad/code formation; ORIGINAL native pixel coordinates are kept for photometry (COORDINATE ledger, no pixel resample)",
        "lossy": false
      },
      "why": {
        "text": "resolveLensDistortion returns null (identity NO-OP) unless a lens is confidently resolved, and it rejects the truthy placeholder 'Unknown'/'Unknown Lens' via the same guard optics_manager uses, keying on the LENS MODEL and never on the (possibly lying) focal-length value. It must return null for the bundled CR2, which is what preserves the byte-identical-by-construction guarantee when no trusted lens resolves.",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
            "lines": "170-181"
          },
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
            "lines": "354-360"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m2_hardware/optics_resolver.ts",
      "to": "src/engine/core/optics_manager.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m2_hardware/optics_resolver.ts:72",
      "verification": "VERIFIED",
      "how": {
        "what": "HardMetadata -> effective focal length (mm) + OpticsHint | null (the labelled assumption for the receipt)",
        "transform": "OpticsManager.resolveFocalLengthWithHint trust ladder: explicit focal_length_hint_mm (user evidence) > hint-provider seam (wide-field prior, triggered by the fl===50 + placeholder-lens discriminator) > raw EXIF focal_length",
        "lossy": false
      },
      "why": {
        "text": "This is the LYING-EXIF trap gate: the bundled CR2 carries focal_length=50 (a factory-default lie; the real lens is a 14mm Rokinon) with no LensModel. Reading that 50 as truth would seed the scale search at the wrong focal length, so the resolver never trusts the raw EXIF focal directly — it routes through the hint providers, which encapsulate the fl===50 + placeholder-lens discriminator and can replace it with a named wide-field assumption that rides out to the receipt.",
        "cites": [
          {
            "path": "src/engine/core/optics_manager.ts",
            "lines": "102-131"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m2_hardware/optics_resolver.ts",
      "to": "src/engine/pipeline/m2_hardware/sensor_db.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m2_hardware/optics_resolver.ts:79",
      "verification": "VERIFIED",
      "how": {
        "what": "camera body string -> SensorProfile.pixel_size_um (micrometers)",
        "transform": "findSensorByCamera(cameraModel) (exact-first, overlap-scored, ambiguity->null); a null profile or non-positive pitch makes resolveOpticsFromExif return null so the DSLR frame falls through to the blind solve instead of a fabricated scale",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m2_hardware/optics_resolver.ts",
      "to": "src/engine/pipeline/stages/metrology.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/m2_hardware/optics_resolver.ts:54",
      "verification": "EXTRACTED",
      "how": {
        "what": "in: focal length (mm) + sensor pixel pitch (micrometers); out: pixel scale (arcsec/px) tagged source EXIF_SENSOR_DB",
        "transform": "computeScaleFromOptics = 206.265 * pitch_um / FL_mm (the 206265 arcsec/radian small-angle constant with the um->mm folding pre-applied); metrology consumes it as the EXIF/USER scale seed but it does NOT earn the FITS-header trust skip, so Vector Consensus still refines",
        "lossy": false
      }
    },
    {
      "from": "src/engine/pipeline/m2_hardware/scale_manager.ts",
      "to": "src/engine/pipeline/stages/detect.ts",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/m2_hardware/scale_manager.ts:78",
      "verification": "EXTRACTED",
      "how": {
        "what": "in: native arcsec/px + native (x,y) sensor pixels; out: science-buffer arcsec/px + science (x,y) coordinates (and the inverse scienceToNative)",
        "transform": "science buffer = floor(sensor/2) on each axis (2x2 Bayer bin), so getscienceScale = nativeScale * (nativeW/scienceW) ~= x2 and scienceToNative maps detected stars back to sensor pixels by (nativeW/scienceW); NATIVE<->science<->PREVIEW are the three tracked resolution spaces",
        "lossy": false
      }
    },
    {
      "from": "src/engine/pipeline/m3_gpu_preprocess/demosaic_pipeline.ts",
      "to": "boundary:wgsl_structs",
      "type": "contract",
      "cite": "src/engine/contracts/binary_layouts.ts:227",
      "verification": "VERIFIED",
      "how": {
        "what": "WGSL struct members, std140/std430-style alignment (vec3 aligns to 16 bytes; struct size rounded to largest member alignment); little-endian; PIXEL ledger, image y-down",
        "transform": "GPU preprocess path defines the struct layout crossing JS->WebGPU (exact field order/padding UNVERIFIED)",
        "lossy": false
      },
      "why": {
        "text": "SHARED-WGSL HAZARD: one shader consumed by multiple bind layouts, so a struct-member change ripples across every bind group using it. This is why a WGSL struct edit is an enumerated LAW-7 boundary.",
        "cites": [
          {
            "path": "src/engine/contracts/binary_layouts.ts",
            "lines": "233-234"
          }
        ]
      },
      "review_note": "binary_layouts.ts:227-243 std140/std430 alignment, demosaic_pipeline.ts named, SHARED-WGSL HAZARD:233-235. Type contract apt for struct layout. Confirmed."
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/AtmosphericPhysics.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:667",
      "how": {
        "what": "detectRayleighGradient -> {top, bottom} (mean luminance of the top/bottom 5% of rows, unitless [0,1]); analyzeMieScattering -> a per-star scalar mie_index (netOuter/netCore halo flux ratio)",
        "transform": "signal_processor writes s.mie_index and s.rayleigh_index = (top+bottom)/2 on each clean star and returns background_level_top/bottom on the packet. REPORTED ONLY — no detection threshold reads these (honest-or-absent; the packet omits any scattering_type attribution)",
        "lossy": true
      },
      "verification": "VERIFIED",
      "why": {
        "text": "The header is explicit that the top-vs-bottom slope is an UNATTRIBUTED brightness gradient — light pollution, an altitude gradient, or vignetting produce the identical slope — so it must not be presented as measured scattering, and it never adjusts a threshold.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/AtmosphericPhysics.ts",
            "lines": "11-35"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "664-671"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/CelestialStructures.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:388",
      "how": {
        "what": "traceMilkyWayCenterline(points, horizon) -> mwCenterline; generateMilkyWayEllipses(points, horizon) -> mwEllipses (Milky-Way structure products traced against a horizon)",
        "transform": "signal_processor traces once against a height*0.8 placeholder horizon (before terrain is measured), then RE-traces against the measured silhouette only when DETECT_MW_REAL_HORIZON is on and terrain evidence exists (default-OFF => placeholder trace, byte-identical); results ride out as packet.milky_way_centerline / _ellipses",
        "lossy": false
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/TerrestrialEnvironment.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:547",
      "how": {
        "what": "BackgroundSurfaceModeler: fitSurface(buffer, w, h, horizon, vanguardCandidates) fits a deg-2 sky surface; evaluate(x,y) returns the modeled background at a pixel; coeffs() returns {a,b,c,d,e,f}",
        "transform": "signal_processor fits the surface (always, line 481) but only the DEEP pass consumes it via subtractBackgroundSurface when DETECT_APPLY_BG_SURFACE is on (default-OFF); flag off => evaluate has no consumer and the fit result is unused, byte-identical",
        "lossy": false
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/detection_cuts.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:1024",
      "how": {
        "what": "per-blob BlobShapeStats {sharpness = peak/flux, momentFwhmPx = 2.355*sqrt(sigMaj*sigMin) from 11x11-stamp 2nd moments on the detection grid (px), momentEllipticity = 1 - sigMin/sigMaj}, then cullThermalBlobs returns the surviving blob array",
        "transform": "computeBlobShapeStats measures each centroid TS-side and the fields are attached to every SignalPoint at unpack time; cullThermalBlobs drops FWHM_FLOOR/SHARPNESS/ELLIPTICITY blobs, firing onCut at ASSIGNMENT time so signal_processor's tally() stays measured. Inert thresholds (DETECT_FWHM_FLOOR_PX<=0 etc.) => pass-through, byte-identical",
        "lossy": true
      },
      "verification": "VERIFIED",
      "why": {
        "text": "The cuts run at BOTH the vanguard and deep stride-10 unpack lanes (signal_processor.ts:440 and :559) so no blob escapes measurement; the WASM fwhm field clamps near ~1.5px for tiny blobs, so these moment statistics are the only honest discriminator between a sub-pixel thermal spike and a real undersampled PSF.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "436-444"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "559-563"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/detection_cuts.ts",
      "to": "src/engine/pipeline/m4_signal_detect/source_extractor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/source_extractor.ts:214",
      "how": {
        "what": "BlobShapeStats per DetectedStar + cullThermalBlobs survivor list",
        "transform": "same measurement + per-blob cut applied in the source_extractor lane (auto path + solver re-extraction); each cut increments a local cutTally (measured, logged). Inert thresholds => pass-through, byte-identical",
        "lossy": true
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/detection_flatten.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:412",
      "how": {
        "what": "a flattened DETECTION-luminance COPY (new Float32Array, length w*h) from applyVignetteGainToLum (x (1 + a2 r^2 + a4 r^4)) and/or subtractBackgroundSurface (deg-2 model), plus VignetteFrameFit {a2, a4, fitRms, cornerCenterRatioBefore/After, atGridBound, cells}",
        "transform": "DETECT_APPLY_VIGNETTE_GAIN and DETECT_APPLY_BG_SURFACE (both default-OFF) correct the detection copy only; signal_processor recomputes mean/sigma on the flattened buffer before thresholding. The science buffer lum stays NATIVE. Flags OFF => detectFlat === detectLum and stats are reused => byte-identical",
        "lossy": true
      },
      "verification": "VERIFIED",
      "why": {
        "text": "Owner two-plane law: the render layer applies its OWN vignette flatten on a separate copy — same physics, independent application, not doubling. fitVignetteFromDetectionLum returns null when the frame is too sparse to fit honestly, so the correction is skipped rather than fabricated (honest-or-absent).",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "399-420"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "537-551"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/detection_guard.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "control",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:573",
      "how": {
        "what": "DetectionFastFailDiagnostic | null from evaluateDetectionDensity(deepCount, vanguardCount, width, height)",
        "transform": "trips only when deepCount >= DETECT_MIN_CANDIDATES_FOR_GUARD AND deepDensityPerMP >= DETECT_MAX_CANDIDATE_DENSITY_PER_MP; a non-null result (default DETECT_DENSITY_GUARD_MODE=0) makes signal_processor THROW with the structured diagnostic attached, BEFORE the O(deep x vanguard) dedup pass",
        "lossy": false
      },
      "verification": "VERIFIED",
      "why": {
        "text": "Converts the ~470s Carina-class silent hang into a loud + fast structured failure. The double predicate (absolute floor AND per-MP density) means Carina 60Da (9,613 deep/MP over 4.49MP) trips while any smaller or normal frame cannot.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "566-605"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/detection_guard.ts",
            "lines": "45-70"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/detection_guard.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "fallback",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:583",
      "how": {
        "what": "applyDensityCap(deepCandidates, width, height) -> {kept: top-N-by-flux, dropped, n, m}, where N = densityCapCount (the count that sits exactly at the density-guard boundary)",
        "transform": "alternate arm when DETECT_DENSITY_GUARD_MODE=1: instead of throwing, keep the brightest N deep candidates and stamp the faint remainder culling_reason='HIGH_DENSITY' (tally()'d), bounding the downstream O(deep x vanguard) work to N. Reads the SAME calibrated density constant the guard trips on; never moves it",
        "lossy": true
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "control",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:460",
      "how": {
        "what": "HorizonEnvelope.hasTerrainEvidence boolean (gate: coverage >= 0.4 AND areaBelow > 3% of frame AND densityBelow < 0.25*densityAbove)",
        "transform": "false (full-sky/tracked frame, no externally supplied horizonVector) => effectiveHorizonVector stays undefined, the background modeler samples the full frame, and every TOPOGRAPHY cull is SKIPPED — byte-identical to today's full-sky behavior; true => horizon derived and topography culling enabled",
        "lossy": false
      },
      "verification": "VERIFIED",
      "why": {
        "text": "The three gate thresholds are engineering judgment (heuristic, owner-design commit f17fa7e) with no measured FP/FN rate yet — the m4_horizon_envelope tests pin behavior snapshots, not a derivation. They move only via the flagged graduation-harness path, so the gate is deliberately conservative: absent evidence, detection behaves exactly as the full-sky path.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts",
            "lines": "127-165"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "446-473"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/horizon_envelope.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:459",
      "how": {
        "what": "HorizonEnvelope {points: [{x, y, measured}] (bins=96, y in image px, y-down = larger-is-lower), coverage, hasTerrainEvidence} from computeHorizonEnvelope(vanguardCandidates, width, height)",
        "transform": "when terrain evidence is present signal_processor.envelopeToHorizonVector rasterizes the supported nodes into a per-pixel Uint16Array horizonVector (terrain y per column x, linear-interpolated, clamped [0, height-1]); consumed by the foreground-shielding TOPOGRAPHY cull and lifted to native for the step-3 overlay",
        "lossy": true
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/hot_pixel_map.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:347",
      "verification": "VERIFIED",
      "how": {
        "what": "HotPixelResult.data — a detection-support luminance buffer (Float32, length w*h, native detection grid) plus flagged count and method ('MASTER_DARK'|'STATISTICAL'|'NONE')",
        "transform": "removeThermalArtifacts subtracts a geometry-matching master dark (preferred) or replaces statistical single-pixel spikes with their 8-neighbour median into a COPY; signal_processor extracts blobs on hotPix.data while the science buffer lum is untouched. Copy-on-flag: zero flags OR sub-density-gate => data === input lum (byte-identical)",
        "lossy": true
      },
      "why": {
        "text": "Two-ledger law: the masked buffer is detection-support only; photometry, Mie/Rayleigh, the background model and every receipt measurement keep reading the native lum. A real star elevates its 8-neighbour ring through the PSF so it is kept; a hot pixel does not and is replaced before it can seed a blob or drag a centroid.",
        "cites": [
          {
            "path": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
            "lines": "341-357"
          },
          {
            "path": "src/engine/pipeline/m4_signal_detect/hot_pixel_map.ts",
            "lines": "79-122"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/hot_pixel_map.ts",
      "to": "src/engine/pipeline/m4_signal_detect/source_extractor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/source_extractor.ts:130",
      "how": {
        "what": "HotPixelResult.data detection-support copy (Float32, length w*h)",
        "transform": "detectSources calls removeThermalArtifacts(lum,...,null) — masterDark is always null in this lane (no live producer), so only the statistical map can run; extraction reads hotPix.data (detLum). Copy-on-flag => byte-identical when spike density is below the gate",
        "lossy": true
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m4_signal_detect/source_extractor.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m4_signal_detect/signal_processor.ts:376",
      "how": {
        "what": "diffuse Milky-Way sample points [{x, y, brilliance}] from SourceExtractor.detectMilkyWay (64px grid scan, 5x5 average > bg + 3.5*sigma)",
        "transform": "signal_processor sorts them by brilliance, caps at 50, and feeds them to CelestialStructures.traceMilkyWayCenterline / generateMilkyWayEllipses; results ride out as packet.milky_way / _centerline / _ellipses",
        "lossy": true
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/confirm_status.ts",
      "to": "src/engine/pipeline/stages/package.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/package.ts:1355",
      "verification": "VERIFIED",
      "how": {
        "what": "classifyConfirmStatus(sol.deep_confirmed, PIPELINE_CONSTANTS.SOLVER_CONFIRM_SET_EXCESS_Z) -> ConfirmStatusBlock { status, setExcessZ, nTargets, confirmed, setGateZ, fdr_shadow, verdict_driver }",
        "transform": "package.ts derives the single four-state verdict over the already-computed deep_confirmed block for the receipt's confirm_status field; the fdr_set_shadow carrier is stripped from the deep_confirmed dump so it surfaces ONCE here",
        "lossy": false
      },
      "why": {
        "text": "The receipt/UI read the packaged confirm_status rather than re-deriving the verdict, so a REFUSED or too-few-targets solve cannot masquerade as a plain confident 'solved'. The set-gate Z is cited in from pipeline_config, so package/confirm_status own no calibrated constant.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "1330-1356"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
      "to": "src/engine/pipeline/m4_signal_detect/detection_cuts.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts:237",
      "verification": "VERIFIED",
      "how": {
        "what": "computeBlobShapeStats(L, w, h, m.x, m.y, m.bg, peakAbove, m.flux) -> { momentFwhmPx, momentEllipticity, sharpness }",
        "transform": "the C2 shape-consistency test measures the candidate stamp's moments on the native grid and compares momentFwhm to the frame PSF within tolerance, plus ellipticity/sharpness ceilings, to reject hot pixels (sharpness->1) and cosmics (high ellipticity)",
        "lossy": false
      },
      "why": {
        "text": "Shape consistency reuses the detection lane's moment stats rather than a private copy; when the frame PSF is absent/undersampled or SNR is below the stable-moment floor the test honestly returns NOT_MEASURED and the burden shifts to the local null + set gate.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
            "lines": "215-247"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
      "to": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
      "type": "contract",
      "cite": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts:57",
      "verification": "VERIFIED",
      "how": {
        "what": "reuses forcedMeasure, scrambledPositions (local-annulus mode), binomialExcessZ, ForcedMeasurement, ForcedPosition from deep_verify — unchanged",
        "transform": "the set-level family-wise gate computes its excess with the SAME binomialExcessZ the +10 sigma-class escalation uses, and draws its per-star + frame-wide nulls with the SAME deterministic sampler, so the two confirmation statistics cannot fork",
        "lossy": false
      },
      "why": {
        "text": "Deliberate single-source contract: no numeric primitive is re-implemented in forced_confirm. If binomialExcessZ or the scrambled sampler changed, both the escalation and the confirmation gate must move together — that shared-math coupling is the point of importing rather than copying.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
            "lines": "51-60"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
            "lines": "404-410"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
      "to": "src/engine/pipeline/m6_plate_solve/fdr_confirm.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts:488",
      "verification": "VERIFIED",
      "how": {
        "what": "candidate + scrambled-null SNR pools (plain numbers) -> computeFdrSetShadow (ALWAYS) and computeFdrShadow (only when input.fdrShadow)",
        "transform": "the set-level FDR shadow is computed unconditionally from the SAME empirical null the family-wise gate draws and rides on fdr_set_shadow; the phase-1 N-invariant block attaches only under the flag. Neither moves the live setExcessZ gate fields (byte-identical when off)",
        "lossy": false
      },
      "why": {
        "text": "The swap-ready FDR verdict is banked alongside the shipped gate so confirm_status can later drive on it (it does, since the 2026-07-15 flip). Computing it from the already-drawn null keeps it a bounded O(N+M) push of numbers with no extra photometry.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
            "lines": "430-507"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "to": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
      "type": "control",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:1514",
      "verification": "VERIFIED",
      "how": {
        "what": "deepVerifyEscalation(...).excessZ (binomial excess of predicted-position acceptances over the scrambled on-frame null)",
        "transform": "if excessZ >= SOLVER_UW_ESCALATE_MIN_EXCESS_Z the sub-threshold anchor-sweep candidate WCS is handed to the normal verifyWCS chain; a pass returns success (status SUCCESS_UW_ESCALATED), a below-gate excess collapses the candidate as junk",
        "lossy": false
      },
      "why": {
        "text": "The ultra-wide anchored sweep can peak below the wide-net sigma bar for a TRUE center; catalog-forced photometry at that center is the extra evidence that rescues it. The escalation number is a real accept gate here (not diagnostic-only), producing a locked solution when it and verifyWCS both pass.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "1483-1526"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "to": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:2786",
      "verification": "VERIFIED",
      "how": {
        "what": "post-solve harvest: catalog rows (ra_hours HOURS, dec_degrees) + solution.wcs -> projectCatalogToPixels -> in-frame ForcedPosition[] (native pixels), then runForcedPhotometry over the luminance grid -> CATALOG_FORCED ForcedMeasurement[]",
        "transform": "gnomonic TAN about crval[0] (HOURS) -> linear CD-inverse -> pixel, capped to SOLVER_DEEP_HARVEST_MAX_POSITIONS; matched-aperture flux/SNR at fixed positions. CAVEAT: projection is LINEAR CD only, never the fitted SIP polynomial (deep_verify.ts:223-235), so off-axis positions on a SIP-carrying frame land at the wrong pixels (standing P-item)",
        "lossy": true
      },
      "why": {
        "text": "runPostSolveDeepHarvest densifies the solve with forced photometry at catalog-predicted positions; an active lens prior re-projects those positions through the Brown-Conrady toNative model so apertures hit native pixels. All measurements are tagged CATALOG_FORCED and never laundered into blind discoveries.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "2786-2810"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/deep_verify.ts",
            "lines": "223-249"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "to": "src/engine/pipeline/m6_plate_solve/forced_confirm.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:2987",
      "verification": "VERIFIED",
      "how": {
        "what": "accepted CATALOG_FORCED candidates (ForcedMeasurement[]) + in-frame catalog positions + native Float32 luminance + rApPx/sigmaPix/fwhmPx + FramePsfRef + config overrides (SOLVER_CONFIRM_SET_EXCESS_Z, SOLVER_CONFIRM_SNR_FLOOR, ...) -> confirmForcedSet -> ForcedConfirmSetResult",
        "transform": "runPostSolveConfirmation runs the per-star conjunction + set-level family-wise excess gate; result becomes solution.deep_confirmed (confirmed count zeroed when the gate collapses)",
        "lossy": false
      },
      "why": {
        "text": "This is where the ~2 sigma candidate pool is promoted (or refused) under the set-level gate. The live gate constant SOLVER_CONFIRM_SET_EXCESS_Z is injected here from pipeline_config, decoupled from the module default; the FDR shadow flag is threaded through the same call.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "2987-3009"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "to": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:1138",
      "verification": "VERIFIED",
      "how": {
        "what": "StarCatalogAdapter.getinstance().findStarsInField(ra0, dec0, fetchRadiusDeg, targetJd) -> StandardStar[]; ra_hours in internal HOURS, dec_degrees, magnitude_V, color_index_BV",
        "transform": "cone query over the loaded sectors; the adapter has already normalized the Gaia-degrees/HYG-hours hybrid to HOURS, so the solver consumes uniform-HOURS rows (no per-row unit branch on this side)",
        "lossy": false
      },
      "why": {
        "text": "solvePlate seeds the blind/quad matcher from this catalog cone; loadCatalog + ensureSectorLoaded must have run first (solver_entry.ts:256, and the anchor/escalation paths page their own sectors). This is the sole reference-star source the quad match and verify chain compare against.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "256"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "1138"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "to": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:2358",
      "verification": "VERIFIED",
      "how": {
        "what": "candidate wcs + full detection list + frame dims + safeScale + wide-net params -> runUwTightReverifyDeep (variant A) -> UwDeepEvidenceResult | null",
        "transform": "the async wrapper pages a sampled atlas pattern (center + four corners) for a DEEP catalog the >16deg blind path otherwise skips, deep-wide-matches the full detection set, and runs the pure tight-reverify core on that evidence",
        "lossy": false
      },
      "why": {
        "text": "Round-1 measured the tight tier STARVES on the wide-net bright subset (~10-16 pairs) because ultra-wide fields skip deep-sector paging; variant A repairs the evidence source without touching the pure tier math. Gated by SOLVER_UW_TIGHT_DEEP.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "2354-2375"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "to": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
      "type": "fallback",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:2377",
      "verification": "VERIFIED",
      "how": {
        "what": "on a wide-net FAIL (uwSigma < UW_MIN_SIGMA || uwUnique < UW_MIN_UNIQUE) and shouldAttemptTightReverify, runUwTightReverify(wcs, catalogStars, detected, wideMatches, config) -> UwTightReverifyResult",
        "transform": "fit Brown-Conrady on provisional matches then re-verify through a tight net; SOLVER_UW_FIT_REVERIFY (default ON) runs it as a verdict-NEUTRAL diagnostic, SOLVER_UW_TIGHT_ACCEPT (default OFF) is what lets an accepted result adopt the tight matches as the solution",
        "lossy": false
      },
      "why": {
        "text": "This is the degradation arm for ultra-wide frames that fail the wide gate; it is fail-soft (any error -> declined, never breaks a solve) and pin-safe (SeeStar is narrow, CR2 passes the wide gate, so neither sacred enters this branch regardless of either flag).",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "2340-2403"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
      "to": "src/engine/pipeline/m6_plate_solve/tps_eval.ts",
      "type": "contract",
      "cite": "src/engine/pipeline/m6_plate_solve/tps_eval.ts:5",
      "verification": "VERIFIED",
      "how": {
        "what": "tpsKernel + evalTpsField (pure, Math-only) reused by the fitter and re-exported as evalField",
        "transform": "tps_fitter solves the regularized TPS linear system then evaluates the fitted field through the SHARED evaluator; export/asdf_writer bakes its GWCS tabular lookup from the same tps_eval, so the two byte paths cannot diverge",
        "lossy": false
      },
      "why": {
        "text": "One implementation, no code in two places (LAW 4). Housing the evaluator in an import-free leaf is specifically what keeps wasm off the ASDF serializer's chain while guaranteeing the fitter and the serializer evaluate the spline identically.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_eval.ts",
            "lines": "1-11"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
      "to": "src/engine/pipeline/m7_astrometry/residual_analyzer.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts:22",
      "verification": "VERIFIED",
      "how": {
        "what": "matched-star residual pairs via ResidualAnalyzer.skyToLinearPixel: per pair u = detected.x - crpix[0], v = detected.y - crpix[1]; dx = detected.x - expX, dy = detected.y - expY (linear-WCS projection of the catalog star)",
        "transform": "the spline is fit to the SAME detected-minus-linear-prediction displacement the SIP fitter models; planet sentinels (residual_arcsec >= 999) are filtered so they cannot inject huge outliers",
        "lossy": false
      },
      "why": {
        "text": "TPS and SIP model the identical quantity and must share the projection convention; sourcing it from ResidualAnalyzer.skyToLinearPixel (single source of truth) is what keeps the two distortion models consistent instead of re-deriving the linear pixel projection.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
            "lines": "21-30"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts",
      "to": "src/engine/pipeline/m6_plate_solve/star_catalog_adapter.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts:88",
      "verification": "VERIFIED",
      "how": {
        "what": "ensureSectorLoaded(ra, dec, pageDeg) over center+four corners, then findStarsInField(crval[0], crval[1], halfDiagDeg, CATALOG_EPOCH_JD) -> deep StandardStar[] (ra_hours HOURS), brightest-N capped",
        "transform": "pages a SAMPLED atlas pattern across the candidate footprint (each disk <= SECTOR_LOAD_MAX_RADIUS_DEG) and pools the deep rows the >16deg blind path skipped; the atlas singleton caches sectors so repeated fires re-page cheaply",
        "lossy": false
      },
      "why": {
        "text": "Fail-soft coordinate-space wrapper: any paging/fetch error returns null and the caller declines. It only repairs the EVIDENCE SOURCE for the tight tier; the science buffer is never touched here.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts",
            "lines": "64-94"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts",
      "to": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts:102",
      "verification": "VERIFIED",
      "how": {
        "what": "deepCat (StandardStar[]) + deepWideMatch(...) provisional MatchedStar[] -> runUwTightReverify(wcs, catalogStars=deepCat, wideMatches=deepMatches, config) -> UwTightReverifyResult",
        "transform": "wide-matches the FULL detection list against the deep catalog for >=100 fit pairs, then hands the deep catalog + deep matches to the PURE tight-reverify core (the module keeps no atlas/IO of its own)",
        "lossy": false
      },
      "why": {
        "text": "Separation of concerns: uw_deep_evidence owns the async atlas paging + deep wide-match, uw_tight_reverify stays a pure coordinate-space function over whatever catalog/matches it is given, so the tier math is testable without atlas or wasm.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_deep_evidence.ts",
            "lines": "96-107"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
      "to": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts:382",
      "verification": "VERIFIED",
      "how": {
        "what": "makeBrownConradyDistortion(fit.k1, fit.k2 ?? 0, imageW, imageH) -> LensDistortionModel with toNative(x,y,out)",
        "transform": "the fitted model is applied via model.toNative in tightMatchPass to bend the linear-WCS-projected catalog positions into native pixels before the tight-net match, so the tight sigma is measured against distortion-corrected predictions",
        "lossy": false
      },
      "why": {
        "text": "makeBrownConradyDistortion is the shared forward-distortion constructor; using it (rather than a private polynomial) keeps the tight tier's reprojection identical to the prior/rematch lanes and to the inner-frac arithmetic in the module header.",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
            "lines": "381-387"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
      "to": "src/engine/pipeline/m2_hardware/lens_distortion_refit.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts:349",
      "verification": "VERIFIED",
      "how": {
        "what": "DistortionPair[] (normalized xn,yn,dx,dy per provisional match) -> fitBrownConrady(pairs, [cx,cy], hd) -> { k1, k2, n_used, not_measured }",
        "transform": "fits radial Brown-Conrady k1/k2 on the wide-match residual field in normalized coords; a not_measured or non-finite k1 marks fitBad and routes to the inner-region pinhole fallback",
        "lossy": false
      },
      "why": {
        "text": "The tight tier needs a distortion model before it can re-verify at fit precision on a wide field; fitBrownConrady is the shared refit primitive (the same lane the lens-distortion rematch pass uses).",
        "cites": [
          {
            "path": "src/engine/pipeline/m6_plate_solve/uw_tight_reverify.ts",
            "lines": "347-351"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/m7_astrometry/residual_analyzer.ts",
      "to": "src/engine/pipeline/export/sip_convention.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/export/sip_convention.ts:11",
      "verification": "VERIFIED",
      "how": {
        "what": "internal SIP coefficients A_internal(u,v)=dx=OBSERVED-IDEAL (performSIPFit; u=detected.x-CRPIX), stored on receipt.solution.astrometry.sip.a/.b",
        "transform": "engine fit -> receipt -> the export-boundary negation bridge",
        "lossy": false
      },
      "review_note": "sip_convention.ts:11-25 documents residual_analyzer.performSIPFit convention (1) A_internal=OBSERVED-IDEAL stored on receipt.solution.astrometry.sip. Confirmed."
    },
    {
      "from": "src/engine/pipeline/orchestrator_session.ts",
      "to": "src/engine/pipeline/stages/ingest.ts",
      "type": "contract",
      "cite": "src/engine/pipeline/orchestrator_session.ts:880",
      "verification": "VERIFIED",
      "how": {
        "what": "out of ingest: DecodedScienceFrame {rawSensor, fullRGB (Float32 w*h*3), width, height, rgbBuffer?}",
        "transform": "the session invokes decodeScienceFrame in step2 (extract.decode), retains fullRGB as this.scienceRgb for SPCC, and NULLS the raw ArrayBuffer after step2 — which is why step-1 re-entry is forbidden by design (buffer-ownership contract)",
        "lossy": false
      },
      "why": {
        "text": "ingest BORROWS rawBuffer and retains no reference; the caller (session) owns the release and nulls it after ingest. That ownership contract is what makes step-1 re-entry forbidden — the raw buffer is gone once extract runs.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "8-16"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/seam_capture.ts",
      "to": "boundary:seam_capsule",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:457",
      "verification": "VERIFIED",
      "how": {
        "what": "per-buffer float32|uint16|uint8 raw LE .bin (flat row-major, luminance w*h / interleaved RGB w*h*3, NO header) + JSON sidecar (capsule.json, SOLE layout authority); sha256-verified on load; y-down native grid",
        "transform": "captureSeam (env-gated CAPTURE_SEAMS=1, default OFF) snapshots buffers synchronously at the seam, sidecar stable-stringify-sorted + tmp->rename",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:459-472 per-buffer LE .bin + capsule.json sidecar (sole authority), sha256-verified, captureSeam env-gated CAPTURE_SEAMS=1 default OFF. Confirmed."
    },
    {
      "from": "src/engine/pipeline/stages/calibrate.ts",
      "to": "src/engine/pipeline/m2_hardware/hardware_profiler.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/calibrate.ts:177",
      "verification": "VERIFIED",
      "how": {
        "what": "in: solution + HardMetadata + StarMeasurement[] (per matched star: x/y, flux, fwhm, flux_r/g/b from detected.peak_rgb, measured_bv, catalog bv) + SignalPacket; out: HardwareProfile",
        "transform": "generateHardwareProfile -> HardwareProfiler.generateReport spectral forensics; buildStarMeasurements drops detections lacking a per-channel peak sample (flux_r undefined filter)",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/calibrate.ts",
      "to": "src/engine/pipeline/m6_plate_solve/tps_fitter.ts",
      "type": "control",
      "cite": "src/engine/pipeline/stages/calibrate.ts:113",
      "verification": "VERIFIED",
      "how": {
        "what": "in: solution (fire gate = distortion_pattern_detected AND countRefinablePairs > 20); out: {tps, gate}",
        "transform": "fitTpsGated runs the same fire gate as SIP then an out-of-sample CV gate; solution.astrometry.tps = the model when admitted else null (explicit), and tps_gate is ALWAYS recorded as the honest why",
        "lossy": false
      },
      "why": {
        "text": "An interpolating TPS spline whose in-sample rms (~3″) does not generalize (measured out-of-sample ~35″) is REFUSED (tps:null) rather than shipped. TPS is a post-solve COORDINATE observation that never touches the SIP block or any asserted solve field, so the pinned SeeStar/CR2 numbers stay byte-identical.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "100-121"
          },
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "134-140"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/calibrate.ts",
      "to": "src/engine/pipeline/m7_astrometry/residual_analyzer.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/calibrate.ts:66",
      "verification": "VERIFIED",
      "how": {
        "what": "in: PlateSolution; out: ResidualAnalysis {rms_arcsec, distortion_pattern_detected, sip_coefficients} landed on solution.astrometry (MUTATES solution — that IS the landing)",
        "transform": "ResidualAnalyzer.analyze fits residual RMS + a global order-3 SIP; non-fatal (a failure warns and returns null)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/detect.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "control",
      "cite": "src/engine/pipeline/stages/detect.ts:152",
      "verification": "VERIFIED",
      "how": {
        "what": "isNativeBayer(rawSensor) predicate (not demosaiced AND data instanceof Uint16Array)",
        "transform": "true -> analyzeBayerNative reads rawSensor.data (Uint16, 2x2 binned), bypassing luminance; false -> analyzeWithMasking reads the luminance Float32 scienceBuffer — selects WHICH upstream buffer detection consumes",
        "lossy": false
      },
      "why": {
        "text": "The branch predicate is exported so the wizard's status strings use the same condition as the dispatch. It changes which buffer detection reads (native Uint16 vs luminance Float32), not merely a data value — a wrong branch would detect on the wrong buffer type.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "99-104"
          },
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "152-176"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/detect.ts",
      "to": "src/engine/pipeline/m4_signal_detect/signal_processor.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/detect.ts:143",
      "verification": "VERIFIED",
      "how": {
        "what": "in: science buffers (native Bayer Uint16 w*h, or luminance Float32 w*h) + focalLength + metadata (frameCtx carried on a copy); out: SignalPacket {clean_stars: SignalPoint[]; anomalies; culling_tally}",
        "transform": "detectSignal -> SignalProcessor.analyzeBayerNative / analyzeWithMasking: 3-pass masking + morphological extraction + foreground culling",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/ingest.ts",
      "to": "src/engine/core/ImageProcessor.ts",
      "type": "control",
      "cite": "src/engine/pipeline/stages/ingest.ts:247",
      "verification": "VERIFIED",
      "how": {
        "what": "predicate metadata_reaper.isRawFile(rawBuffer) selecting the decode path",
        "transform": "isRaw=false (JPEG/TIFF) routes to ImageProcessor.decodeFullResImage (browser full-res decoder) instead of extractRawSensorData; isRaw=true stays on the RAW/FITS arm",
        "lossy": false
      },
      "why": {
        "text": "decodeScienceFrame forks its whole decode arm on isRawFile: a non-RAW input must use the browser decoder (already-rendered pixels), while RAW/FITS goes through the sensor decode. Sending a JPEG down the RAW arm would fail to decode; sending a RAW down the browser arm would lose the linear sensor data SPCC/photometry need.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "224-248"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/ingest.ts",
      "to": "src/engine/pipeline/m1_ingestion/metadata_reaper.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/ingest.ts:243",
      "verification": "VERIFIED",
      "how": {
        "what": "DecodedSensorPayload {data: Uint16Array Bayer CFA (w*h at `stride`) | Float32Array interleaved RGB (w*h*3); width; height; stride; isDemosaiced; cfaMosaicLuma; bayerPattern; blackLevel; whiteLevel; rawler cal record; nanMask 1-bit/pixel}",
        "transform": "extractRawSensorData decodes the RAW bitstream (rawler default arm / libraw cold path per VITE_DECODER_RAWLER), BayerStorageService cache-backed; ingest is the sole producer of rawSensor",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/ingest.ts",
      "to": "src/engine/pipeline/m3_gpu_preprocess/demosaic_pipeline.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/ingest.ts:321",
      "verification": "VERIFIED",
      "how": {
        "what": "in: rawSensor.data (Uint16 Bayer CFA, w*h at `stride`) + DemosaicParams {cfaOffsetX/Y from bayerPattern, blackLevel/whiteLevel from the FITS header, wbR/G/B = 1/1/1 neutral}; out: interleaved linear RGB Float32 (w*h*3)",
        "transform": "demosaicWebGPU; params come from buildDemosaicParamsForPayload (real header levels, NEVER the Canon 14-bit 2048/16383/RGGB default; WB honest-neutral on CFA FITS)",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/metrology.ts",
      "to": "src/engine/core/optics_manager.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/metrology.ts:119",
      "verification": "VERIFIED",
      "how": {
        "what": "in: HardMetadata; out: {value_mm (focal length, mm), hint (labelled assumption or null)}",
        "transform": "OpticsManager.resolveFocalLengthWithHint seeds the Tri-Lock FL byte-identically while capturing any labelled wide-field assumption for the receipt; owns the 'Unknown Lens' truthy-placeholder handling",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/metrology.ts",
      "to": "src/engine/pipeline/m2_hardware/optics_resolver.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/metrology.ts:90",
      "verification": "VERIFIED",
      "how": {
        "what": "in: HardMetadata (EXIF focal length + camera identity); out: {pixel_scale (arcsec/px), pixel_pitch_um (um), hint (labelled FL assumption), userSupplied}",
        "transform": "resolveOpticsFromExif: EXIF FL (through OpticsManager.getEffectiveFocalLength, which owns the 'Unknown Lens' truthy-trap) x SENSOR_DB most-specific pitch; result mutated back onto metadata.pixel_scale/pixel_pitch_um (trust-ladder rung 2)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/metrology.ts",
      "to": "src/engine/pipeline/m7_astrometry/metrology.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/metrology.ts:120",
      "verification": "VERIFIED",
      "how": {
        "what": "in: vanguard = cleanStars.slice(0,10) (brightest SignalPoint[]) + focal length fl.value_mm (mm) + pixel_pitch_um (um); out: scaleLock (arcsec/px)",
        "transform": "MetrologyService.solveScale — the blind Tri-Lock last-resort rung; combines FL(mm) x sensor pitch(um) geometry with the vanguard star field to fit arcsec/px",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/package.ts",
      "to": "boundary:fits_io",
      "type": "unit-conversion",
      "cite": "src/engine/pipeline/stages/package.ts:161",
      "verification": "VERIFIED",
      "how": {
        "what": "in: engine solution crval[0]/ra_hours in HOURS; out: receipt.wcs CRVAL1 in DEGREES",
        "transform": "CRVAL1 = fitted.crval[0] * 15 (fitted branch, :161) and sol.ra_hours * 15 (synthesized branch, :185) — receipt WCS is ALREADY degrees downstream",
        "lossy": false
      },
      "review_note": "package.ts:161 CRVAL1=fitted.crval[0]*15 (fitted branch), :185 sol.ra_hours*15 (synthesized). Both branches confirmed; receipt is already-degrees downstream."
    },
    {
      "from": "src/engine/pipeline/stages/package.ts",
      "to": "src/engine/contracts/binary_layouts.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/package.ts:586",
      "verification": "VERIFIED",
      "how": {
        "what": "in: BINARY_LAYOUTS active-atlas boundary goldenVector {md5, manifestPath}; out: pipeline_provenance.atlas_id + reproducibility atlas identity (id/versionSource/manifestPath)",
        "transform": "activeAtlasGolden selects atlas_gaia_cells (default since the 2026-07-15 cutover) vs atlas_rows (legacy cold path) by the VITE_ATLAS_GAIA state and reads goldenVector.md5 — the LAW-7 build-time manifest fingerprint, never a runtime hash",
        "lossy": false
      },
      "why": {
        "text": "The receipt reads its atlas content identity from the SINGLE engine-side source of truth (the LAW-7 binary-layout contract for the ACTIVE atlas boundary). Reading the wrong boundary is exactly the wrong-atlas misidentification the external review flagged — a Gaia-default run must not fingerprint itself as the legacy catalog.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "574-621"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/package.ts",
      "to": "src/engine/pipeline/stages/schema_versions.ts",
      "type": "contract",
      "cite": "src/engine/pipeline/stages/package.ts:61",
      "verification": "VERIFIED",
      "why": {
        "text": "buildReceipt imports RECEIPT_SCHEMA_VERSION from schema_versions.ts and emits it as the receipt `version` field, never hand-pinning a number — schema_versions.ts is the single source of truth for the Toolchest API contract version. If package.ts pinned its own literal, the emitted version could drift from the documented schema lineage.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/package.ts",
            "lines": "8-11"
          },
          {
            "path": "src/engine/pipeline/stages/schema_versions.ts",
            "lines": "9-14"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/psf_characterize.ts",
      "to": "src/engine/pipeline/m10_psf/psf_field.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/psf_characterize.ts:70",
      "verification": "VERIFIED",
      "how": {
        "what": "in: {lum: Float32 luminance (native w*h OR 2x2-binned floor(w/2)*floor(h/2)); width bw; height bh; stars: solved matched-star (x,y) positions on that grid, planet sentinels excluded; vignette | null}; out: PsfFieldReport {fwhmMedianMaj/MinPx, ellipticityMedian, orientationMedianDeg, grid: SCIENCE_NATIVE | SCIENCE_BINNED2X, regions}",
        "transform": "characterizePsfField fits a per-star 2D Gaussian (Levenberg-Marquardt) on the buffer's OWN grid — native or binned, never resampled before measurement (PIXEL ledger)",
        "lossy": false
      },
      "why": {
        "text": "The native-vs-2x2-binned grid disambiguation is load-bearing: stamps are measured on the buffer's actual grid and the report is tagged with which one, so downstream arcsec<->px conversions (psf_attribution) use the right scale. Incoherent dims return null (honest-absent), never a fabricated map.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_characterize.ts",
            "lines": "43-77"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/psf_characterize.ts",
      "to": "src/engine/pipeline/m10_psf/vignette_map.ts",
      "type": "control",
      "cite": "src/engine/pipeline/stages/psf_characterize.ts:66",
      "verification": "VERIFIED",
      "how": {
        "what": "flag PIPELINE_CONSTANTS.PSF_FLUX_VIGNETTE_CORRECT gating the vignette input",
        "transform": "flag ON -> fitVignetteLuma(lum, bw, bh) supplies an achromatic (magnitude) correction map; default OFF -> null -> characterizePsfField records no corrected amplitude (byte-identical)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/science.ts",
      "to": "src/engine/pipeline/m10_psf/psf_physics.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/science.ts:72",
      "verification": "VERIFIED",
      "how": {
        "what": "in: field-center ra_hours (HOURS) + dec_degrees (DEGREES) + gps_lat/lon + observation instant; out: airmass (sec z, dimensionless) or null",
        "transform": "deriveFieldCenterAirmass reuses the canonical alt-az path (targetGeometry -> altitudeDeg, airmassFromAltitude) LAW-4 verbatim; honest-or-absent — null when either trust gate fails or altitude <= ~3 deg (plane-parallel sec z diverges)",
        "lossy": false
      },
      "why": {
        "text": "SPCC's field-center airmass must equal psf_attribution's for the same field center + instant, so the astronomy math is reused (LAW 4), not re-derived, with identical rounding. A blind DSLR with no trusted clock/GPS yields null (never the legacy fabricated 1.0).",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "39-77"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/science.ts",
      "to": "src/engine/pipeline/m10_psf/vignette_map.ts",
      "type": "control",
      "cite": "src/engine/pipeline/stages/science.ts:378",
      "verification": "VERIFIED",
      "how": {
        "what": "flag PIPELINE_CONSTANTS.PSF_FLUX_VIGNETTE_CORRECT gating the per-band vignette input to the SPCC flux extraction",
        "transform": "flag ON -> fitVignettePerBand(scienceRgb.data, w, h) yields per-channel r/g/b (a2,a4) maps that feed the flux divide; default OFF -> null -> extraction byte-identical (both extra args inert)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/science.ts",
      "to": "src/engine/pipeline/m8_photometry/spcc_calibrator.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/science.ts:382",
      "verification": "VERIFIED",
      "how": {
        "what": "in: matchedStars + scienceRgb (full-res linear RGB Float32, w*h*3, + optional nanMask) + scales (null in the wizard native path, ScaleManager in the auto preview path) + exposureForFit + FrameContext ctx + vignette + airMass; out: SpccCalibration {zpFit, colorFit, stars[], gains, bandPartition}",
        "transform": "computeSpccCalibration measures per-channel aperture photometry and fits the color transform + instrumental-mag zero-point (GaiaG-only band partition; Johnson-V refused, never pooled)",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/solve.ts",
      "to": "src/engine/events/pipeline_events.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/solve.ts:217",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_started / stage_finished envelopes, one per ATTEMPTED solve branch (solve.quad_wasm | solve.uw_sweep | solve.uw_escalation), each with wall-ms + counts {attempts, matched}",
        "transform": "emitSolveBranch reads result.diagnostics.forensics + branch_timing (pure READ, solver internals untouched); winner painted PASS, attempted-but-lost FAIL with real accrued ms, never-attempted emits nothing; wrapped in try/catch so instrumentation never breaks the solve",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/solve.ts",
      "to": "src/engine/pipeline/m6_plate_solve/hint_resolver.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/solve.ts:97",
      "verification": "VERIFIED",
      "how": {
        "what": "in: configHints {ra_hours: HOURS, dec_degrees: DEGREES} (from CallerTargetHint, catalog/hint convention) + HardMetadata; out: {ra_hours: HOURS, dec_degrees: DEGREES, radius_deg, source}",
        "transform": "resolveEffectiveHints ladder CONFIG -> FITS_HEADER -> ZENITH -> BLIND; the azimuth-mode sentinel (ra === -1) is guarded OUT so it is never forwarded as a literal RA; no rung firing preserves the blind {radius_deg:180} verbatim",
        "lossy": false
      },
      "why": {
        "text": "RA is in HOURS and Dec in DEGREES here (the catalog/hint convention), and the ra===-1 azimuth sentinel is NOT a sky coordinate — forwarding it as a literal RA of -1h would poison the search center. This is the loud unit/convention boundary between the upload surface and the solver's search prior.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve.ts",
            "lines": "53-60"
          },
          {
            "path": "src/engine/pipeline/stages/solve.ts",
            "lines": "86-95"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/pipeline/stages/solve.ts",
      "to": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/solve.ts:241",
      "verification": "VERIFIED",
      "how": {
        "what": "in: ImageData (solve buffer) + SolveContext + existingStars (curated DetectedStar[], the landmine #3 forwarding parameter); out: SolveResult {success, solution {wcs, matched_stars, ra_hours, ...}, diagnostics}",
        "transform": "autoSolvePlate smart facade collapses WASM quad matcher / ultra-wide anchored sweep / deep-verify escalation into one call; absent existingStars means the solver re-extracts",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/ForensicCalibrationStep.tsx",
      "to": "src/engine/ui/calibration/CalibrationCharts.tsx",
      "type": "data-flow",
      "cite": "src/engine/ui/ForensicCalibrationStep.tsx:227",
      "verification": "VERIFIED",
      "how": {
        "what": "measured profile coefficients -> chart components: {k1,k2,k3, rRefPx: fit.r_ref_px} -> DistortionChart; vignette_v1 -> VignetteChart; QuiverModel + pixel_scale -> ResidualQuiver",
        "transform": "charts render only when distortionMeasured (fit.n_matches >= 10 && r_ref_px > 0); honest zeros are labeled as no-measurement, and the quiver needs >= 15 arrows",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/ForensicCalibrationStep.tsx",
      "to": "src/engine/ui/calibration/quiver_model.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/ForensicCalibrationStep.tsx:93",
      "verification": "VERIFIED",
      "how": {
        "what": "the live PlateSolution -> buildQuiverModel(solution) -> QuiverModel | null",
        "transform": "built LAZILY inside a useMemo only when charts are enabled AND an M7 astrometry analysis exists (~one projection per matched star); null (too few arrows / no analysis) renders an honest empty state, never a fabricated field",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/ForensicCalibrationStep.tsx",
      "to": "src/engine/ui/diag_prefs.ts",
      "type": "control",
      "cite": "src/engine/ui/ForensicCalibrationStep.tsx:48",
      "verification": "EXTRACTED",
      "how": {
        "what": "diagnosticsVisualsEnabled(autoRun) || chartsRequested -> chartsEnabled",
        "transform": "gates whether the quiver model + distortion/vignette geometry are computed at all; AUTO runs render text stats only unless the persisted full-diagnostics opt-in (or a this-mount request) turns visuals on",
        "lossy": false
      },
      "why": {
        "text": "Chart/quiver geometry is expensive and is a MANUAL-flow default; AUTO runs skip it for speed unless the user opted into full diagnostics. This gate is the owner performance directive expressed at the step-6 boundary — geometry is always lazy (render-time), never in the pipeline hot path.",
        "cites": [
          {
            "path": "src/engine/ui/ForensicCalibrationStep.tsx",
            "lines": "42-96"
          }
        ]
      }
    },
    {
      "from": "src/engine/ui/PipelineWizard.tsx",
      "to": "src/engine/hooks/usePipelineEvents.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/PipelineWizard.tsx:178",
      "verification": "VERIFIED",
      "how": {
        "what": "session.events (the pipeline event bus) -> a batched readonly PipelineEvent[] snapshot",
        "transform": "one subscription powers the header event count, the Back guard (no navigation mid-stage via inspectorModel.stageRunning), the inspector drawer, and the corrected-view recompute trigger",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/PipelineWizard.tsx",
      "to": "src/engine/hooks/usePipelineFSM.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/PipelineWizard.tsx:162",
      "verification": "VERIFIED",
      "how": {
        "what": "(file: File, hint?: {ra,dec,label}) -> {step, session: OrchestratorSession, metadata, isInitializing, nextStep/prevStep/setStep, updateMetadata}",
        "transform": "the FSM hook creates the session once per file and owns the step state machine + metadata edits; PipelineWizard renders from and drives it",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/PipelineWizard.tsx",
      "to": "src/engine/pipeline/m2_hardware/lens_profiles.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/PipelineWizard.tsx:323",
      "verification": "VERIFIED",
      "how": {
        "what": "a lens model string -> findLensByModel(model) -> {model, focal_lengths[0]}, written to metadata as lens_model + focal_length + optics_source:'USER'",
        "transform": "a resolved lens seeds the first focal length; an unresolved free string records lens_model with no fabricated focal length (honest-or-absent). Focal length seeds only the solver's scale prior, never the fitted result",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/PipelineWizard.tsx",
      "to": "src/engine/pipeline/m2_hardware/sensor_db.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/PipelineWizard.tsx:308",
      "verification": "VERIFIED",
      "how": {
        "what": "a camera-BODY string from cameraDropdownOptions() -> findSensorByCamera(bodyId) -> {pixel_size_um}, written to metadata as pixel_pitch_um + optics_source:'USER'",
        "transform": "the BODY is stored (never the chip sensor_model) because the solve re-resolves optics via resolveOpticsFromExif -> findSensorByCamera(camera_model), which is BODY-keyed; an unresolved / Unknown selection seeds NO fabricated pitch (honest-or-absent)",
        "lossy": false
      },
      "why": {
        "text": "Storing the sensor_model instead of the body would make the solve's BODY-keyed re-resolve MISS (the old 3/15 seam noted in-file at C9). The body resolves tier-1 exact; an Unknown/Manual pick records the selection honestly and seeds no fake pixel pitch (LAW 3).",
        "cites": [
          {
            "path": "src/engine/ui/PipelineWizard.tsx",
            "lines": "299-320"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/PipelineWizard.tsx",
      "to": "src/engine/pipeline/orchestrator_session.ts",
      "type": "control",
      "cite": "src/engine/ui/PipelineWizard.tsx:249",
      "verification": "VERIFIED",
      "how": {
        "what": "the corrected-view toggle + session.getCorrectedViewInfo() availability gate session.renderCorrectedPreviewUrl()",
        "transform": "OFF (or no fitted distortion available) -> correctedUrl stays null, the SIP warp is never allocated and the preview renders byte-identically; ON + available -> the render-plane de-distorted preview URL is computed. RENDER PLANE ONLY — never touches the solve/WCS/matched stars",
        "lossy": false
      },
      "why": {
        "text": "The corrected view is a render-plane display option that consumes the coordinate + pixel ledgers and feeds neither (LAW 1). The OFF path allocates no warp so the preview is byte-identical, and getCorrectedViewInfo() reports NOT AVAILABLE honestly when no fitted SIP exists — the pill is only shown post-solve where the preview canvas lives.",
        "cites": [
          {
            "path": "src/engine/ui/PipelineWizard.tsx",
            "lines": "226-257"
          },
          {
            "path": "src/engine/ui/PipelineWizard.tsx",
            "lines": "717-728"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/calibration/CalibrationCharts.tsx",
      "to": "src/engine/ui/calibration/chart_math.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/calibration/CalibrationCharts.tsx:156",
      "verification": "VERIFIED",
      "how": {
        "what": "distortion: (r in [0,1], k1,k2,k3, rRefPx) -> distortionShiftPx sample curve; vignette: (r, v1) -> vignetteGainAt; axes via niceTicks; labels via fmtCoef",
        "transform": "the chart components sample the pure math models at render time to build the SVG paths and tick labels — all presentation geometry over MEASURED coefficients, nothing fabricated",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/calibration/quiver_model.ts",
      "to": "src/engine/pipeline/m7_astrometry/residual_analyzer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/calibration/quiver_model.ts:67",
      "verification": "VERIFIED",
      "how": {
        "what": "per matched star: catalog (ra, dec) [sky, RA hours] + the solution WCS {crpix, cd, crval} -> ResidualAnalyzer.skyToLinearPixel -> projected pixel (x,y) [solve-buffer px]; arrow dx/dy = detected − projected (px)",
        "transform": "the SAME linear-WCS projection convention the M7 fit used, so the quiver residual matches the fitted residual; this is the sky->pixel frame crossing at the heart of the residual field",
        "lossy": false
      },
      "why": {
        "text": "The quiver must project through the identical convention M7 used or the displayed residual arrows disagree with the fitted residuals. skyToLinearPixel takes catalog sky coordinates (RA in HOURS internally) and the linear WCS and returns solve-buffer pixels; the arrow is detected − projected. Using any other projection would show a coherent bias that isn't in the fit.",
        "cites": [
          {
            "path": "src/engine/ui/calibration/quiver_model.ts",
            "lines": "54-76"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/calibration/quiver_model.ts",
      "to": "src/engine/ui/calibration/chart_math.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/calibration/quiver_model.ts:47",
      "verification": "VERIFIED",
      "how": {
        "what": "(medianResidualPx, targetPx = 0.03 * frame diagonal) -> quiverMagnification -> a labeled {1,2,5}x10^n scale factor clamped to [1,500]",
        "transform": "finalizeQuiverModel derives the honest arrow magnification so the median residual arrow is ~targetPx long on the plot; the factor is shown in the legend so arrows are never silently exaggerated",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/dashboard/replay/replay_state.ts",
      "to": "src/engine/events/capture_record.ts",
      "type": "contract",
      "cite": "src/engine/ui/dashboard/replay/replay_state.ts:151",
      "verification": "VERIFIED",
      "how": {
        "what": "CaptureEnvelope[] (per-stage capture record: stage_id, seq, t_start/t_end, ms, ok, verdict, counts, warnings) -> ReplayFrame (per-stage phase + tallies + record slice at scrub time t)",
        "transform": "deriveReplayFrame maps each envelope to a phase at t (pending/active/complete) and reveals verdict/ms/ok/counts ONLY once complete; a not-yet-reached stage is pending with a null verdict (honest-or-absent), never an interpolated timing",
        "lossy": false
      },
      "why": {
        "text": "replay_state depends on the CaptureEnvelope layout produced by the events layer and transforms it into the scrub-time contract the time-slider and replay widgets consume. It fabricates nothing between events: durations and counts come straight from the emitter, and phases are the only thing that move with the scrub.",
        "cites": [
          {
            "path": "src/engine/ui/dashboard/replay/replay_state.ts",
            "lines": "16-25"
          },
          {
            "path": "src/engine/ui/dashboard/replay/replay_state.ts",
            "lines": "105-133"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_asdf.ts",
      "to": "src/engine/pipeline/export/asdf_writer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/utils/save_asdf.ts:20",
      "verification": "VERIFIED",
      "how": {
        "what": "(receipt, image: AsdfImage, {libraryVersion}) -> a fresh exact-sized Uint8Array of ASDF (science frame + GWCS) bytes",
        "transform": "serializeAsdf builds the ASDF file; save_asdf wraps the returned buffer in a Blob and triggers the browser download (thin sink, byte production entirely in the shared serializer)",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_export.ts",
      "to": "src/engine/pipeline/export/asdf_writer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/utils/save_export.ts:182",
      "verification": "VERIFIED",
      "how": {
        "what": "(receipt, image, {libraryVersion: pkg.version}) -> ASDF Uint8Array via serializeAsdf",
        "transform": "the dispatcher's 'asdf' case; throws NO_FRAME when image is absent before calling the serializer",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_export.ts",
      "to": "src/engine/pipeline/export/fits_writer.ts",
      "type": "control",
      "cite": "src/engine/ui/utils/save_export.ts:60",
      "verification": "VERIFIED",
      "how": {
        "what": "hasFittedWcs(receipt) = (receipt.wcs.SOURCE === 'FITTED') gates the FITS/ASDF availability rows",
        "transform": "exportAvailability surfaces the FIRST unmet precondition (no run -> no fitted WCS -> no frame) as a human disabled reason; a SYNTHESIZED (approximate) WCS is never offered as a science export",
        "lossy": false
      },
      "why": {
        "text": "This is the UI-side half of the fitted-WCS-only export law (LAW 3, honest-or-absent): the availability matrix disables FITS/ASDF with a human reason when no star-verified WCS exists, so a user is never offered a science export the serializer would refuse. The serializer's own throw is the second, independent guard.",
        "cites": [
          {
            "path": "src/engine/ui/utils/save_export.ts",
            "lines": "58-100"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_export.ts",
      "to": "src/engine/pipeline/export/fits_writer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/utils/save_export.ts:176",
      "verification": "VERIFIED",
      "how": {
        "what": "(receipt, image, {libraryVersion: pkg.version}) -> FITS Uint8Array via serializeFits",
        "transform": "the dispatcher's 'fits' case; throws NO_FRAME when image is absent before calling the serializer",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_export.ts",
      "to": "src/engine/pipeline/stages/receipt_serializer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/utils/save_export.ts:170",
      "verification": "VERIFIED",
      "how": {
        "what": "receipt (result model) -> JSON receipt string via serializeReceipt",
        "transform": "the unified dispatcher's 'receipt' case; the string is then written through the runtime-appropriate sink (Tauri fs.writeFile or browser download)",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_export.ts",
      "to": "src/engine/ui/dashboard/solve_queue/connectors.ts",
      "type": "control",
      "cite": "src/engine/ui/utils/save_export.ts:152",
      "verification": "VERIFIED",
      "how": {
        "what": "isTauriRuntime() selects the write sink for every format",
        "transform": "TRUE -> tauriSave (native save-dialog + fs.writeFile via lazily-imported tauri plugins so the browser bundle never loads them); FALSE -> browserDownload (Blob + anchor click). Same bytes either way",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_fits.ts",
      "to": "src/engine/pipeline/export/fits_writer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/utils/save_fits.ts:20",
      "verification": "VERIFIED",
      "how": {
        "what": "(receipt, image: FitsImage, {libraryVersion}) -> a fresh exact-sized Uint8Array of big-endian FITS bytes",
        "transform": "serializeFits builds the FITS file; save_fits wraps the returned buffer in a Blob and triggers the browser download. Throws (no download) when the receipt has no FITTED WCS — the export law is enforced inside the serializer",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "src/engine/ui/utils/save_packet.ts",
      "to": "src/engine/pipeline/stages/receipt_serializer.ts",
      "type": "data-flow",
      "cite": "src/engine/ui/utils/save_packet.ts:26",
      "verification": "VERIFIED",
      "how": {
        "what": "the AstroPacket result model (opaque object with heavy Float32Array buffers)",
        "transform": "serializeReceipt applies the typed-array-stripping replacer + stable ordering, producing the downloadable JSON receipt string; save_packet only does the Blob/anchor/download mechanics",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "stage:bc_measure",
      "to": "stage:bc_rematch",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1788",
      "verification": "VERIFIED",
      "how": {
        "what": "this.bcMeasured — the MEASURED per-capture Brown-Conrady fit (k1/k2 + n_used/n_pairs coverage + mustache verdict) from measureBrownConradyFromSolution (:1746); pure observation, mutates nothing (receipt-write lens_distortion_measured)",
        "transform": "runBcRematchPass consumes bcMeasured; when the fit passed its coverage gates it BC-corrects the FULL detection set's matching coords (COORDINATE ledger), re-matches against the catalog to recover edge stars, re-runs SIP refinement on the densified set, culls under the SAME acceptance the originals cleared, and forced-photometry existence-checks the survivors",
        "lossy": false
      },
      "why": {
        "text": "This is the two-pass rail's measure->apply seam: bc_measure is always-on and side-effect-free precisely so the solve stays byte-identical when the rematch declines; bc_rematch is the ONLY post-solve writer of matched_stars/astrometry/num_stars/matchEpoch (orchestrator_session.ts:1774-1775).",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1731-1741"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1763-1778"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1746 measureBrownConradyFromSolution producer, :1786-1788 runBcRematchPass({bcMeasured: this.bcMeasured}). Confirmed measure->apply seam."
    },
    {
      "from": "stage:bc_measure",
      "to": "stage:bc_rematch",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_cr2.mjs:178-181",
      "verification": "VERIFIED",
      "how": {
        "what": "bc.matched_before===56 / matched_after===56 / rms_before_arcsec===630.8214 / rms_after_arcsec===616.8437 pinned exactly — bc_rematch's before-value is bc_measure's initial measured RMS",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "run_wizard_cr2.mjs:178-181 pin bc.matched_before===56 / matched_after===56 / rms_before===630.8214 / rms_after===616.8437 exactly. Confirmed. (NB: CLAUDE.md's 537.8008 rms_before is stale vs the live 630.8214; out of scope.)"
    },
    {
      "from": "stage:bc_measure",
      "to": "stage:bc_rematch",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:bc_rematch",
      "to": "stage:calibrate.hardware_profile",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:bc_rematch",
      "to": "stage:solve",
      "type": "fallback",
      "cite": "src/engine/pipeline/m2_hardware/lens_distortion_rematch_pass.ts:336",
      "verification": "VERIFIED",
      "how": {
        "what": "guard = (neverWorseVerdict === 'APPLIED' && assign.false_guard_passes) ? 'APPLIED' : 'KEPT_ORIGINAL'",
        "transform": "NEVER-WORSE structural guard (not a calibrated threshold): the densified candidate replaces the solve's solution ONLY with strictly more matches AND no worse post-chain RMS; KEPT_ORIGINAL => applied=false, the ORIGINAL solve-owned matched_stars/astrometry/stamps survive byte-identical (matchEpoch bump lives in the APPLIED branch ONLY, :352)",
        "lossy": false
      },
      "why": {
        "text": "This guard is LOAD-BEARING for the pinned regressions: well-corrected narrow FITS recover nothing and KEEP, so the sacred SeeStar solve stays bit-identical with the rematch rail default-ON. Downstream consumers never depend on rematch SUCCESS — a throw degrades to bcRematch=null and they run on the original set (orchestrator_session.ts:1776-1778).",
        "cites": [
          {
            "path": "src/engine/pipeline/m2_hardware/lens_distortion_rematch_pass.ts",
            "lines": "330-337"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1770-1773"
          }
        ]
      },
      "review_note": "lens_distortion_rematch_pass.ts:336-337 guard=(neverWorse==='APPLIED' && false_guard_passes)?'APPLIED':'KEPT_ORIGINAL'; matchEpoch bump only in APPLIED branch (:354, comment cited :352). KEPT_ORIGINAL keeps original byte-identical. Confirmed."
    },
    {
      "from": "stage:calibrate.hardware_profile",
      "to": "stage:render_apply_sip",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:calibrate.m7_analyze",
      "to": "stage:calibrate.sip_gate",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/calibrate.ts:91",
      "verification": "VERIFIED",
      "how": {
        "what": "analysis.sip_coefficients (the candidate order-3 SIP, internal convention OBSERVED-IDEAL)",
        "transform": "gateEmittedSip(solution, analysis.sip_coefficients) — the candidate goes to the out-of-sample CV gate; gate only runs when a candidate exists",
        "lossy": false
      },
      "review_note": "calibrate.ts:88 if(analysis.sip_coefficients), :91 gateEmittedSip(solution, analysis.sip_coefficients). Gate runs only when candidate exists. Confirmed."
    },
    {
      "from": "stage:calibrate.m7_analyze",
      "to": "stage:calibrate.sip_gate",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:calibrate.sip_gate",
      "to": "stage:bc_rematch",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_cr2.mjs:177",
      "verification": "VERIFIED",
      "how": {
        "what": "assert(bc.false_guard_passes === false, '... wrong-sign control is the holding prong post-sip-gate') — bc_rematch's wrong-sign holding behavior is explicitly gated on the sip_gate verdict (OOS_WORSE_THAN_LINEAR per docs/GATES.md:44)",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "run_wizard_cr2.mjs:177 assert(bc.false_guard_passes===false, 'wrong-sign control is the holding prong post-sip-gate'). Confirmed."
    },
    {
      "from": "stage:calibrate.sip_gate",
      "to": "stage:calibrate.tps_gate",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:calibrate.sip_gate",
      "to": "stage:m7_refine",
      "type": "control",
      "cite": "src/engine/pipeline/stages/calibrate.ts:93",
      "verification": "VERIFIED",
      "how": {
        "what": "sipGate.admitted boolean + always-recorded sip_gate verdict",
        "transform": "ADMITTED => solution.astrometry.sip = analysis.sip_coefficients (m7_refine's receipt-write 'astrometry' carries the SIP); REFUSED => sip stays ABSENT, sip_gate verdict records why (honest-or-absent). Gate = k-fold out-of-sample CV: SIP emitted ONLY when it generalizes better than the no-correction linear baseline AND better than a plain affine plane out of sample — both COMPARISONS, no tuned constant",
        "lossy": false
      },
      "why": {
        "text": "A low-order global polynomial fit from a sparse, center-weighted matched set EXTRAPOLATES catastrophically at the frame corners — measured 34x worse than astrometry.net on the pinned CR2 frame (OURS 3449 arcsec vs 102 arcsec vs Gaia, test_results/sip_comparison_2026-07-15). Without this gate that SIP would ride the receipt into every export/render consumer.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "71-87"
          }
        ]
      },
      "review_note": "calibrate.ts:93 if(sipGate.admitted) solution.astrometry.sip=...; :94 sip_gate verdict always recorded. Control (emit-or-absent). Confirmed."
    },
    {
      "from": "stage:calibrate.tps_gate",
      "to": "stage:m7_refine",
      "type": "control",
      "cite": "src/engine/pipeline/stages/calibrate.ts:114",
      "verification": "VERIFIED",
      "how": {
        "what": "fitTpsGated(solution) => { tps, gate }; fire-gate SAME as SIP (rms > 1.2 arcsec distortion flag AND >20 refinable pairs, calibrate.ts:111)",
        "transform": "solution.astrometry.tps = model when admitted, else null (explicit); tps_gate verdict ALWAYS recorded. Gate = out-of-sample CV: an interpolating spline whose in-sample rms_after (~3 arcsec) does NOT generalize (measured OOS ~35 arcsec on SeeStar/M66) is REFUSED",
        "lossy": false
      },
      "why": {
        "text": "TPS is an interpolator — in-sample residuals are nearly meaningless as evidence. The OOS CV refusal is what keeps a spline that memorized the matched set out of the ASDF/GWCS tabular carry. Post-solve COORDINATE observation: the SIP block and every asserted SOLVE field are untouched, so pinned SeeStar/CR2 numbers stay byte-identical.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "100-117"
          }
        ]
      },
      "review_note": "calibrate.ts:111 fire-gate (distortion && >20 pairs), :114 fitTpsGated(solution), :116 tps=model|null explicit, :117 verdict always recorded. Confirmed."
    },
    {
      "from": "stage:calibrate.tps_gate",
      "to": "stage:m7_refine",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:calibrate.tps_gate",
      "to": "stage:solve",
      "type": "evidence",
      "cite": "tools/api/solve_seestar.apispec.ts:131-133",
      "verification": "VERIFIED",
      "how": {
        "what": "expect(receipt.solution.astrometry.tps).toBeNull() + expect(receipt.solution.astrometry.tps_gate?.admitted).toBe(false) — the tps_gate's out-of-sample refusal verdict is written directly into the solve stage's astrometry block",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_seestar.apispec.ts:132-133 expect(receipt.solution.astrometry.tps).toBeNull() + tps_gate?.admitted).toBe(false). Confirmed OOS refusal written to solve astrometry block."
    },
    {
      "from": "stage:extract",
      "to": "stage:calibrate.hardware_profile",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1831-1834",
      "verification": "VERIFIED",
      "how": {
        "what": "SignalPacket (this.signal — detection statistics: star/anomaly counts, culling tallies)",
        "transform": "passed unchanged as generateHardwareProfile's `signal` argument",
        "lossy": false
      },
      "why": {
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "172-183"
          }
        ],
        "text": "generateHardwareProfile forwards `signal` straight into HardwareProfiler.generateReport."
      },
      "review_note": "orchestrator_session.ts:1831-1834 generateHardwareProfile(this.solution!, this.metadata!, this.signal!); calibrate.ts:177-182 forwards `signal` straight into HardwareProfiler.generateReport. Confirmed."
    },
    {
      "from": "stage:extract",
      "to": "stage:forced_confirm",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2068",
      "verification": "VERIFIED",
      "how": {
        "what": "FrameContext (sensor gain_e_adu / read-noise profile, possibly extended with master-calibration deposits during extract)",
        "transform": "feeds makeFrameProduct so forced-photometry SNR uses the CCD equation in electrons (source Poisson + read noise) rather than a raw-count heuristic",
        "lossy": false
      },
      "why": {
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1008-1023"
          }
        ],
        "text": "this.frameCtx (originating at load, line 652) can be extended during extract before forced_confirm's makeFrameProduct(sb, bw, bh, this.frameCtx) call reads it."
      },
      "review_note": "orchestrator_session.ts:2068 makeFrameProduct(sb, bw, bh, this.frameCtx) — FrameContext gain/RN feeds CCD-equation SNR. Confirmed."
    },
    {
      "from": "stage:extract",
      "to": "stage:metrology",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_seestar.mjs:79",
      "verification": "VERIFIED",
      "how": {
        "what": "assertRange(snap?.scaleLock, 3.70, 3.78) — the metrology stage's scale-lock output is asserted right after the extract/signal step",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "run_wizard_seestar.mjs:79 assertRange(snap?.scaleLock, 3.70, 3.78) at the scale-lock step — metrology's scaleLock output is asserted. Confirmed."
    },
    {
      "from": "stage:extract",
      "to": "stage:metrology",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:extract",
      "to": "stage:psf_attribution",
      "type": "control",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2022",
      "verification": "VERIFIED",
      "how": {
        "what": "this.timestampTrusted boolean (derived during step2_Extract)",
        "transform": "gates whether the differential-refraction/seeing attribution tier can be CALCULATED vs stays NOT_MEASURED",
        "lossy": false
      },
      "why": {
        "text": "psf_attribution's refraction block is explicitly `gatedOn: 'timestampTrusted && GPS present'` — a false timestampTrusted (unset-clock forensics from step2) forces that tier NOT_MEASURED regardless of other inputs.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "953-976"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "296"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:2022 timestampTrusted into runPsfAttribution; psf_attribution.ts:296 refraction gatedOn 'timestampTrusted && GPS present' => NOT_MEASURED when false. Confirmed control gate."
    },
    {
      "from": "stage:extract.decode",
      "to": "stage:extract.detect",
      "type": "control",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1130-1148",
      "verification": "VERIFIED",
      "why": {
        "text": "isNativeBayer(rawSensor) — a predicate on decode's rawSensor payload (isDemosaiced flag + whether .data is a Uint16Array) — decides which branch detectSignal takes: the native-Bayer branch reads rawSensor.data directly (bypassing luminance entirely), the luminance branch reads the extract.luminance scienceBuffer. This changes which upstream buffer detection actually consumes, not just a data value.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "100-104"
          },
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "143-177"
          }
        ]
      },
      "review_note": "detect.ts:100-104 isNativeBayer(!isDemosaiced && data instanceof Uint16Array); detect.ts:152-176 native branch reads rawSensor.data (analyzeBayerNative), luminance branch reads scienceBuffer (analyzeWithMasking). Genuine buffer-selecting branch. Confirmed."
    },
    {
      "from": "stage:extract.decode",
      "to": "stage:extract.detect",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_seestar.mjs:64",
      "verification": "VERIFIED",
      "how": {
        "what": "assert((count >= 50) || (snap?.signalStars >= 50)) — detection count asserted immediately after the FITS decode step completes",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "run_wizard_seestar.mjs:64 assert((count>=50)||(snap?.signalStars>=50)) — detection produced >=50 stars from the decoded frame. Confirmed (assertion is at the detection step, not literally 'immediately after decode' — framing slightly loose but the quoted text and edge are correct)."
    },
    {
      "from": "stage:extract.decode",
      "to": "stage:extract.luminance",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:995-998",
      "verification": "VERIFIED",
      "how": {
        "what": "fullRGB (interleaved linear Float32, w*h*3) + rawSensor.cfaMosaicLuma boolean",
        "transform": "computeluminance calls reduceToLuminance(rgb, weights, lum) to project RGB -> single-channel Float32 luminance (w*h); cfaMosaicLuma selects LUMA_EQUAL vs LUMA_REC709 weights when the default-OFF CFA_LUMA_PARITY_FIX flag is on",
        "lossy": true
      },
      "why": {
        "text": "computeluminance's two parameters (rgb, cfaMosaicLuma) are exactly decode's fullRGB and rawSensor.cfaMosaicLuma output fields — extract.luminance has no other producer.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "2507-2522"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:995-998 computeluminance(fullRGB, rawSensor?.cfaMosaicLuma) — decode's fullRGB + cfaMosaicLuma are exactly computeluminance's two inputs; extract.luminance has no other producer. Confirmed."
    },
    {
      "from": "stage:extract.decode",
      "to": "stage:extract.luminance",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:extract.decode",
      "to": "stage:extract.spectral_peek",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1171-1174",
      "verification": "VERIFIED",
      "how": {
        "what": "fullRGB (interleaved linear Float32, w*h*3)",
        "transform": "peekSpectralData nearest-pixel-samples fullRGB at each detected star's rounded (x,y) to fill peak_rgb + measured_bv",
        "lossy": true
      },
      "why": {
        "text": "peekSpectralData's `rgb` parameter is the same fullRGB reference produced by extract.decode; it is read here for the last time before the buffer is discarded.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1652-1672"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1171-1173 peekSpectralData(fullRGB, this.signal!) — fullRGB from decode. Confirmed (last read before buffer discard :1177)."
    },
    {
      "from": "stage:extract.decode",
      "to": "stage:spcc",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:923-932",
      "verification": "VERIFIED",
      "how": {
        "what": "full-res linear RGB Float32 (w*h*3), retained as this.scienceRgb for FITS and raw-DSLR (rawSensorDecode) inputs only",
        "transform": "direct reference retained (no resample, no discard) across step2/step5; passed as runSpcc's `scienceRgb` aperture-photometry input",
        "lossy": false
      },
      "why": {
        "text": "this.scienceRgb is set once, immediately after decode, straight from decode's `fullRGB` output; SPCC is its sole consumer (released to null right after SPCC reads it).",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1907-1909"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1949-1953"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1909 this.scienceRgb passed to runSpcc, :1953 released to null right after SPCC; science.ts:319 useSpcc = !!scienceRgb && matches. SPCC sole consumer. Confirmed (set-site :923 not directly opened, corroborated)."
    },
    {
      "from": "stage:extract.detect",
      "to": "boundary:toolchest_arrow_export",
      "type": "evidence",
      "cite": "tools/api/arrow_sink.apispec.ts:114",
      "verification": "VERIFIED",
      "how": {
        "what": "expect(detections.numRows).toBe((receipt.signal?.clean_stars ?? []).length) — extract.detect's clean-star detection set row count is carried exactly into detections.arrow",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "arrow_sink.apispec.ts:114 expect(detections.numRows).toBe((receipt.signal?.clean_stars ?? []).length). Confirmed exact."
    },
    {
      "from": "stage:extract.detect",
      "to": "stage:extract",
      "type": "control",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1158",
      "verification": "VERIFIED",
      "why": {
        "text": "`if (!this.signal) throw new Error('Signal analysis failed.')` — a null/failed detection result aborts the entire step2_Extract (stage:extract) call, not just the detect substage.",
        "cites": []
      },
      "review_note": "orchestrator_session.ts:1158 if(!this.signal) throw 'Signal analysis failed.' — aborts step2_Extract. Confirmed."
    },
    {
      "from": "stage:extract.detect",
      "to": "stage:extract.spectral_peek",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1171-1174",
      "verification": "VERIFIED",
      "how": {
        "what": "SignalPacket.clean_stars + .anomalies (SignalPoint[] arrays)",
        "transform": "spectral_peek iterates both arrays and mutates each SignalPoint IN PLACE, adding peak_rgb / measured_bv fields",
        "lossy": false
      },
      "why": {
        "text": "peekSpectralData's `packet` parameter is `this.signal` — extract.detect's own output object.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1652-1672"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1173 peekSpectralData(fullRGB, this.signal!) — this.signal from detect, mutated in place with peak_rgb. Confirmed."
    },
    {
      "from": "stage:extract.detect",
      "to": "stage:extract.spectral_peek",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:extract.detect",
      "to": "stage:metrology",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1233",
      "verification": "VERIFIED",
      "how": {
        "what": "signal.clean_stars (detected star list) from detectSignal",
        "transform": "metrology consumes clean_stars to solve the true pixel scale (resolveScaleLock)",
        "lossy": false
      },
      "review_note": "orchestrator_session.ts:1231-1236 resolveScaleLock(this.metadata, this.signal.clean_stars, ...). Confirmed."
    },
    {
      "from": "stage:extract.detect",
      "to": "stage:solve",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1452",
      "verification": "VERIFIED",
      "how": {
        "what": "signal.clean_stars -> curated star set",
        "transform": "selectCuratedStars(scienceBuffer, w, h, signal) feeds the blind/quad solver; solve guards 'Extract signal before Metrology'",
        "lossy": false
      },
      "review_note": "orchestrator_session.ts:1452 selectCuratedStars(this.scienceBuffer, w, h, this.signal) feeds solver. Confirmed."
    },
    {
      "from": "stage:extract.detect",
      "to": "stage:spcc",
      "type": "control",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1926",
      "verification": "VERIFIED",
      "how": {
        "what": "cfa_verdict classification ({klass, supported}) on this.signal",
        "transform": "gates SPCC's color-eligibility refusal (mono -> REFUSED_GRAY, unsupported CFA phase -> UNCALIBRATED_CFA) before any color fit is attempted",
        "lossy": false
      },
      "why": {
        "text": "A mono or CFA-phase-unsupported verdict makes runSpcc refuse to fit color entirely (INVALID_REFUSED), regardless of whether scienceRgb/matched-star inputs are otherwise present — a genuine run/no-run gate on the color fit, not just a data value feeding a computation.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "309-313"
          },
          {
            "path": "src/engine/pipeline/stages/science.ts",
            "lines": "349-359"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1926 this.signal?.cfa_verdict into runSpcc; science.ts:353-359 cfaVerdict mono=>REFUSED_GRAY, supported===false=>UNCALIBRATED_CFA before any fit. Genuine run/no-run gate. Confirmed."
    },
    {
      "from": "stage:extract.luminance",
      "to": "stage:extract.detect",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1106",
      "verification": "VERIFIED",
      "how": {
        "what": "luminance Float32 science buffer (w*h, native grid)",
        "transform": "assigned to detectScienceBuffer (identity copy-reference unless a pre-detection transform intervenes) and passed into detectSignal",
        "lossy": false
      },
      "why": {
        "text": "Only reached on the non-native-Bayer branch (see the sibling control edge from extract.decode) — when isNativeBayer is true this buffer is built but never read by detection.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1137-1148"
          },
          {
            "path": "src/engine/pipeline/stages/detect.ts",
            "lines": "152-177"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1139 scienceBuffer: detectScienceBuffer passed to detectSignal; only read on non-native branch. Confirmed (cite :1106 assignment not directly opened)."
    },
    {
      "from": "stage:extract.luminance",
      "to": "stage:extract.detect",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:extract.luminance",
      "to": "stage:forced_confirm",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2045-2070",
      "verification": "VERIFIED",
      "how": {
        "what": "science luminance Float32 buffer (native or 2x-binned), this.scienceBuffer",
        "transform": "measured directly on this buffer (native grid) as the forced-photometry probe input, via makeFrameProduct and runPostSolveConfirmation's `scienceBuffer` argument",
        "lossy": false
      },
      "why": {
        "text": "The whole forced_confirm block is gated on `if (this.scienceBuffer && this.solution)` — extract.luminance's buffer is a hard precondition for the stage to do any work.",
        "cites": []
      },
      "review_note": "orchestrator_session.ts:2045 gated on if(this.scienceBuffer && this.solution); :2068-2070 makeFrameProduct(sb,...) + runPostSolveConfirmation({scienceBuffer: sb}) native grid. Confirmed."
    },
    {
      "from": "stage:extract.luminance",
      "to": "stage:psf_field",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1990-1996",
      "verification": "VERIFIED",
      "how": {
        "what": "science luminance Float32 buffer (native or 2x-binned), this.scienceBuffer",
        "transform": "runPsfCharacterization measures PSF stamps directly on this buffer at solved star positions — native grid, no resample before measurement",
        "lossy": false
      },
      "why": {
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_characterize.ts",
            "lines": "28-53"
          }
        ],
        "text": "runPsfCharacterization's `scienceBuffer` input is this.scienceBuffer; it returns null honestly if that buffer is absent."
      },
      "review_note": "orchestrator_session.ts:1990-1991 runPsfCharacterization({scienceBuffer: this.scienceBuffer}) — measured on native grid. Confirmed."
    },
    {
      "from": "stage:extract.spectral_peek",
      "to": "stage:calibrate.hardware_profile",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1662-1665",
      "verification": "VERIFIED",
      "how": {
        "what": "per-star peak_rgb [r,g,b] + measured_bv, riding on the SignalPoint object that stage:solve later copies into solution.matched_stars[].detected",
        "transform": "buildStarMeasurements reads d.peak_rgb[0..2] / d.measured_bv off the matched detection to build the HardwareProfiler's spectral-forensics StarMeasurement[] input; stars with no peak_rgb sample are dropped (flux_r === undefined filter)",
        "lossy": true
      },
      "why": {
        "text": "This is the ONLY producer of the flux_r/flux_g/flux_b/measured_bv fields HardwareProfiler.generateReport's spectral forensics consumes. The data crosses through stage:solve's matching step (out of this agent's domain — the SignalPoint object is copied by reference into matched_stars[].detected there) but both edge endpoints (spectral_peek, hardware_profile) are in-domain, so the edge is emitted with this mediation noted rather than silently dropped.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "144-169"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1831-1834"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1662-1665 spectral_peek writes p.peak_rgb=[r,g,b] + p.measured_bv; calibrate.ts:159-168 buildStarMeasurements reads d.peak_rgb?.[0..2] (flux_r/g/b) + d.measured_bv and .filter(s=>s.flux_r!==undefined). Confirmed incl. the drop-on-no-sample."
    },
    {
      "from": "stage:extract.spectral_peek",
      "to": "stage:extract",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:forced_confirm",
      "to": "boundary:toolchest_arrow_export",
      "type": "evidence",
      "cite": "tools/api/arrow_sink.apispec.ts:113-116",
      "verification": "VERIFIED",
      "how": {
        "what": "forced.numRows asserted equal to receipt.deep_confirmed.confirmed_stars.length AND receipt.confirm_status.confirmed — forced_confirm's confirmed-star set is carried intact into forced_confirmed.arrow",
        "transform": "confirmed_stars array -> Arrow table rows",
        "lossy": false
      },
      "review_note": "arrow_sink.apispec.ts:115-116 forced.numRows toBe (receipt.deep_confirmed?.confirmed_stars ?? []).length AND toBe receipt.confirm_status.confirmed. Confirmed."
    },
    {
      "from": "stage:forced_confirm",
      "to": "stage:calibrate",
      "type": "evidence",
      "cite": "tools/api/solve_seestar.apispec.ts:109-128",
      "verification": "VERIFIED",
      "how": {
        "what": "confirm_status block (verdict_driver/status/setExcessZ/nTargets/confirmed/setGateZ) asserted byte-exact — the DERIVED classification of forced_confirm's deep_confirmed output surfaces on the receipt at stage:calibrate's fold-completion",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_seestar.apispec.ts:122-128 confirm_status verdict_driver/status/setExcessZ(243.21)/nTargets(211)/confirmed(211)/setGateZ(15) asserted byte-exact. Confirmed."
    },
    {
      "from": "stage:forced_confirm",
      "to": "stage:calibrate",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:load",
      "to": "stage:calibrate.hardware_profile",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1831-1834",
      "verification": "VERIFIED",
      "how": {
        "what": "HardMetadata (camera/lens/exposure EXIF record)",
        "transform": "passed unchanged as generateHardwareProfile's `metadata` argument",
        "lossy": false
      },
      "why": {
        "cites": [
          {
            "path": "src/engine/pipeline/stages/calibrate.ts",
            "lines": "172-183"
          }
        ],
        "text": "generateHardwareProfile forwards `metadata` straight into HardwareProfiler.generateReport."
      },
      "review_note": "orchestrator_session.ts:1831-1834 generateHardwareProfile(solution, this.metadata!, signal); calibrate.ts:177-182 forwards `metadata` straight into HardwareProfiler.generateReport. Confirmed."
    },
    {
      "from": "stage:load",
      "to": "stage:extract",
      "type": "evidence",
      "cite": "tools/api/solve_seestar.apispec.ts:42-45",
      "verification": "VERIFIED",
      "how": {
        "what": "headless smoke reads the raw FITS buffer (fs.readFileSync) and calls runWizardPipeline(ab) — the populated receipt asserted later in the same test proves load's raw buffer reached extract",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_seestar.apispec.ts:42-45 fs.readFileSync(FIT_PATH) + runWizardPipeline(ab); populated receipt asserted later proves raw buffer reached extract. Confirmed."
    },
    {
      "from": "stage:load",
      "to": "stage:extract.decode",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:641",
      "verification": "VERIFIED",
      "how": {
        "what": "sourceFormat string ('FITS'/'CR2'/...) set by detectMagicFormatSync, plus metadata.timestamp",
        "transform": "decodeScienceFrame branches its whole decode arm (FITS pure-TS decoder vs RAW rawler/libraw vs browser JPEG) on sourceFormat; the timestamp is only a decode-cache-key discriminant (bayerCacheKey), not a pixel transform",
        "lossy": false
      },
      "why": {
        "text": "step1 (load) is the sole producer of sourceFormat; step2's decode substage passes it straight into decodeScienceFrame's dispatch, so which decoder actually runs is decided entirely by load's output.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "880-885"
          },
          {
            "path": "src/engine/pipeline/stages/ingest.ts",
            "lines": "207-249"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1130-1148 detectSignal dispatch; sourceFormat branches decode. (Confirmed structurally via decode->detect region; cite :641 not directly opened but consistent.)"
    },
    {
      "from": "stage:load",
      "to": "stage:extract.decode",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:load",
      "to": "stage:extract.detect",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1145-1147",
      "verification": "VERIFIED",
      "how": {
        "what": "metadata.focal_length + the full HardMetadata object",
        "transform": "forwarded as SignalDetectParams.focalLength / .metadata; SignalProcessor reads it as detection context",
        "lossy": false
      },
      "review_note": "orchestrator_session.ts:1145-1146 focalLength: this.metadata?.focal_length, metadata: this.metadata forwarded to detectSignal. Confirmed."
    },
    {
      "from": "stage:load",
      "to": "stage:psf_attribution",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2016-2024",
      "verification": "VERIFIED",
      "how": {
        "what": "EXIF fields exposure_time / aperture (f-number) / focal_length",
        "transform": "validated (non-zero, non-DEFAULT) EXIF values feed the diffraction-floor and seeing/drift-magnitude physics calculations",
        "lossy": false
      },
      "why": {
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "337-340"
          }
        ],
        "text": "runPsfAttribution's `i.metadata` is this.metadata (load's output), read for exposureSec/fNumber/focalLengthMm."
      },
      "review_note": "orchestrator_session.ts:2019 metadata: this.metadata; psf_attribution.ts:338-340 validated (>0) exposure_time/aperture/focal_length feed diffraction+seeing. Confirmed."
    },
    {
      "from": "stage:load",
      "to": "stage:spcc",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:652",
      "verification": "VERIFIED",
      "how": {
        "what": "FrameContext per-frame sensor profile (photometry gain_e_adu etc.)",
        "transform": "used by runSpcc as the instrumental-magnitude zero-point basis (`ctx` argument)",
        "lossy": false
      },
      "why": {
        "text": "this.frameCtx originates at step1 (this.frameCtx = result.ctx) and may be extended during extract with master-calibration deposits before SPCC reads it; the base sensor profile is load's output.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1008-1023"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1917"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1917 this.frameCtx into runSpcc as ctx (gain/ZP basis); originates step1. Confirmed via consumer site (cite :652 not directly opened, corroborated)."
    },
    {
      "from": "stage:m7_refine",
      "to": "stage:bc_measure",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:m7_refine",
      "to": "stage:bc_rematch",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1709",
      "verification": "VERIFIED",
      "how": {
        "what": "solution.astrometry (the KEPT path's final astrometry, incl. gated SIP) as the rematch's candidate-astrometry INPUT",
        "transform": "P0-4 REORDER runs m7_refine FIRST: the rematch reads solution.astrometry.sip for its rms_before baseline, then RE-RUNS applyAstrometricRefinement on its own densified candidate set (lens_distortion_rematch_pass.ts:260)",
        "lossy": false
      },
      "review_note": "orchestrator_session.ts:1709-1712 comment: m7_refine runs FIRST, is both KEPT final astrometry AND rematch candidate-astrometry input (rematch reads solution.astrometry.sip for rms_before). Confirmed."
    },
    {
      "from": "stage:m7_refine",
      "to": "stage:render_apply_sip",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1874",
      "verification": "VERIFIED",
      "how": {
        "what": "solution.astrometry.sip (gated, internal convention) + the PIXEL-ledger previewFloat32 + wcs.crpix scaled to preview coords",
        "transform": "RENDER-plane crossing (LAW 1: render consumes BOTH ledgers, feeds NEITHER): ImageProcessor.applySipUndistort re-warps the preview through the fitted SIP inverse and rewrites previewUrl ONLY; DEFAULT OFF via PIPELINE_CONSTANTS.RENDER_APPLY_SIP (:1858) — flag/SIP/preview conditions unmet => {applied:false}, byte-identical; never touches WCS / matched_stars / the solve numbers (:1848-1849)",
        "lossy": true
      },
      "why": {
        "text": "The single explicit COORDINATE->RENDER ledger crossing in step5: a coordinate-ledger product (SIP) reaches pixels, but only display pixels. Default-off keeps the sacred path byte-identical (SeeStar has no SIP so it is a no-op regardless); the internal-convention SIP is correct here BECAUSE in-app consumers use convention (1) end to end — only the FITS/ASDF export boundary negates (see loud_edges_c sip_convention edges).",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1843-1856"
          },
          {
            "path": "src/engine/pipeline/export/sip_convention.ts",
            "lines": "50-54"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1850 sip=solution.astrometry.sip, :1858 RENDER_APPLY_SIP flag, :1874 applySipUndistort, :1878 rewrites previewUrl only; :1848-1849 never touches WCS/matched_stars. RENDER-plane crossing confirmed, default-off."
    },
    {
      "from": "stage:metrology",
      "to": "stage:solve",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1329",
      "verification": "VERIFIED",
      "how": {
        "what": "scaleLock (arcsecPerPx) from the shared metrology stage",
        "transform": "nativeScale = this.scaleLock || metadata.pixel_scale || 2.0 seeds the solver's scale prior (metrology sets scale_locked @1243)",
        "lossy": false
      },
      "review_note": "orchestrator_session.ts:1329 nativeScale = this.scaleLock || metadata.pixel_scale || 2.0; scale_locked emit at :1243. Confirmed."
    },
    {
      "from": "stage:metrology",
      "to": "stage:solve.quad_wasm",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_seestar.mjs:77",
      "verification": "VERIFIED",
      "how": {
        "what": "assert(elapsed < 15_000, 'scale lock took Xms — Tri-Lock ran? must be instant header lock') — metrology's scale must resolve fast enough to seed the quad solve's search band",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "run_wizard_seestar.mjs:77 assert(elapsed < 15_000, 'scale lock ... must be instant header lock') — metrology scale resolves fast enough to seed the quad search. Quoted text exact; timing-based evidence (interpretation reasonable for an evidence badge)."
    },
    {
      "from": "stage:metrology",
      "to": "stage:solve.quad_wasm",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:psf_attribution",
      "to": "stage:forced_confirm",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:psf_field",
      "to": "stage:forced_confirm",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2054-2075",
      "verification": "VERIFIED",
      "how": {
        "what": "frame-level PSF reference {fwhmPx, ellipticity, source, undersampled}",
        "transform": "derived from psfField.fwhmMedianMajPx / .ellipticityMedian / .method, compared against the SOLVER_CONFIRM_UNDERSAMPLED_FWHM_PX constant, and passed as forced_confirm's `framePsf` shape-test reference",
        "lossy": false
      },
      "why": {
        "text": "Explicitly sequenced 'AFTER psf_field so it consumes the MEASURED frame PSF for the shape test' in the source comment.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "2038-2039"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:2055-2059 framePsf from psfField.fwhmMedianMajPx/ellipticityMedian/method, compared to SOLVER_CONFIRM_UNDERSAMPLED_FWHM_PX; :2075 passed to runPostSolveConfirmation. Confirmed."
    },
    {
      "from": "stage:psf_field",
      "to": "stage:forced_confirm",
      "type": "evidence",
      "cite": "tools/api/confirm_null_evidence.apispec.ts:54-59",
      "verification": "VERIFIED",
      "how": {
        "what": "framePsf is built from s.psfField.{fwhmMedianMajPx,ellipticityMedian,method} and passed into runPostSolveConfirmation — proves forced_confirm consumes psf_field's measured PSF for aperture sizing",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "confirm_null_evidence.apispec.ts:54-59 framePsf built from s.psfField.{fwhmMedianMajPx,ellipticityMedian,method}; :70-72 passed into runPostSolveConfirmation. Confirmed forced_confirm consumes psf_field's measured PSF."
    },
    {
      "from": "stage:psf_field",
      "to": "stage:forced_confirm",
      "type": "fallback",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2055-2060",
      "verification": "VERIFIED",
      "why": {
        "text": "When psfField is absent or fwhmMedianMajPx is null, framePsf is set to null (not a fabricated value) — forced_confirm's shape test runs without a frame-PSF reference rather than failing.",
        "cites": []
      },
      "review_note": "orchestrator_session.ts:2055-2060 framePsf = (psfField && fwhmMedianMajPx!=null) ? {...} : null — null (not fabricated) when absent. Confirmed."
    },
    {
      "from": "stage:psf_field",
      "to": "stage:psf_attribution",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:2016-2024",
      "verification": "VERIFIED",
      "how": {
        "what": "PsfFieldReport (measured FWHM-major/minor, ellipticity, orientation field)",
        "transform": "psf_attribution copies (never mutates) psf_field's measured values and decomposes the measured anisotropy into physically-calculable systematics {drift/diffraction/seeing/refraction/coma} + residual",
        "lossy": false
      },
      "why": {
        "text": "Explicitly sequenced 'AFTER psf_field because it READS the measured field (the arbiter)' in the source comment — the clearest data-flow edge in this domain.",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "2009-2014"
          },
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "312-334"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:2009-2024 runPsfAttribution({psfField, solution, ...}); comment :2010 'AFTER psf_field because it READS the measured field'. Confirmed."
    },
    {
      "from": "stage:psf_field",
      "to": "stage:psf_attribution",
      "type": "fallback",
      "cite": "src/engine/pipeline/stages/psf_attribution.ts:312-315",
      "verification": "VERIFIED",
      "why": {
        "text": "When psfField (or solution) is null, runPsfAttribution short-circuits to emptyReport() — every field NOT_MEASURED — instead of throwing or fabricating a decomposition.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/psf_attribution.ts",
            "lines": "284-306"
          }
        ]
      },
      "review_note": "psf_attribution.ts:315 if(!psf||!sol) return emptyReport(...); emptyReport:284-306 all NOT_MEASURED. Confirmed honest-absent fallback."
    },
    {
      "from": "stage:psf_field",
      "to": "stage:psf_attribution",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:render_apply_sip",
      "to": "stage:spcc",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:solve",
      "to": "boundary:toolchest_arrow_export",
      "type": "evidence",
      "cite": "tools/api/arrow_sink.apispec.ts:86-98",
      "verification": "VERIFIED",
      "how": {
        "what": "run_summary.arrow row values (ra_hours/dec_degrees/pixel_scale/confidence/stars_matched) asserted toBe the receipt.solution values AND the SACRED anchors — proves the Arrow sink losslessly carries the solve stage's scalar outputs to disk",
        "transform": "receipt scalar fields -> single-row Arrow record batch",
        "lossy": false
      },
      "review_note": "arrow_sink.apispec.ts:89-94 run_summary ra_hours/dec_degrees/pixel_scale/confidence/stars_matched toBe receipt.solution.* AND SACRED anchors. Confirmed lossless carry."
    },
    {
      "from": "stage:solve",
      "to": "stage:calibrate",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/calibrate.ts:66",
      "verification": "VERIFIED",
      "how": {
        "what": "solution.matched_stars + solution.wcs (COORDINATE ledger: WCS + matched star correspondences)",
        "transform": "step5 calibrate reads matched_stars + wcs for m7 analysis / SIP-TPS gating / bc measure+rematch / SPCC / forced-confirm; the sub-stages (m7_analyze, sip_gate, tps_gate, m7_refine, bc_measure, bc_rematch, render_apply_sip) are withStage seams INSIDE calibrate, not separate base nodes",
        "lossy": false
      },
      "why": {
        "text": "This is the COORDINATE-ledger handoff: solve owns the WCS + matched_stars; calibrate reads them but its post-solve products (SPCC per-star, forced-confirm, psf_field, render_apply_sip) must never mutate WCS/matched_stars/the solve numbers. render_apply_sip inside calibrate is the RENDER plane (default-off, byte-identical off) — it consumes both ledgers and feeds neither (LAW 1).",
        "cites": [
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1735-1736"
          },
          {
            "path": "src/engine/pipeline/orchestrator_session.ts",
            "lines": "1848-1856"
          }
        ]
      },
      "review_note": "orchestrator_session.ts:1735-1736 (comment inside bc_measure) evidences calibrate reads solution.matched_stars + solution.wcs; sub-stages are withStage seams inside calibrate. Confirmed as coarse handoff."
    },
    {
      "from": "stage:solve",
      "to": "stage:calibrate.m7_analyze",
      "type": "data-flow",
      "cite": "src/engine/pipeline/stages/calibrate.ts:66",
      "verification": "VERIFIED",
      "how": {
        "what": "the PlateSolution: matched_stars (with per-star residual_arcsec) + the fitted WCS (COORDINATE ledger)",
        "transform": "ResidualAnalyzer.analyze(solution) computes residual RMS (arcsec) + distortion_pattern_detected + a CANDIDATE global order-3 SIP fit (fit whenever distortion fired + >20 matches); result landed on solution.astrometry at :68",
        "lossy": false
      },
      "review_note": "calibrate.ts:66 ResidualAnalyzer.analyze(solution), landed on solution.astrometry:68. Confirmed."
    },
    {
      "from": "stage:solve",
      "to": "stage:calibrate.m7_analyze",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:solve",
      "to": "stage:forced_confirm",
      "type": "evidence",
      "cite": "tools/api/confirm_null_evidence.apispec.ts:44-46,61-73",
      "verification": "VERIFIED",
      "how": {
        "what": "runPostSolveConfirmation is re-invoked directly on solution.wcs (crpix-shifted by known offsets); the set-gate collapses on every wrong WCS — proves forced_confirm consumes the solve stage's fitted WCS as its position-prediction input",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "confirm_null_evidence.apispec.ts:61-73 wcs=solution.wcs, crpix+off sweep [18,30,45,70,120], runPostSolveConfirmation on each wrong WCS; the set-gate must collapse on all. Confirmed forced_confirm consumes solve's fitted WCS as position prediction."
    },
    {
      "from": "stage:solve.quad_wasm",
      "to": "stage:solve",
      "type": "data-flow",
      "cite": "src/engine/pipeline/m6_plate_solve/solver_entry.ts:1754",
      "verification": "VERIFIED",
      "how": {
        "what": "flatMatches from scoreQuadsPlanarLocal (wasm solve_planar_local): flat 9-float stride per candidate [detId x4, catId x4, dist_sq] — geometric quad-match CANDIDATES, unitless code-space distances",
        "transform": "wasm quad matcher hands candidate correspondences back; the CPU f64 verify + WCS fit downstream own every verdict (candidates are never the answer); attempt clocked via accrueBranchTiming('solve.quad_wasm') at :1770",
        "lossy": false
      },
      "why": {
        "text": "This is the primary solve branch: when it locks, the stage_finished winner is solve.quad_wasm (stages/solve.ts:190, PASS with matched count; uw_sweep/uw_escalation only win via their SUCCESS_UW_* forensics statuses). Without this handoff there is no matched_stars set and nothing downstream (m7, BC, SPCC, forced-confirm) has input.",
        "cites": [
          {
            "path": "src/engine/pipeline/stages/solve.ts",
            "lines": "186-199"
          },
          {
            "path": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
            "lines": "1750-1764"
          }
        ]
      },
      "review_note": "solver_entry.ts:1754 scoreQuadsPlanarLocal, :1770 accrueBranchTiming('solve.quad_wasm'); solve.ts:190 winner=solve.quad_wasm on success. Confirmed primary branch."
    },
    {
      "from": "stage:solve.quad_wasm",
      "to": "stage:solve",
      "type": "evidence",
      "cite": "tools/api/solve_seestar.apispec.ts:136-141",
      "verification": "VERIFIED",
      "how": {
        "what": "byte-identical toBe() assertions on receipt.solution.{ra_hours,dec_degrees,pixel_scale,confidence,stars_matched} prove the wasm quad-solve output threads into the solve stage's receipt unmodified",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_seestar.apispec.ts:136-141 byte-exact toBe() on receipt.solution.{ra_hours,dec_degrees,pixel_scale,confidence,stars_matched}=SACRED. Confirmed."
    },
    {
      "from": "stage:solve.quad_wasm",
      "to": "stage:solve",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "fits"
      ]
    },
    {
      "from": "stage:spcc",
      "to": "stage:spcc_render_gains",
      "type": "data-flow",
      "cite": "src/engine/pipeline/orchestrator_session.ts:1962-1981",
      "verification": "VERIFIED",
      "how": {
        "what": "TLS-fitted per-channel gains block {gains[3], applied, nStars, r2} (spcc.cal.gains)",
        "transform": "spcc_render_gains re-renders the preview through spcc.cal.gains ONLY when the quality gate passed (g.applied) — otherwise SKIP, preview left on its empirical white-balance",
        "lossy": false
      },
      "why": {
        "text": "This is the load-bearing link between SPCC's measured color calibration and the render-lane white-balance application; spcc_render_gains has no other data source.",
        "cites": []
      },
      "review_note": "orchestrator_session.ts:1963 g=spcc.cal?.gains, :1964 gate on g.applied, :1971 {gains:g.gains,applied:true} re-renders preview else SKIP. Confirmed."
    },
    {
      "from": "stage:spcc",
      "to": "stage:spcc_render_gains",
      "type": "evidence",
      "cite": "tools/api/solve_cr2.apispec.ts:184-196",
      "verification": "VERIFIED",
      "how": {
        "what": "spcc.color_slope/color_intercept/color_r2/zeropoint asserted, then spcc.gains.applied===false asserted (r2<0.55 refusal) — proves spcc_render_gains consumes the spcc color fit's r2 to decide WB gain application",
        "transform": "n/a",
        "lossy": false
      },
      "review_note": "solve_cr2.apispec.ts:189-196 spcc.color_slope/color_intercept/color_r2/zeropoint asserted, then spcc.gains.applied===false (r2<0.55 refusal, :182/:195 comment). Confirmed gains consume the color fit's r2 to decide WB application."
    },
    {
      "from": "stage:spcc",
      "to": "stage:spcc_render_gains",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "stage:spcc_render_gains",
      "to": "stage:psf_field",
      "type": "stage-order",
      "cite": "test_results/e2e/seestar_2026-07-16T23-30-58-723Z/receipt.json",
      "verification": "VERIFIED",
      "how": {
        "what": "stage_finished emission order (StageRecord fold)",
        "transform": "consecutive adjacency in receipt.stage_records — fold order, not asserted data hand-off",
        "lossy": false
      },
      "walked_by": [
        "cr2",
        "fits"
      ]
    },
    {
      "from": "tools/api/headless_driver.ts",
      "to": "boundary:fits_io",
      "type": "data-flow",
      "cite": "tools/api/headless_driver.ts:383",
      "verification": "VERIFIED",
      "how": {
        "what": "an optional master dark/flat FITS file path -> a MasterFrame {data: Float32Array plane 0, width, height, md5, source}",
        "transform": "loadMasterFits opens the FITS via openFits + readPlaneRaw(f,0) (tools/stack/fits_io.mjs), copies plane 0 to Float32, and maps NaN/+-Inf out-of-footprint pixels to 0 so the master is a plain additive/divisive plane",
        "lossy": true
      },
      "why": {
        "text": "The R3 master-calibration seam consumes the FITS I/O boundary through the shared stacker reader. The NaN->0 flatten is lossy but the README notes calibration frames sit far from 0 so the exact-0 mask never bites. UNITS CAVEAT (open item): the master is loaded in its native FITS units and reconciling them to the engine's normalized detection buffer is unfinished — the seam, dim-validation refusal, and receipt are what this proves today, not the units match.",
        "cites": [
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "371-392"
          },
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "339-349"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/headless_driver.ts",
      "to": "boundary:toolchest_arrow_export",
      "type": "data-flow",
      "cite": "tools/api/headless_driver.ts:219",
      "verification": "VERIFIED",
      "how": {
        "what": "the FINISHED WizardReceipt object + the pre-decode frame sha (12-hex content id)",
        "transform": "persistArrowTables calls exportAllTables(receipt) then writeArrowFile for each of the four tabular products (matched_stars/detections/forced_confirmed/run_summary) into <SKYCRUNCHER_ARROW_SINK>/<runId>__<sha>/, plus a manifest.json",
        "lossy": true
      },
      "why": {
        "text": "This is the FIRST production consumer of the Arrow-export boundary (LAW 7: exportAllTables in packages/toolchest owns the schema/strides — nothing is defined here). Env-gated (SKYCRUNCHER_ARROW_SINK unset => zero behaviour), fires AFTER the receipt is built, and fully guarded (a read-only/racy FS degrades to null, never a thrown run) so the receipt is byte-identical whether the sink is on or off. Only the tabular SUBSET of the receipt rides Arrow — rasters ride typed arrays, never mixed — hence lossy.",
        "cites": [
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "186-248"
          },
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "492-495"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/headless_driver.ts",
      "to": "src/engine/pipeline/orchestrator_session.ts",
      "type": "control",
      "cite": "tools/api/headless_driver.ts:441",
      "verification": "VERIFIED",
      "how": {
        "what": "the source ArrayBuffer + RunWizardOptions (atlasRoot, overrides, callerHint, searchPriors, masterDark/Flat paths); frameSha256 hashed once BEFORE step1 to survive the libraw buffer-detach",
        "transform": "constructs OrchestratorSession(generatePreviews:false) and awaits step1_Load -> step2_Extract -> step3_Metrology -> step4_Solve -> step5_Calibrate -> step6_Integrate in order; the returned receipt is used directly (no second export-date-skewed copy)",
        "lossy": false
      },
      "why": {
        "text": "This ordered drive of the real session IS the mechanism that makes the headless receipt value-identical to the browser wizard export (LAW 2). generatePreviews:false + the fs atlas loader are the only headless deltas; the step sequence and every stage are the real engine, which is why the CR2/SeeStar apispec pins can assert browser numbers byte-for-byte.",
        "cites": [
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "413-482"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/headless_driver.ts",
      "to": "stage:calibrate",
      "type": "fallback",
      "cite": "tools/api/headless_driver.ts:455",
      "verification": "VERIFIED",
      "how": {
        "what": "the null session.solution after step4_Solve",
        "transform": "on no geometric lock the driver SKIPS step5_Calibrate + step6_Integrate, emits run_finished{ok:false}, and returns an honest exportFailurePacket receipt (stageOfDeath:'solve') with arrowDir:null and no community push",
        "lossy": false
      },
      "why": {
        "text": "Honest-or-absent degradation: a frame that produced no lock still banks a FAILURE receipt instead of throwing at the engine's step5 calibrate guard (`if (!this.solution) throw`). This branch is HEADLESS-ENTRY-SCOPED and fires ONLY when there is no solution, so the SOLVED path (both pinned reference solves + the apispec gates) is byte-identical by construction. The Arrow/community sinks are solve-products (need matched_stars) and are correctly skipped on this arm.",
        "cites": [
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "446-476"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/headless_driver.ts",
      "to": "tools/community/push_solve.mjs",
      "type": "data-flow",
      "cite": "tools/api/headless_driver.ts:286",
      "verification": "VERIFIED",
      "how": {
        "what": "receiptBytes = Buffer.from(JSON.stringify(receipt)) + the pre-decode frameSha256 hex + an optional engineRef",
        "transform": "pushCommunitySolve forwards them to pushSolveFromReceipt, which content-addresses by frame+artifact sha (two-level dedup) and uploads to the community R2 store; returns the 12-hex frame content id or null",
        "lossy": false
      },
      "why": {
        "text": "Same discipline as the Arrow sink: env-gated (SKYCRUNCHER_COMMUNITY_PUSH unset => no-op), fires AFTER the receipt is built, creds-absent OR any error => silent no-op, so the receipt is byte-identical either way. Network I/O is a tools-lane concern — the engine never imports this; the driver hands the finished receipt across. Content-addressing uses the PRE-decode frame sha because the libraw arm may have detached the source buffer.",
        "cites": [
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "250-298"
          },
          {
            "path": "tools/api/headless_driver.ts",
            "lines": "497-500"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/run.mjs",
      "to": "tools/api/solve_to_receipt.runspec.ts",
      "type": "control",
      "cite": "tools/api/run.mjs:111",
      "verification": "VERIFIED",
      "how": {
        "what": "the resolved FITS input path, the receipt output path, and an optional validated {KEY:value} config-override JSON, handed over as the env vars API_RUN_FITS / API_RUN_RECEIPT_OUT / API_RUN_CONFIG",
        "transform": "spawnSync(vitest run -c tools/api/run.config.ts) forks a FRESH vitest process whose collected spec is solve_to_receipt.runspec.ts; the child runs the real solve and writes the receipt, then run.mjs reads it back, projects --select paths, and sets the exit code",
        "lossy": false
      },
      "why": {
        "text": "A plain .mjs cannot resolve the engine @/ alias or boot the compiled wasm, so the CLI forks the vitest-hosted runspec (the same mechanism the overnight FITS driver uses). Forking a fresh process per solve is deliberate — the process-global PIPELINE_CONSTANTS mutation from --config can never leak between solves.",
        "cites": [
          {
            "path": "tools/api/run.mjs",
            "lines": "104-123"
          },
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "29-31"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/solve_to_receipt.runspec.ts",
      "to": "src/engine/pipeline/constants/pipeline_config.ts",
      "type": "data-flow",
      "cite": "tools/api/solve_to_receipt.runspec.ts:51",
      "verification": "VERIFIED",
      "how": {
        "what": "a flat {CONSTANT_NAME: number|string|boolean} override map parsed from the API_RUN_CONFIG env",
        "transform": "applyConfigOverrides(overrides) Object.assigns KNOWN constants onto the live PIPELINE_CONSTANTS BEFORE the solve (consumers read constants at call-time); unknown/mistyped keys are rejected + warned, never silently created; absent env => strict no-op => byte-identical calibrated solve",
        "lossy": false
      },
      "why": {
        "text": "This is the config-as-argument seam (NEXT_MOVES section 11b): the runspec is the only place run.mjs's --config knob experiment actually reaches the engine constants. Because this vitest process solves exactly once (run.mjs forks per solve), the process-global mutation never leaks; the receipt is then stamped experimental so an experimental run is never mistaken for a calibrated one.",
        "cites": [
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "44-53"
          },
          {
            "path": "tools/api/run.mjs",
            "lines": "22-31"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/solve_to_receipt.runspec.ts",
      "to": "src/engine/pipeline/stages/receipt_serializer.ts",
      "type": "data-flow",
      "cite": "tools/api/solve_to_receipt.runspec.ts:64",
      "verification": "VERIFIED",
      "how": {
        "what": "the finished WizardReceipt object from runWizardPipeline",
        "transform": "serializeReceipt(receipt) produces the CANONICAL receipt bytes (the shared Float32Array-stripping replacer path, byte-identical to the browser download) which are written to API_RUN_RECEIPT_OUT",
        "lossy": true
      },
      "why": {
        "text": "The runspec's whole contract is 'real solve -> full canonical receipt on disk'; using the shared serializeReceipt (not JSON.stringify) is what makes the dumped bytes identical to what the browser wizard downloads. Typed-array buffers are stripped by the replacer, so the on-disk JSON is a lossy projection of the in-memory receipt by design.",
        "cites": [
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "10-14"
          },
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "61-69"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/solve_to_receipt.runspec.ts",
      "to": "src/engine/pipeline/stages/workbench_deposit.ts",
      "type": "data-flow",
      "cite": "tools/api/solve_to_receipt.runspec.ts:59",
      "verification": "VERIFIED",
      "how": {
        "what": "a Node JSONL storage adapter (makeNodeJsonlStorage(WORKBENCH_DIR)) injected as the workbench store",
        "transform": "configureWorkbench({storage}) installs it so the always-on post-package deposit hook fired inside exportPacket persists a per-rig workbench row synchronously (completes before the receipt is returned)",
        "lossy": false
      },
      "why": {
        "text": "Wires the Optical Workbench default-on collection into the headless FITS run: local-only, gitignored dir (overridable via WORKBENCH_DIR for isolated evidence runs). Never-fatal by construction — a storage failure cannot perturb the receipt written just after, so the byte-identical gate holds.",
        "cites": [
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "32-34"
          },
          {
            "path": "tools/api/solve_to_receipt.runspec.ts",
            "lines": "55-61"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/stage_replay.replayspec.ts",
      "to": "boundary:seam_capsule",
      "type": "data-flow",
      "cite": "tools/api/stage_replay.replayspec.ts:116",
      "verification": "VERIFIED",
      "how": {
        "what": "in: the frozen post-(N-1) input capsule dir = capsule.json sidecar (SOLE layout authority) + per-buffer LE .bin (float32/uint16/uint8, flat row-major, sha256 in the sidecar); out: the replayed capsule dir in the same format",
        "transform": "loadCapsule sha256-verifies every buffer (throws, refusing partial replay, on any mismatch) and types it per its dtype; after running one real stage it writes changed buffers as bytes + sha-only entries for unchanged, tmp->rename on capsule.json",
        "lossy": false
      },
      "why": {
        "text": "This fills the boundary:seam_capsule consumer edge that loud_edges_c.json had to skip: the .mjs executor it named is not a base node, but this vitest-hosted replayspec IS the node that actually loads and re-writes capsules. The sha256 verify-on-load is load-bearing — a partial or corrupt capsule is refused loudly rather than silently replayed against stale bytes.",
        "cites": [
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "116-132"
          },
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "381-418"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/api/stage_replay.replayspec.ts",
      "to": "boundary:wasm_typed_array",
      "type": "control",
      "cite": "tools/api/stage_replay.replayspec.ts:179",
      "verification": "VERIFIED",
      "how": {
        "what": "the compiled wasm_compute_bg.wasm module bytes",
        "transform": "bootRealWasm() initSyncs the module (same specifier family the engine uses => one instance) and runs a post-boot sentinel, so psf_field's refine_stars_lm crossing resolves to the REAL fn instead of undefined",
        "lossy": false
      },
      "why": {
        "text": "replay.config.ts empties setupFiles to dodge the base suite's wasm MOCK, but that also means nothing boots the real module — so the stages that reach into it (m7_refine -> gnomonic_project; psf_field -> refine_stars_lm, the wasm_typed_array boundary producer) would get undefined fns and SILENTLY degrade. The explicit initSync here is the control precondition that makes the replay run the real numeric path.",
        "cites": [
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "39-46"
          },
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "74-87"
          },
          {
            "path": "tools/api/stage_replay.replayspec.ts",
            "lines": "252-264"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/atlas/atlas_to_arrow.mjs",
      "to": "boundary:arrow_seam",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:208",
      "verification": "VERIFIED",
      "how": {
        "what": "Apache Arrow IPC columnar (bitmap validity buffer + typed data buffer, null-count in field node), little-endian",
        "transform": "atlas rows -> Arrow RecordBatch column buffers (exact column schema UNVERIFIED in schema)",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:210-216 Arrow IPC columnar LE, producer atlas_to_arrow.mjs. Confirmed."
    },
    {
      "from": "tools/atlas/merge_hyg_sectors.mjs",
      "to": "boundary:atlas_rows",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:181",
      "verification": "VERIFIED",
      "how": {
        "what": "36 sectors/level_3_sector_0..35.json MINIFIED one-object-per-line, exact bytes = open-bracket + rows comma-plus-LF-joined + close-bracket; a Gaia block then a CONTIGUOUS HYG block, no interleave; regenerable BYTE-EXACT 38/38",
        "transform": "final HYG-merge writer (STRICT mag>6.8 HYG faint end to ~mag 21); sector id = decIndex*6 + raIndex",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:152-158 minified one-obj/line Gaia-then-HYG contiguous, regenerable 38/38; units:164 STRICT mag>6.8; sector id:157 decIndex*6+raIndex. Confirmed."
    },
    {
      "from": "tools/atlas/rebucket_sectors.mjs",
      "to": "tools/atlas/merge_hyg_sectors.mjs",
      "type": "data-flow",
      "cite": "tools/atlas/rebucket_sectors.mjs:37",
      "verification": "VERIFIED",
      "how": {
        "what": "Gaia-only re-bucketed sectors written to _rebuilt/level_3_sector_<id>.json — minified one-object-per-line (rows comma+LF-joined, wrapped in square brackets), each row JSON.stringify of a Gaia object (ra in DEGREES, mag_g, source_id); id = decIndex*6 + raIndex",
        "transform": "merge_hyg reads these as its --rebuilt Gaia block, then appends the STRICT mag>6.8 HYG rows (ra in HOURS) contiguously after; rebucket dedups by source_id first",
        "lossy": false
      },
      "why": {
        "text": "The atlas reproduce chain is generate_star_atlas -> rebucket_sectors (Gaia-only, adapter-correct ids) -> merge_hyg_sectors (HYG-filled final sectors). rebucket's output is exactly merge_hyg's --rebuilt input; the two row shapes it stitches carry ra in DIFFERENT units (Gaia degrees vs HYG hours), the per-row trap consumers must re-discriminate downstream.",
        "cites": [
          {
            "path": "tools/atlas/merge_hyg_sectors.mjs",
            "lines": "6-8"
          },
          {
            "path": "tools/atlas/merge_hyg_sectors.mjs",
            "lines": "13-27"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/atlas/verify_atlas_repro.mjs",
      "to": "boundary:atlas_rows",
      "type": "evidence",
      "cite": "tools/atlas/verify_atlas_repro.mjs:6",
      "verification": "VERIFIED",
      "how": {
        "what": "standing gate proving the frozen atlas_rows layout (L1/L2 anchors + 36 minified level-3 sectors) is byte-for-byte regenerable from committed inputs; independent reference reimplementation lands on the same bytes",
        "transform": "n/a",
        "lossy": false
      },
      "why": {
        "text": "The shipped deep catalog is gitignored and irreplaceable, so its layout contract has no other authority than 'we can regenerate it byte-exact'. This gate IS that authority; it also records that the frozen bytes encode the legacy buggy (ra-degrees-treated-as-hours) generator bucketing, so a corrected-generator regen would be content-identical but byte-different and need an owner rebaseline.",
        "cites": [
          {
            "path": "tools/atlas/verify_atlas_repro.mjs",
            "lines": "25-34"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/atlas/verify_atlas_repro.mjs",
      "to": "tools/atlas/merge_hyg_sectors.mjs",
      "type": "evidence",
      "cite": "tools/atlas/verify_atlas_repro.mjs:22",
      "how": {
        "what": "byte-proof gate imports the HYG-merge RULE from the tool under proof (mergeHygIntoSectors et al.) rather than re-duplicating it, and asserts all 38 files byte-EQUAL to the shipped atlas (exit 0 iff equal, or an honest content-hash fallback with recorded reason)",
        "transform": "n/a",
        "lossy": false
      },
      "verification": "VERIFIED",
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/corpus/run_corpus.mjs",
      "to": "boundary:atlas_rows",
      "type": "contract",
      "cite": "tools/corpus/run_corpus.mjs:49",
      "verification": "VERIFIED",
      "how": {
        "what": "level_3 sector rows in TWO shapes mixed per file: Gaia rows (mag_g/source_id present) store ra in DEGREES; HYG rows store ra in HOURS",
        "transform": "norm(s) = (mag_g!==undefined || source_id!==undefined) ? {raH: s.ra/15, mag: s.mag_g} : {raH: s.ra, mag: s.mag}; per-row discrimination normalizes everything to internal HOURS",
        "lossy": false
      },
      "why": {
        "text": "Parsing an hour-based HYG row as degrees (ra/15) scattered every mag<10 star ~150 deg away, so the brightest-50 catalog slice shared at most 1 star with the top-30 detections and no true quad could form (M66 FAIL_NO_LOCK). The mag_g/source_id discriminator is the load-bearing fix, and it is the SAME rule the engine's star_catalog_adapter and the stack lane's solve_lib each re-implement.",
        "cites": [
          {
            "path": "tools/corpus/run_corpus.mjs",
            "lines": "42-64"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/corpus/run_corpus.mjs",
      "to": "boundary:libraw_mem_image",
      "type": "contract",
      "cite": "tools/corpus/run_corpus.mjs:437",
      "verification": "VERIFIED",
      "how": {
        "what": "libraw-wasm imageData() with {noInterpolation, outputBps:16, noAutoBright, useCameraWb:false, useAutoWb:false} returns INTERLEAVED RGB16, meta.width*meta.height*3 Uint16, row stride width*3, values 0..65535 (NOT a CFA mosaic)",
        "transform": "explicit payload-contract-violation guard: throw DECODE_UNSUPPORTED if u16.length !== W*H*3; luminance for detection = (R+G+B)/3 normalized to [0,1]",
        "lossy": true
      },
      "why": {
        "text": "The corpus CR2 lane is a SECOND consumer of the libraw interleaved-RGB16 contract (distinct from tools/psf/decode_cr2.mjs and tools/color/rgb_to_xyz.mjs), and it hard-asserts the w*h*3 element count so a decoder that silently returns a CFA mosaic or a different plane count fails loudly instead of mislocating stars.",
        "cites": [
          {
            "path": "tools/corpus/run_corpus.mjs",
            "lines": "400-447"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/corpus/run_corpus.mjs",
      "to": "tools/corpus/ledger.mjs",
      "type": "data-flow",
      "cite": "tools/corpus/run_corpus.mjs:840",
      "verification": "VERIFIED",
      "how": {
        "what": "test_results/corpus_report.json {when, total, byStatus, rows:[{file,status,scale,matches,...}]} written at sweep end, path handed to ingestReport",
        "transform": "ingestReport reads the report and appends one JSONL line per row to the cumulative test_results/corpus_ledger.jsonl, prepending {ts, commit, frame:basename, solved: PASS||startsWith('SOLVED')}; wrapped in try/catch so ledger I/O never breaks the sweep",
        "lossy": false
      },
      "why": {
        "text": "run_corpus overwrites corpus_report.json every sweep; the ledger is the only place the per-frame history survives, and the commit tag on each line is what turns a frame's failed->solved flip into MEASURED improvement across code versions.",
        "cites": [
          {
            "path": "tools/corpus/ledger.mjs",
            "lines": "37-60"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/dslr/dump_cr2_solveframe.mjs",
      "to": "tools/dslr/uw_solve.uwspec.ts",
      "type": "data-flow",
      "cite": "tools/dslr/uw_solve.uwspec.ts:34",
      "verification": "VERIFIED",
      "how": {
        "what": "Stage-A dump JSON (CR2_DUMP path): detections + pixel scale + planet centers (correct Schlyter ephemeris) for one decoded CR2 frame",
        "transform": "the harness reads it (no re-decode) and injects the planet centers as extraSearchCenters, plus optional CR2_CENTERS 'ra,dec' priors in hours,deg; answers 'given a good center, does the sweep+verify lock, at what sigma?'",
        "lossy": false
      },
      "why": {
        "text": "Splitting decode (Stage A) from solve (Stage B) lets the ultra-wide solver be exercised at test speed without a browser or re-decode — the dump is the sole input carrying detections/scale/ephemeris into the headless spec.",
        "cites": [
          {
            "path": "tools/dslr/uw_solve.uwspec.ts",
            "lines": "5-19"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/dslr/uw_solve.uwspec.ts",
      "to": "src/engine/pipeline/m6_plate_solve/solver_entry.ts",
      "type": "evidence",
      "cite": "tools/dslr/uw_solve.uwspec.ts:30",
      "verification": "VERIFIED",
      "how": {
        "what": "vitest spec drives the REAL solver_entry.solvePlate ultra-wide path (anchored rotation sweep -> TS verify) headlessly at test speed, with an atlas-from-disk fetch shim; planet anchors from the Stage-A dump injected as extraSearchCenters priors",
        "transform": "isolates candidate-generator + verifier + detection quality from blind center generation; the per-center loop catches the quad fallback's missing-fn panic so peak-z is observable even on frames that never lock, and CR2_EXPECT_RA asserts the solved RA within CR2_RA_TOL",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/e2e/lib.mjs",
      "to": "src/engine/pipeline/orchestrator_session.ts",
      "type": "data-flow",
      "cite": "tools/e2e/lib.mjs:267",
      "verification": "VERIFIED",
      "how": {
        "what": "a scalar-only snapshot of the live session: status/state/sourceFormat/scaleLock, metadata scalars (camera/focal_length/pixel_scale/ra_hint/gps), signalStars count, computed_jd, and solution scalars (ra_hours/dec_degrees/pixel_scale/rotation/parity/confidence/num_stars/matched/spatial_hash)",
        "transform": "sessionSnapshot runs page.evaluate over window.__astroSession (the app's dev-exposed OrchestratorSession) and returns ONLY scalars — heavy arrays/buffers (matched_stars is reduced to its length) are never pulled across the wire",
        "lossy": true
      },
      "why": {
        "text": "This dev-exposed read is how every e2e scenario verifies the solve: the pinned RA/Dec/scale/matched/confidence asserts all read this snapshot, not the DOM. It is an observation of a browser-context global, not a module import — recorded here so the harness->engine verification path is visible in the graph. Scalar-only by design so a screenshot-heavy e2e never serializes a 50MB buffer.",
        "cites": [
          {
            "path": "tools/e2e/lib.mjs",
            "lines": "266-292"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/e2e/run_wizard_carina.mjs",
      "to": "stage:extract.detect",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_carina.mjs:26",
      "verification": "VERIFIED",
      "how": {
        "what": "browser console.error text matched against /get_cfa_input_ptr|bin_bayer_to_luma/ plus the pageErrors list",
        "transform": "the native-Bayer detection crash is CAUGHT by SignalGraphStep's try/catch so it surfaces as a console.error not an uncaught pageerror; cfaCrash() is true if either channel matched — the harness asserts step-3 native-Bayer detection neither crashes nor mislabels the failure",
        "lossy": false
      },
      "why": {
        "text": "Repro/verify harness for the 'get_cfa_input_ptr on uninitialized wasm' crash in the native-Bayer detection lane (analyzeBayerNative -> binBayerToLuminance) on a Canon 60Da raw-Bayer FITS. It watches BOTH the caught-console-error and uncaught-pageerror channels because the failure is swallowed by the UI try/catch. (Cited to the header contract + crash-detector that were read this session; the numeric step-3 detection assert itself was not in the read range.)",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_carina.mjs",
            "lines": "1-9"
          },
          {
            "path": "tools/e2e/run_wizard_carina.mjs",
            "lines": "22-29"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/e2e/run_wizard_cr2.mjs",
      "to": "stage:calibrate",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_cr2.mjs:173",
      "verification": "VERIFIED",
      "how": {
        "what": "solution.bc_rematch read off the dev-exposed session after step-6 forensic calibration (recovered_stars array dropped, only its length kept)",
        "transform": "pins attempted===true, guard==='KEPT_ORIGINAL', applied===false, false_guard_passes===false (wrong-sign control is the holding prong), matched_before===56, matched_after===56, rms_before_arcsec===630.8214, rms_after_arcsec===616.8437 (exact float repr)",
        "lossy": false
      },
      "why": {
        "text": "Pins the POST-solve measured-Brown-Conrady two-pass rematch OUTCOME (rawler default arm) by ADDITION — the step-5 acquisition pins are untouched. On this frame the wrong-sign guard fails (recovered matches indistinguishable from the negated-coefficient control) so KEPT_ORIGINAL holds and the solve stays byte-identical; the cold libraw path (VITE_DECODER_RAWLER=0) restores different pins. This exercises the calibrate stage's bc_measure->bc_rematch sub-seams which are not separate base nodes.",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_cr2.mjs",
            "lines": "126-183"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/e2e/run_wizard_cr2.mjs",
      "to": "stage:solve",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_cr2.mjs:98",
      "verification": "VERIFIED",
      "how": {
        "what": "the step-5 ultra-wide BLIND solve outcome (solved | honest_failure | timeout) + the bundled-sample solution scalars",
        "transform": "races the solve-confirm actionability against the failure overlay; a dishonest terminal state (neither surfaced) throws; on solve for the bundled sample it asserts scale within 63.35 arcsec/px +-30% and matched>=8; the 420s step ceiling stays above the engine's 360s blind budget so the harness never kills a solve the engine still owns",
        "lossy": false
      },
      "why": {
        "text": "The bundled Canon CR2 ultra-wide blind solve (Phase B's real work). Contract is honest-outcome-either-way for arbitrary CR2_FILE inputs, but the bundled sample pins the rawler-default acquisition (79-matched, ~63.4 arcsec/px). This is the browser twin of solve_cr2.apispec's blindOutcome=solved pin.",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_cr2.mjs",
            "lines": "97-124"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/e2e/run_wizard_cr2_export.mjs",
      "to": "stage:package",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_cr2_export.mjs:90",
      "verification": "VERIFIED",
      "how": {
        "what": "the receipt from window.__astroSession.exportPacket() serialized in-page with save_packet's DROPPED_KEYS filter (scienceBuffer/segmentationMasks/horizonVector/anomaly_grid stripped)",
        "transform": "captures the packet directly after the step-5 solve confirms (steps 6-7 skipped — exportPacket is a pure builder over session state) and asserts matched_stars>=8 and a numeric WCS block (finite CRVAL1)",
        "lossy": true
      },
      "why": {
        "text": "A variant of run_wizard_cr2 whose contract REQUIRES a solution (a failed/absent solve is a scenario failure here) because its purpose is to produce a receipt artifact. It exercises the export/package path with the SAME DROPPED_KEYS filtering as the browser save_packet, so typed arrays never cross the wire as index-keyed objects.",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_cr2_export.mjs",
            "lines": "1-16"
          },
          {
            "path": "tools/e2e/run_wizard_cr2_export.mjs",
            "lines": "88-106"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/e2e/run_wizard_seestar.mjs",
      "to": "stage:solve",
      "type": "evidence",
      "cite": "tools/e2e/run_wizard_seestar.mjs:106",
      "verification": "VERIFIED",
      "how": {
        "what": "the step-5 solution snapshot (ra_hours/dec_degrees/pixel_scale/rotation/parity/confidence/matched) read straight after the solve confirm becomes actionable",
        "transform": "assertRange RA in [11.303,11.369]h, Dec in [12.75,13.35] deg, scale in [3.48,3.88] arcsec/px, matched>=5, confidence in [0.6,1.01], parity abs==1 — the sacred SeeStar geometry",
        "lossy": false
      },
      "why": {
        "text": "The SeeStar M66 FITS pinned reference solve: this scenario is the browser-side twin of solve_seestar.apispec's byte-identical numbers and a break is a revert trigger. The parity assert is deliberately a unit-magnitude check (image-space y-down convention reports +1 for this mirrored-sky frame) rather than a sign assertion, per repo law.",
        "cites": [
          {
            "path": "tools/e2e/run_wizard_seestar.mjs",
            "lines": "85-124"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/psf/corrections.mjs",
      "to": "src/engine/pipeline/m2_hardware/lens_distortion.ts",
      "type": "contract",
      "cite": "tools/psf/corrections.mjs:53",
      "verification": "VERIFIED",
      "how": {
        "what": "the Brown-Conrady radial coordinate function makeBrownConrady(k1,k2,w,h) — r_d = r_u*(1 + k1 r_u^2 + k2 r_u^4), r normalized to the half-diagonal, optical center at frame center",
        "transform": "faithfully ported to the live engine as makeBrownConradyDistortion (the solve-path lens-prior consumer); this lane stays the fast-iteration incubator + CLI driver",
        "lossy": false
      },
      "why": {
        "text": "INCUBATOR DUPLICATION (LAW 4): the same radial model lives in two files by design — the tools lane for fast iteration and the engine for the solve path. The in-file note names the twin explicitly and requires keeping them in sync when the radial model changes; a silent divergence would apply different distortion in the CLI vs the app.",
        "cites": [
          {
            "path": "tools/psf/corrections.mjs",
            "lines": "53-70"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/psf/decode_cr2.mjs",
      "to": "tools/psf/measure_and_clean.mjs",
      "type": "data-flow",
      "cite": "tools/psf/measure_and_clean.mjs:31",
      "verification": "VERIFIED",
      "how": {
        "what": "the decode primitives fixHotPixelsCFA, demosaicBilinear, splitRGB (plus inferDims for the stride-coherence-verified dims), operating on the interleaved RGB16 dominant-channel mem_image",
        "transform": "measure_and_clean builds its linear camera-native working planes: demosaicBilinear(rgb16, w, h, layout.pat) after CFA-level hot-pixel repair (before any interpolation smears them); no white-balance or color matrix applied",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/psf/solution_to_astrometry.mjs",
      "to": "tools/psf/corrections.mjs",
      "type": "unit-conversion",
      "cite": "tools/psf/solution_to_astrometry.mjs:24",
      "verification": "VERIFIED",
      "how": {
        "what": "in: exported AstroPacket receipt (engine solution.wcs crval[0] in HOURS; packet FITS-style wcs block CRVAL1 in DEGREES via generateWCS *15). out: the --astrometry socket JSON {wcs: {crpix, crval:[ra_deg, dec_deg], cd deg/px}, distortion: brown-conrady k1/k2, psf_anchors}",
        "transform": "emits crval in DEGREES per SOLVE_SOCKET_CONTRACT; fits Brown-Conrady about the FRAME CENTER as delta = a*r + k1*r^3 + k2*r^5 (linear term absorbs WCS scale degeneracy, only k1/k2 exported), one 3-sigma reclip; consumed by corrections.getCorrections in place of APPROXIMATE_PROFILE",
        "lossy": true
      },
      "why": {
        "text": "The socket JSON crosses the HOURS/DEGREES land mine deliberately: the engine-internal fitted WCS is HOURS but the tool emits DEGREES (ra_deg) to match corrections.mjs's documented contract. Reading the emitted crval as hours would mislocate the whole correction frame by 15x; the tool handles both packet matched-star shapes (ra_deg vs internal ra_hours) explicitly rather than guessing.",
        "cites": [
          {
            "path": "tools/psf/solution_to_astrometry.mjs",
            "lines": "22-34"
          },
          {
            "path": "tools/psf/corrections.mjs",
            "lines": "14-25"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/stack/fits_io.mjs",
      "to": "boundary:fits_io",
      "type": "unit-conversion",
      "cite": "tools/stack/fits_io.mjs:216",
      "verification": "VERIFIED",
      "how": {
        "what": "in: engine-internal WCS crval[0] in HOURS, crpix 0-based; out: FITS CRVAL1 in DEGREES, CRPIX1 1-based",
        "transform": "CRVAL1 = wcs.crval[0] * 15; CRPIX1 = wcs.crpix[0] + 1 (stacker writer path — eats HOURS)",
        "lossy": false
      },
      "why": {
        "text": "There are TWO WCS->FITS paths and each converts at a DIFFERENT boundary so no value is double-converted: the stacker (here) eats HOURS and multiplies by 15; the receipt/export writer eats DEGREES and must NEVER re-multiply. Feeding a receipt.wcs to wcsCards would x15 an already-degrees CRVAL.",
        "cites": [
          {
            "path": "tools/stack/fits_io.mjs",
            "lines": "195-208"
          }
        ]
      },
      "review_note": "fits_io.mjs:216 CRVAL1=wcs.crval[0]*15, :218 CRPIX1=wcs.crpix[0]+1; docstring:195-208 documents stacker-eats-HOURS boundary. Exact."
    },
    {
      "from": "tools/stack/fits_io.mjs",
      "to": "tools/stack/stack.mjs",
      "type": "data-flow",
      "cite": "tools/stack/stack.mjs:46",
      "verification": "VERIFIED",
      "how": {
        "what": "per-frame FITS planes in ORIGINAL (un-normalized) units + parsed header cards, via openFits/readPlaneRaw/readLuminanceNormalized/readFitsHeaderFd",
        "transform": "stack's prep phase decodes luminance and detects stars from these raw planes; raw units are required because per-frame background subtraction and cross-frame flux normalization happen in original units; BITPIX=-32 out-of-footprint samples (non-finite AND exact 0.0) arrive as NaN so bilinear sampling auto-invalidates at edges",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/stack/solve_lib.mjs",
      "to": "boundary:atlas_rows",
      "type": "contract",
      "cite": "tools/stack/solve_lib.mjs:28",
      "verification": "VERIFIED",
      "how": {
        "what": "same hybrid level_3 sector rows: Gaia rows ra in DEGREES (mag_g/source_id), HYG rows ra in HOURS",
        "transform": "loadAtlas's norm is the corpus runner's discriminator verbatim (mag_g||source_id ? ra/15 : ra); everything normalized to internal HOURS; crval[0] stays HOURS through the whole solve lane",
        "lossy": false
      },
      "why": {
        "text": "The stack lane re-implements the Gaia-deg/HYG-hours per-row discriminator as a literal port of the corpus runner's `norm`; this is the third independent copy of the 15x trap and a LAW-4 drift hazard — a fix to the atlas row units must land in all three (adapter, corpus, stack) or the copies diverge.",
        "cites": [
          {
            "path": "tools/stack/solve_lib.mjs",
            "lines": "25-52"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/stack/solve_lib.mjs",
      "to": "tools/stack/stack.mjs",
      "type": "data-flow",
      "cite": "tools/stack/stack.mjs:47",
      "verification": "VERIFIED",
      "how": {
        "what": "the ported solve machinery: initWasm/loadAtlas region query, gnomonic/inverseGnomonic/pixToSky/skyToPix, extractStars/refineCentroids/medianFwhm, solveAtHint quad locks, refineWCS N-point catalog refit, blindHintGrid",
        "transform": "stack's solve+registration phases call these; a quad lock is only accepted once refineWCS corroborates it (stack's REFINE_MIN_N=12 / REFINE_MAX_RMS_PX=2.5 deep-verify gate) — a coincidence lock cannot find even 8 anchors",
        "lossy": false
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/stack/stack.mjs",
      "to": "tools/psf/imaging.mjs",
      "type": "data-flow",
      "cite": "tools/stack/stack.mjs:51",
      "verification": "VERIFIED",
      "how": {
        "what": "validation/preview raster arrays (stacked render, Siril side-by-side crop, rejection sanity crop)",
        "transform": "writePNG (imported from the psf lane) encodes them to PNG files in the stack output dir",
        "lossy": true
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/stack/stack.mjs",
      "to": "tools/stack/fits_io.mjs",
      "type": "data-flow",
      "cite": "tools/stack/stack.mjs:39",
      "verification": "VERIFIED",
      "how": {
        "what": "final stacked/drizzled float32 plane(s) on the output tangent grid + the output-grid engine WCS (crval[0] in HOURS, crpix 0-based)",
        "transform": "writeFitsPlanar serializes the float32 image; wcsCards turns the engine WCS into FITS keywords, applying CRVAL1 = crval[0]*15 (HOURS->DEGREES) and CRPIX1 = crpix[0]+1 (0-based -> 1-based) at THIS boundary",
        "lossy": false
      },
      "why": {
        "text": "The stacker feeds wcsCards an engine-internal (HOURS) WCS; wcsCards is the stacker-path conversion boundary and multiplies by 15 here. A receipt WCS (already DEGREES) must never be fed to wcsCards or the CRVAL is doubly x15'd — the two WCS->FITS paths convert at different boundaries by design.",
        "cites": [
          {
            "path": "tools/stack/fits_io.mjs",
            "lines": "190-224"
          }
        ]
      },
      "review_note": "haiku cross-check (two-model rule, owner 2026-07-16)"
    },
    {
      "from": "tools/starplates/build_release.mjs",
      "to": "boundary:starplates_blobs",
      "type": "data-flow",
      "cite": "src/engine/contracts/binary_layouts.ts:190",
      "verification": "VERIFIED",
      "how": {
        "what": "t0/t1 blobs + 4-band lost-in-space index (~40.6MB, 4 bands); EXACT blob stride + band-index layout UNVERIFIED in the schema (deliberately UNVERIFIED-heavy)",
        "transform": "release builder; byte layout must be transcribed from the builder BEFORE any consumer rewire (LAW 7)",
        "lossy": false
      },
      "review_note": "binary_layouts.ts:190-205 t0/t1 blobs + 4-band index ~40.6MB, UNVERIFIED-heavy, transcribe-before-rewire (LAW 7). Matches how."
    }
  ]
}
