# Divergence Registry — Heterodyne → Homodyne Strict 1:1 Parity

Records every gap from `REPORT.md` that was dispositioned `DROP` in `DISPOSITIONS.md`. Each entry names the divergence and the one-line rationale.

Populated incrementally by Phase 4 PRs as they encounter DROP-dispositioned rows. Use this file as the authoritative source of "what is intentionally different between heterodyne and homodyne" for future audit re-runs.

## D1 — Physics-exempt files (5 rows)

- file_inventory:missing_py_file | `optimization.nlsq.shear_weighting` | Layer 5 (shear-sensitivity weighting); not applicable to heterodyne's velocity-phase physics model (D1, spec §2)

## D2 — Physics-specific docs (10 rows)

_Populated by Phase 4 PR 10._

## D3 — Parameter-content / shear-physics divergence (~148 rows)

_Populated incrementally by Phase 4 PRs 1–11 as each PR encounters D3-matching rows in its scope. Each entry: `category:kind | qualname | rationale (one of: Layer-5 shear-only / homodyne-only param / homodyne-only physics class / homodyne-only CLI mode)`._

- file_inventory:extra_py_file | `optimization.nlsq.strategies.chunked` | heterodyne-only ChunkedStrategy class (memory-bounded NLSQ chunking); no homodyne equivalent. KEEP (D3 heterodyne-required). Phase 4 PR 1 Task 1.2 Step 2.5 verification classified the chunked.py↔chunking.py pair as "different files" — homodyne's chunking.py contains stratification utility functions (analyze_angle_distribution, StratificationDiagnostics, create_angle_stratified_data, etc.) deferred to a separate port task (not in scope for PR 1 Task 1.2 as originally written).
- file_inventory:missing_py_file | `optimization.nlsq.strategies.chunking` (deferred task #37) | Homodyne `strategies/chunking.py` is 1199 lines containing 13 public utility functions (`analyze_angle_distribution`, `AngleDistributionStats`, `StratificationDiagnostics`, `estimate_stratification_memory`, `estimate_nlsq_optimization_memory`, `calculate_adaptive_chunk_size`, `create_angle_stratified_data`, `create_angle_stratified_indices`, `StratifiedIndexIterator`, `get_stratified_chunk_iterator`, `should_use_stratification`, `compute_stratification_diagnostics`, `format_diagnostics_report`). Heterodyne's `strategies/stratified_ls.py` is 193 lines containing ONE class (`StratifiedLSStrategy`) — class-based abstraction that embeds equivalent stratification logic in the strategy's `fit()` method rather than exposing it as a 13-function public utility API. Porting homodyne's chunking.py to heterodyne would (a) create a parallel public API that conflicts with the StratifiedLSStrategy class-based design, (b) add 1006 lines of utility code that StratifiedLSStrategy's internal implementation already covers, and (c) require careful 14-param adaptation throughout. Same architectural divergence pattern as PRs 5-9 (class-based vs functional API). KEEP (D3 heterodyne architectural choice — StratifiedLSStrategy class wrapper). Phase 4 deferred task #37.
- file_inventory:extra_py_file | `optimization.nlsq.strategies.base` | heterodyne-only strategy dispatch abstraction (`StrategyResult` dataclass, `FittingStrategy` Protocol, `select_strategy` function, `_estimate_available_memory`). Referenced by 11 source files: every strategy class (`chunked`, `jit_strategy`, `residual`, `residual_jit`, `sequential`, `hybrid_streaming`, `out_of_core`, `stratified_ls`), `executors.py`, `strategies/__init__.py`, and `nlsq/__init__.py`. Homodyne removed its equivalent `selection.py` in v2.12.0 (note: "Use NLSQ's WorkflowSelector instead") and replaced `FittingStrategy` with a raw-Callable ABC interface (`OptimizationExecutor.execute(residual_fn, xdata, ydata, ...)`). Migrating heterodyne to homodyne's pattern would require restructuring all 11 source files and 2 test files with no structural target to migrate to. KEEP (D3, heterodyne architectural choice). Phase 4 PR 1 Task 1.3.
- file_inventory:extra_py_file | `optimization.nlsq.strategies.jit_strategy` | heterodyne-only `JITStrategy` class: JAX-JIT-compiled full Jacobian + residual fitting strategy with XLA compilation-cache key logging, covariance estimation from the Jacobian (`_estimate_covariance_from_jac`), compile-time/run-time accounting, and a `dogbox→trf` coercion guard. Referenced from 10 distinct files: `strategies/__init__.py`, `strategies/base.py` (`select_strategy` default), `strategies/executors.py` (lazy import), `strategies/residual.py` (docstring cross-reference), `strategies/residual_jit.py` (docstring comparison), `strategies/sequential.py` (strategy dispatch map), `nlsq/__init__.py` (public re-export), and 3 test files (`test_strategies_nlsq.py`, `test_strategies_base.py`, `test_no_scipy.py`). Homodyne has no `jit_strategy.py` in its strategies directory (homodyne strategies: `residual_jit`, `executors`, `stratified_ls`, `out_of_core`, `residual`, `chunking`, `sequential`, `hybrid_streaming`). KEEP (D3, heterodyne architectural choice — JIT strategy is the primary high-performance path for the heterodyne two-path integral model). Phase 4 PR 1 Task 1.4.
- file_inventory:extra_py_file | `optimization.nlsq.validation.bounds` | heterodyne-only `BoundsValidator` class: validates fitted parameters against the heterodyne 14-parameter `DEFAULT_REGISTRY` (hard bounds check + soft edge-fraction proximity warning). Imports `DEFAULT_REGISTRY` directly and issues severity-graduated `ValidationReport` entries. Homodyne has no `bounds.py` in its validation subpackage (homodyne validation: `result_validator.py`, `fit_quality.py`, `input_validator.py`). Only 2 distinct import sites outside the file itself, both `__init__.py` re-exports (`validation/__init__.py` and `nlsq/__init__.py`), but the functionality is inseparable from heterodyne's parameter registry. KEEP (D3, heterodyne-required — bounds contract tied to the 14-param registry absent from homodyne). Phase 4 PR 1 Task 1.4.
- file_inventory:extra_py_file | `optimization.nlsq.validation.convergence` | heterodyne-only `ConvergenceValidator` class: assesses NLSQ optimizer convergence quality (termination status, iteration-limit detection, outlier-residual ratio check). Imports base types (`ValidationIssue`, `ValidationReport`, `ValidationSeverity`) from `validation/result.py`. Homodyne has no `convergence.py` in its validation subpackage; convergence checks are folded into `result_validator.py` as individual `validate_*` functions without a dedicated class. Only 2 distinct import sites outside the file itself, both `__init__.py` re-exports. KEEP (D3, heterodyne architectural choice — separate convergence class retained from heterodyne's original validation architecture). Phase 4 PR 1 Task 1.4.
- file_inventory:extra_py_file | `optimization.nlsq.validation.result` | heterodyne-only base-type module: defines `ValidationSeverity` (Enum), `ValidationIssue` (dataclass), `ValidationReport` (dataclass with `.errors`/`.warnings`/`.summary` helpers), and the class-based `ResultValidator`. These base types are imported by `validation/bounds.py`, `validation/convergence.py`, and `validation/__init__.py`. Heterodyne additionally has `validation/result_validator.py` (the homodyne-parity functional API introduced in the NLSQ parity fixes of 2026-05-11). Homodyne's `result_validator.py` defines its own local `ValidationReport` dataclass inline and exports `validate_covariance`, `validate_optimized_params`, `validate_result_consistency` as standalone functions — it has no separate base-type module. The heterodyne `result.py` cannot be collapsed into `result_validator.py` without also migrating `bounds.py` and `convergence.py` (both D3 KEEP above), which would require dissolving those two files. KEEP (D3, foundation dependency for three D3-kept files; elimination requires a coordinated multi-file restructure deferred to a future parity PR). Phase 4 PR 1 Task 1.4.
- classes:missing_method | `optimization.nlsq.anti_degeneracy_controller.AntiDegeneracyController.get_shear_weights` | Layer 5 method; heterodyne has no shear term in the g2 formula (D3, spec §2). Phase 4 PR 2 Task 2.4.
- classes:missing_method | `optimization.nlsq.anti_degeneracy_controller.AntiDegeneracyController.update_shear_phi0` | Layer 5 method; heterodyne has no shear term in the g2 formula (D3, spec §2). Phase 4 PR 2 Task 2.4.
- classes:missing_method | `optimization.nlsq.anti_degeneracy_controller.AntiDegeneracyController.use_shear_weighting` | Layer 5 property; always False in heterodyne — no shear physics (D3, spec §2). Phase 4 PR 2 Task 2.4.
- classes:missing_field  | `optimization.nlsq.anti_degeneracy_controller.AntiDegeneracyController.shear_weighter` | Layer 5 field; heterodyne has no shear term in the g2 formula (D3, spec §2). Phase 4 PR 2 Task 2.4.
- missing_method | `optimization.nlsq.adapter.NLSQAdapter.fit` kwarg `shear_transforms` | Layer-5 shear-only; heterodyne has no shear sinc term in the g2 formula, so this kwarg is omitted from the ported `fit()` signature. `analysis_mode` default changed from homodyne's `'static_isotropic'` to `'full'` — heterodyne always uses its 14-parameter full model. Phase 4 PR 3 Batch 1.

- changed:signature | `optimization.nlsq.jacobian.analyze_parameter_sensitivity` | Heterodyne takes `(jacobian: np.ndarray, param_names)` (pre-computed Jacobian); homodyne takes `(residual_fn, x_subset, params, param_names)`. Heterodyne's callers all pass the Jacobian array directly (11 test sites). Migrating to the residual_fn interface would break the existing compute_numerical_jacobian + compare_jacobians toolchain. KEEP (D3, heterodyne architectural choice — Jacobian-first API). Phase 4 PR 3 Batch 4.
- changed:signature | `optimization.nlsq.jacobian.compute_jacobian_condition_number` | Heterodyne takes `(jacobian: np.ndarray) -> float`; homodyne takes `(residual_fn, x_subset, params) -> float | None`. Same rationale as analyze_parameter_sensitivity above — pre-computed Jacobian API used by all regression tests. KEEP (D3, heterodyne architectural choice). Phase 4 PR 3 Batch 4.
- changed:signature | `optimization.nlsq.jacobian.estimate_gradient_noise` | Heterodyne takes `(jacobian, residuals) -> dict[str, float]`; homodyne takes `(residual_fn, x_subset, params, n_samples, perturbation, seed) -> float | None`. Heterodyne's richer return type (mean/max ratio + noisy fraction) is a superset. KEEP (D3, heterodyne architectural choice). Phase 4 PR 3 Batch 4.
- missing_in_heterodyne | `optimization.nlsq.fit_computation.compute_g2_batch` | Homodyne's g2 uses `(t1, t2, L)` two-time matrix with sample-to-detector distance; heterodyne uses `(t, q, dt)` single time array — not portable. Heterodyne equivalent is `compute_c2_batch`. KEEP (D3, physics model difference — two-path integral architecture). Phase 4 PR 3 Batch 4.
- missing_in_heterodyne | `optimization.nlsq.fit_computation.compute_g2_batch_with_per_angle_scaling` | Same physics mismatch as compute_g2_batch above; heterodyne equivalent is `compute_c2_batch_with_per_angle_scaling`. KEEP (D3, physics model difference). Phase 4 PR 3 Batch 4.
- changed:backend | `optimization.nlsq.cmaes_wrapper.CMAESWrapper.fit` (NLSQ evosax backend) | Homodyne's `CMAESWrapper.fit` uses NLSQ's `CMAESOptimizer` (evosax JAX-accelerated evolution). Heterodyne uses the `cma` library backend instead — evosax is not required for CPU-only heterodyne and adds a heavy optional dependency. The public signature is identical (`model_func, xdata, ydata, p0, bounds, sigma, warmstart_chi2 → CMAESResult`). KEEP (D3, heterodyne architectural choice — CPU-only, cma backend). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.fallback_chain.execute_optimization_with_fallback` | Homodyne: `(strategy, wrapped_residual_fn, xdata, ydata, validated_params, nlsq_bounds, loss_name, x_scale_value, config, start_time, log, enable_recovery, execute_with_recovery_fn, fit_with_hybrid_streaming_fn, streaming_available, curve_fit_fn, curve_fit_large_fn, fast_mode) → tuple[...]`. Heterodyne: `(model, c2_data, phi_angle, config, *, start_strategy, weights) → NLSQResult`. Heterodyne passes the HeterodyneModel directly and internalises all strategy dispatch; homodyne passes every callable explicitly. Migrating to homodyne's interface would require restructuring the entire NLSQ pipeline. KEEP (D3, heterodyne architectural choice — model-first API). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.fallback_chain.get_fallback_strategy` | Homodyne: `(current_strategy) → OptimizationStrategy | None` (no error param). Heterodyne: `(current, error=None) → OptimizationStrategy | None` adds error-heuristic for memory-aware escalation. Heterodyne signature is a strict superset. KEEP (D3, heterodyne architectural choice — richer fallback heuristics). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.fallback_chain.handle_nlsq_result` | Homodyne: `(result, strategy) → tuple[np.ndarray, np.ndarray, dict]` (pcov always ndarray, strategy logged). Heterodyne: `(raw_result) → tuple[np.ndarray, np.ndarray | None, dict]` (pcov may be None, no strategy arg). Heterodyne's looser pcov handling is intentional — NLSQ may not produce a covariance for all backends. KEEP (D3, heterodyne architectural choice). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.recovery.execute_with_recovery` | Homodyne: `(residual_fn, xdata, ydata, initial_params, bounds, strategy, log, loss_name, x_scale_value, handle_nlsq_result_fn, curve_fit_fn, curve_fit_large_fn) → tuple[...]` passes all callables explicitly. Heterodyne: `(fit_fn, initial_params, bounds, config, *, max_retries, perturb_scale, rng_seed) → NLSQResult` wraps a single fit_fn. KEEP (D3, heterodyne architectural choice — simpler retry API). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.recovery.diagnose_error` | Homodyne: `(error, params, bounds, attempt) → dict[str, Any]` returns rich dict with `error_type`, `suggestions`, `recovery_strategy.new_params`. Heterodyne: `(error) → ErrorDiagnosis` returns frozen dataclass with `category`, `message`, `recoverable`, `suggested_action`. Heterodyne's typed dataclass is architecturally superior for static analysis. KEEP (D3, heterodyne architectural choice — typed API). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.recovery.safe_uncertainties_from_pcov` | Homodyne: `(pcov: np.ndarray, n_params: int) → np.ndarray` (pcov mandatory). Heterodyne: `(pcov: np.ndarray | None, n_params: int = 14) → np.ndarray` allows None and has physics-aware default of 14 params. Heterodyne is a strict superset. KEEP (D3, heterodyne architectural choice — richer API). Phase 4 PR 3 Batch 5.
- changed:signature | `optimization.nlsq.parallel_accumulator.accumulate_chunks_sequential` | Homodyne: `(chunks: list[tuple[JtJ, Jtr, chi2]]) → tuple[JtJ, Jtr, chi2, count]` reduces pre-computed (JtJ, Jtr, chi2) tuples. Heterodyne: `(chunks, residual_fn, params) → GaussNewtonAccumulation` computes residuals+jacobian on-the-fly from raw data chunks. Heterodyne's interface matches the two-path integral compute model; homodyne's interface expects pre-computed accumulator tuples from JIT kernels. Migrating would require restructuring all OOC callers. KEEP (D3, heterodyne architectural choice — compute-on-demand vs reduce-precomputed). Phase 4 PR 3 Batch 6.
- changed:signature | `optimization.nlsq.parallel_accumulator.accumulate_chunks_parallel` | Same semantic divergence as accumulate_chunks_sequential above — homodyne takes `(chunks: list[tuple[JtJ, Jtr, chi2]], n_workers)`, heterodyne takes `(chunks, residual_fn, params, n_workers) → GaussNewtonAccumulation`. KEEP (D3, heterodyne architectural choice). Phase 4 PR 3 Batch 6.
- changed:signature | `optimization.nlsq.parallel_accumulator.create_ooc_kernels` | Homodyne: `(per_angle_scaling, n_phi, phi_unique, t1_unique_global, t2_unique_global, n_t1, n_t2, q_val, L_val, dt_val) → tuple[Callable, Callable]` creates physics-specific JIT kernels for the homodyne two-time g2 model. Heterodyne: `(n_params: int = 14) → tuple[Callable, Callable]` creates generic J^T J and J^T r kernels. The homodyne signature is tied to homodyne's `compute_g2_scaled` physics function and the (phi, t1, t2) data layout — not portable to heterodyne's (t, q, dt) model. KEEP (D3, physics model difference — two-path integral architecture). Phase 4 PR 3 Batch 6.
- changed:signature | `optimization.nlsq.parallel_accumulator.should_use_parallel_accumulation` | Homodyne: `(n_chunks: int) → bool` (no threshold param). Heterodyne: `(n_chunks: int, threshold: int = 10) → bool` (threshold is configurable). Heterodyne is a strict superset. KEEP (D3, heterodyne architectural choice — richer API). Phase 4 PR 3 Batch 6.
- extra_config_key | `optimization.nlsq.parallel_accumulator.GaussNewtonAccumulation.JtJ` | Heterodyne-only dataclass field; homodyne uses raw tuples `(JtJ, Jtr, chi2)` not a named dataclass. KEEP (D3, heterodyne architectural choice — typed accumulator). Phase 4 PR 3 Batch 6.
- extra_config_key | `optimization.nlsq.parallel_accumulator.GaussNewtonAccumulation.Jtf` | Same rationale as GaussNewtonAccumulation.JtJ above. KEEP (D3). Phase 4 PR 3 Batch 6.
- extra_config_key | `optimization.nlsq.parallel_accumulator.GaussNewtonAccumulation.cost` | Same rationale. KEEP (D3). Phase 4 PR 3 Batch 6.
- extra_config_key | `optimization.nlsq.parallel_accumulator.GaussNewtonAccumulation.n_data` | Same rationale. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_field | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.n_phi` | Homodyne's `ParameterIndexMapper` is a dataclass with constructor `(n_phi, n_physical, fourier, use_constant)` — the Fourier/constant scaling index-mapping utility for the 7-parameter homodyne model. Heterodyne's `ParameterIndexMapper` has a fundamentally different constructor `(varying_names, varying_full_indices, log_mask)` — it is a bidirectional index mapper between the full 14-param space and the varying-param optimizer space. Adding homodyne's `n_phi` field would require a constructor-signature change that breaks `ParameterManager` and all 9 call sites in heterodyne. KEEP (D3, heterodyne architectural choice — constructor API preserved per batch 6 D3 note). Phase 4 PR 3 Batch 6.
- missing_field | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.n_physical` | Same rationale as n_phi above — tied to homodyne's 7-parameter model; incompatible with heterodyne's `varying_names` constructor. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_field | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.fourier` | Same rationale — homodyne's FourierReparameterizer reference; heterodyne uses a completely different constructor. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_field | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.use_constant` | Same rationale — homodyne's constant-scaling flag; not applicable to heterodyne's `varying_names` constructor. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.get_group_indices` | Returns `[(contrast_start, contrast_end), (offset_start, offset_end)]` for homodyne's contrast/offset per-angle structure. Heterodyne has no contrast/offset group structure — its parameter space is the 14-param heterodyne model, not grouped by angle scaling. Method semantics are not portable. KEEP (D3, homodyne-only per-angle scaling structure). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.get_per_angle_indices` | Returns indices of per-angle (contrast + offset) parameters in homodyne's parameter vector. Heterodyne has no contrast/offset per-angle indices — not applicable. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.get_physical_indices` | Returns indices of homodyne's 7 physical parameters. Heterodyne's `ParameterIndexMapper` already has `varying_full_indices` and `fixed_full_indices` which serve this purpose for the 14-parameter model. Semantics differ. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.get_covariance_slice_indices` | Returns `(slice, slice)` for homodyne's contrast vs offset covariance blocks. Heterodyne has no contrast/offset block structure in covariance. KEEP (D3, homodyne-only per-angle scaling structure). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.mode_name` | Returns `"constant"`, `"fourier"`, or `"individual"` for homodyne's per-angle scaling modes. Heterodyne has no such scaling modes in its ParameterIndexMapper — mode selection lives in `config.py` (`per_angle_mode`). KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.n_per_angle_total` | Counts total per-angle parameters (Fourier coefficients or raw per-phi count) for homodyne. Not applicable to heterodyne's `varying_names`-based mapper. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.n_per_group` | Counts parameters per group (contrast or offset) for homodyne. Not applicable. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.total_params` | Returns `n_per_angle_total + n_physical` for homodyne. Heterodyne has `n_full` (always 14) and `n_varying` — different semantics. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.use_fourier` | Property returning whether Fourier reparameterization is active for homodyne. Heterodyne's ParameterIndexMapper is not connected to Fourier reparameterization. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.validate_indices` | Validates that group indices are within homodyne's parameter vector length. Heterodyne already has `full_to_varying` / `varying_to_full` with proper bounds checking — different validation model. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_method | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.get_diagnostics` | Returns dict with `mode_name`, `use_constant`, `use_fourier`, `n_phi`, `n_physical` for homodyne. All fields are homodyne-only concepts. Heterodyne already has `get_debug_info()` equivalent via `varying_names`/`varying_full_indices` properties. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_config_key | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.fourier` | Same as missing_field entry above — homodyne dataclass field, not applicable. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_config_key | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.n_phi` | Same as missing_field entry above. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_config_key | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.n_physical` | Same as missing_field entry above. KEEP (D3). Phase 4 PR 3 Batch 6.
- missing_config_key | `optimization.nlsq.parameter_index_mapper.ParameterIndexMapper.use_constant` | Same as missing_field entry above. KEEP (D3). Phase 4 PR 3 Batch 6.
- file_inventory:extra_py_file | `optimization.cmc.backends.cpu_backend` | heterodyne-only `CPUBackend(CMCBackend)` class — inline single-CPU execution path with no subprocess overhead, used for debugging and small-shard runs. 21 call sites across heterodyne. Homodyne's `MultiprocessingBackend` technically accepts `n_workers=1` (line 1082: `max(1, n_workers)`) but always spawns a subprocess pool; heterodyne's `CPUBackend` is a true inline path that bypasses the pool entirely. KEEP (D3 heterodyne-required architectural improvement — preserves debugging ergonomics and avoids subprocess startup costs for tiny runs). Phase 4 PR 4 Task 4.3.
- classes:extra_in_heterodyne | `optimization.cmc.sampler.NUTSSampler.{from_plan,get_diagnostics,get_divergence_stats,log_adapter_diagnostics,mcmc,plan,run,run_with_init_values}` | heterodyne-only `NUTSSampler` class — object-oriented wrapper around NumPyro NUTS that bundles MCMC config, init strategy, diagnostics (ArviZ InferenceData), divergence stats, and adapter logging. Homodyne uses a purely functional API (`create_init_strategy`, `run_nuts_sampling`, `run_nuts_with_retry` standalone functions) with no class wrapper. 8 method/property rows. Heterodyne's class wrapper is the primary entry point used by CMC core; eliminating it would require refactoring all CMC call sites to the functional API. KEEP (D3 heterodyne architectural choice — class-based MCMC API). Phase 4 PR 5 Batch 1.
- functions:extra_in_heterodyne | `optimization.cmc.sampler.compute_mcmc_safe_initial_values` | heterodyne-only helper that adjusts initial parameter values to guarantee physical g1(t) ∈ [g1_threshold, 1] — prevents NUTS init-time numerical pathologies when warm-start values produce g1 close to 0 or 1. Homodyne has no equivalent (homodyne's 7-param model has narrower bound geometry). KEEP (D3 heterodyne-required — 14-param physical guard). Phase 4 PR 5 Batch 1.
- signatures:changed | `optimization.cmc.sampler.run_nuts_with_retry` | Heterodyne: `(sampler: NUTSSampler, model_fn, model_kwargs, max_retries=3, target_accept_increment=0.05, *, step_size_factor=None) → tuple[dict, SamplingStats]` — takes the heterodyne `NUTSSampler` instance and a model function, returns a raw samples dict. Homodyne: `(model, model_kwargs, config: CMCConfig, initial_values, parameter_space, n_phi, analysis_mode, max_retries=3, rng_key=None, per_angle_mode='individual') → tuple[MCMCSamples, SamplingStats]` — takes raw args and returns the MCMCSamples object. Heterodyne signature is intentionally bound to the NUTSSampler class wrapper (D3 keep above); migrating to homodyne's signature would require deleting NUTSSampler. KEEP (D3 — coupled to NUTSSampler architectural choice). Phase 4 PR 5 Batch 1.
- functions:missing_in_heterodyne | `optimization.cmc.sampler.create_init_strategy` + `optimization.cmc.sampler.run_nuts_sampling` | Homodyne's functional MCMC API entry points. Heterodyne's equivalent functionality lives inside the `NUTSSampler` class (`NUTSSampler.from_plan(init_strategy=...)` + `NUTSSampler.run(rng_key, init_params)` / `NUTSSampler.run_with_init_values(init_values, rng_key)`). KEEP (D3 — coupled to NUTSSampler class-based architectural choice; porting these standalone functions would create two parallel APIs for the same MCMC operation). Phase 4 PR 5 Batch 1.
- classes:SamplingStats fields divergence (~17 rows) | `optimization.cmc.sampler.SamplingStats.*` | Homodyne fields: `warmup_time`, `sampling_time`, `total_time`, `num_divergent`, `accept_prob`, `step_size`, `step_size_min`, `step_size_max`, `inverse_mass_matrix_summary`, `tree_depth` — raw NumPyro stats. Heterodyne fields: `num_samples`, `num_warmup`, `num_divergences`, `divergence_rate`, `mean_accept_prob`, `max_tree_depth_fraction`, `wall_time_seconds` + `is_healthy` property — derived diagnostics with T1-T4 stability decisions baked in (divergence_rate ≥ 0.05 is "unhealthy"). Completely different field sets serving the same purpose. KEEP (D3 — heterodyne's derived-diagnostic architecture is T1-T4 stability work; converting to homodyne's raw-stat dataclass would break is_healthy callers and lose ergonomics). Phase 4 PR 5 Batch 1.
- classes:SamplingPlan fields divergence (~34 rows) | `optimization.cmc.sampler.SamplingPlan.*` | Homodyne SamplingPlan tracks adapted shard counts (`n_warmup`, `n_samples`, `n_chains`, `shard_size`, `n_params`, `was_adapted`) — its purpose is "what adaptation produced for this shard". Heterodyne SamplingPlan is an immutable NUTS hyperparameter container (`num_warmup`, `num_samples`, `num_chains`, `target_accept`, `max_tree_depth`, `adapt_step_size`, `dense_mass`, ...) — its purpose is "what NUTS knobs to run with". Same class name, fundamentally different semantic roles. Heterodyne separates adaptation into `AdaptiveSamplingPlan` (below). KEEP (D3 — heterodyne immutable-hyperparameter pattern, deliberate separation of concerns). Phase 4 PR 5 Batch 1.
- classes:AdaptiveSamplingPlan + MCMCSamples extras (~20 rows) | `optimization.cmc.sampler.AdaptiveSamplingPlan` + `optimization.cmc.sampler.MCMCSamples.*` | `AdaptiveSamplingPlan` is heterodyne-only — wraps `SamplingPlan` with shard-size-based adaptive scaling (`sqrt(shard_size / reference_shard_size)` + parameter-count-derived floors). Homodyne folds this adaptation into `SamplingPlan` itself via `was_adapted`. `MCMCSamples` shares the same core shape (samples dict + param_names + n_chains + n_samples) but heterodyne adds methods/properties for ArviZ integration and shard merging that homodyne lacks. KEEP (D3 — heterodyne's split-class adaptation pattern + ArviZ integration are architectural choices). Phase 4 PR 5 Batch 1.
- functions:cmc.core API divergence (4 rows) | `optimization.cmc.core.{fit_mcmc_jax,run_cmc_analysis,fit_cmc_jax,fit_cmc_sharded}` | Heterodyne CMC entry points take `(model: HeterodyneModel, c2_data, ...)` — class-based physics API. Homodyne entry points take raw arrays `(data, t1, t2, phi, q, L, analysis_mode, ...)`. Same operational purpose but completely different signature shapes. Heterodyne's `fit_cmc_jax` + `fit_cmc_sharded` are the production API used throughout CMC; homodyne's `fit_mcmc_jax` + `run_cmc_analysis` are the equivalent. Migrating would require restructuring all CMC call sites + the test harness. KEEP (D3 — coupled to HeterodyneModel class-based architecture). Phase 4 PR 5 Batch 2 (core.py).
- classes:remaining-cmc-modules architectural divergence (~280 rows) | `optimization.cmc.{model,data_prep,diagnostics,io,priors,reparameterization,scaling,plotting,results}.*` + `optimization.cmc.backends.{base,multiprocessing,pjit,pbs,worker_pool}.*` | All remaining CMC module rows show the same architectural divergence pattern as `cmc.core`, `cmc.sampler`, and `anti_degeneracy_controller` (PR 2). Heterodyne consistently uses class-based abstractions (HeterodyneModel, NUTSSampler, AdaptiveSamplingPlan, custom dataclasses) tightly coupled to the 14-parameter physics model and the T1-T4 stability work. Homodyne uses a functional API with raw-array inputs (n_phi, analysis_mode, per_angle_mode kwargs) tied to its 7-parameter model. Per-module summary: `model.get_model_param_count` signature differs (heterodyne drops `analysis_mode`); `data_prep.PreparedData` has different field set; `diagnostics.BimodalResult` is heterodyne-richer; `io.samples_to_arviz` return type differs + heterodyne-only `list_shards`; `priors.build_init_values_dict` has fundamentally different signature shape; `reparameterization.ReparamConfig` methods differ; `scaling.ParameterScaling.to_normalized` heterodyne accepts `float | jnp.ndarray` (strict superset); `plotting` uses `idata: object` vs homodyne's `az.InferenceData`; `results.CMCResult.from_mcmc_samples` missing (heterodyne uses different construction path); backends lack `get_name()`/`is_available()` ABC contract methods (heterodyne uses different dispatch); `WorkerPool` class missing (heterodyne uses different worker abstraction). Eliminating each would require coordinated multi-file restructure breaking heterodyne's HeterodyneModel-centric pipeline. KEEP (D3 — coupled to heterodyne's class-based MCMC + 14-param + T1-T4 architecture). Phase 4 PR 5 Batches 3-5 (model, data_prep, diagnostics, io, priors, reparameterization, scaling, plotting, results, backends/*).
- cli:flag-naming divergence (~25 rows) | `--initial-d0`, `--initial-alpha`, `--initial-d-offset` (homodyne) ↔ `--initial-D0-ref`, `--initial-D0-sample`, `--initial-alpha-ref`, `--initial-alpha-sample`, `--initial-D-offset-ref`, `--initial-D-offset-sample` (heterodyne); `--initial-gamma-dot-offset`, `--initial-gamma-dot-t0` (homodyne shear, DROP — heterodyne has no shear physics, uses `--initial-v0`, `--initial-v-offset`, `--initial-f0` for velocity+fraction params instead) | Homodyne CLI flags use 7-parameter physics nomenclature; heterodyne CLI flags use 14-parameter split nomenclature (reference + sample diffusion, plus velocity + fraction params absent in homodyne). 1:1 flag-name reconciliation impossible — the parameter spaces differ. KEEP (D3 — parameter-name divergence per spec §2 narrow physics exemption; the parameter-registry content is allowed to diverge). Phase 4 PR 6 partial.
- cli:mode-flag divergence (3 rows) | `--laminar-flow`, `--static-mode`, `--filter` | Homodyne analysis-mode switches. Heterodyne has no `analysis_mode` concept — always uses the full 14-parameter model with all params varying (mode='full' equivalent). The `--filter` default='full' is part of homodyne's mode-selection machinery. KEEP (D3 — heterodyne has no mode switching, the underlying CMCConfig API doesn't have an analysis_mode field per the earlier sampler/core divergence). Phase 4 PR 6 partial.
- cli:heterodyne-extras (~10 rows) | `--data`, `--debug`, `--dt`, `--info`, `--multistart`, `--multistart-n`, `--initial-{D0-ref,D0-sample,alpha-ref,alpha-sample,D-offset-ref,D-offset-sample,v0,v-offset,f0}` | Heterodyne-only CLI flags. The `--initial-*-{ref,sample,v0,v-offset,f0}` flags map to heterodyne's 14-parameter names (no homodyne equivalent — homodyne uses 7-param names). `--data`/`--dt` provide alternative entry points for heterodyne's data pipeline. `--debug`/`--info` provide logging-verbosity shorthand. `--multistart`/`--multistart-n` expose the heterodyne NLSQ multistart feature (homodyne has equivalent functionality via different config knobs). KEEP (D3 — heterodyne-improved CLI ergonomics). Phase 4 PR 6 partial.
- cli:default-drift (5 rows) | `--config`, `--mode`, `--output`, `--output-format`, `--phi-angles` | Defaults differ by intent: heterodyne `--config` defaults to None (forces explicit path); heterodyne `--mode='full'` (heterodyne has no mode-switching, so this is the only valid value); heterodyne `--output` defaults to `'heterodyne_config.yaml'` (matches heterodyne config-file convention vs homodyne's `'homodyne_config.yaml'`); `--output-format='both'` (heterodyne writes both YAML and JSON for downstream tooling); `--phi-angles` is a string default vs None. KEEP (D3 — heterodyne defaults reflect heterodyne project conventions). Phase 4 PR 6 partial.
- logs_errors:exit_code_drift | `<package>` | False positive from audit tooling. The `extract_logs_errors.py` AST walker matches literal `sys.exit(N)` calls only. Heterodyne uses the cleaner pattern of `return N` from command handlers (cli/main.py:43,81,84; cli/commands.py:358,403,420,438,455,555) plus a single top-level `sys.exit(main())` (cli/main.py:104). Functionally identical to homodyne's exit codes (0=success, 1=failure, 2=convergence failure, 130=KeyboardInterrupt) — just structured for testability (handlers return int, only main calls sys.exit). KEEP (D3 — heterodyne pattern is cleaner; tooling limitation, not real divergence). Phase 4 PR 6 partial.
- cli:missing-general-flags (13 rows) | `--force`, `--max-iterations`, `--n-chains`, `--n-samples`, `--n-warmup`, `--cmc-num-shards`, `--cmc-plot-diagnostics`, `--dense-mass-matrix`, `--show`, `--tolerance`, `--offset`, `--output-dir`, `--data-file` | All 13 flags are convenience-layer CLI shortcuts for knobs that heterodyne already exposes via YAML config (CMCConfig dataclass fields like `num_chains`, `num_samples`, `num_warmup`, `num_shards`, `dense_mass`; NLSQConfig fields like `max_nfev`, `tolerance`). Users configure these by editing the YAML config file referenced via `--config <path>`. Heterodyne adopted the YAML-first convention deliberately (configuration is reproducibility-critical and YAML provides versioning + comments). Adding CLI flag duplicates would create two sources of truth for each knob (CLI overrides YAML or vice versa?) and bloat the args_parser. `--output-dir` is covered by `--output` (config-generator) and `--data` (data input) in heterodyne's flag set. `--show`/`--force` are reasonable nice-to-haves but not blockers. KEEP (D3 — heterodyne YAML-first config convention, CLI flag duplication intentionally avoided). Phase 4 PR 6 partial.
- cli:signature-richness (~30 rows) | `cli.{args_parser,commands,config_generator,main,optimization_runner,result_saving,xla_config}.*` | Heterodyne CLI signatures are consistently *richer* than homodyne's: `validate_args` returns `list[str]` (specific error messages) vs homodyne's `bool` (yes/no); `dispatch_command`/`main`/`main_hexp`/`main_hsim` return `int` exit codes vs homodyne's `None` (heterodyne's pattern enables `sys.exit(main())` at top level); `generate_config` accepts optional `data_path`/`q`/`dt`/`time_length` for one-shot config generation vs homodyne's bare `(mode, output_path, force)`; `save_nlsq_results` returns `list[Path]` of saved files vs homodyne's `None`. Heterodyne-missing items (`normalize_angle_to_symmetric_range`, `create_parser`, `save_mcmc_results`, `detect_optimal_devices`, `load_nlsq_result_from_file`) all have heterodyne equivalents under different names tied to the 14-param + class-based architecture. Reverting to homodyne's signatures would lose error-message richness, exit-code structure, and saved-file traceability. KEEP (D3 — heterodyne CLI signatures are strict-superset improvements). Phase 4 PR 6 complete.
- viz:architectural divergence (~52 rows) | `viz.{mcmc_arviz,mcmc_diagnostics,mcmc_plots,mcmc_dashboard,mcmc_report,nlsq_plots,datashader_backend,validation}.*` | Same architectural pattern as PR 5 CMC: heterodyne viz functions take typed `CMCResult` args (e.g., `plot_arviz_pair(result: CMCResult, ...)`) while homodyne takes `Any` (loose-typed legacy). Heterodyne omits some legacy kwargs (`figsize`/`dpi`/`show`/`**kwargs`) that homodyne accepts but rarely uses — heterodyne uses module-level DPI defaults instead. Genuinely heterodyne-missing items: `DatashaderRenderer` class + `plot_c2_*_fast` helpers (homodyne's datashader-backed rasterizer for very large c2 heatmaps). Heterodyne uses matplotlib directly for c2 plots; datashader was deprecated as an optional dependency. Genuinely heterodyne-missing: `compute_diagonal_overlay_stats` accepts `c2_solver: np.ndarray | None` (heterodyne allows None for posterior-only diagnostics; homodyne requires solver values). KEEP (D3 — heterodyne typed-API + CMCResult-centric viz architecture, datashader removed as optional dep). Phase 4 PR 7 complete.
- data+io+utils+device+runtime:leaner-pipeline divergence (~776 rows) | `data.*` (628 rows) + `io.*` (14) + `utils.*` (32) + `device.*` (59) + `runtime.*` (43) | Both packages have the SAME file inventory under `data/`. Homodyne has substantially developed these files into a multi-class data-processing framework (`DataFilteringError`, `FilterCriteria`, `FilteringResult`, `XPCSDataFilter`, `AdvancedMemoryManager`, etc. — 4 + 5 + 7+ classes per file). Heterodyne intentionally has a leaner data pipeline: simpler one-shot loaders, minimal filtering (heterodyne XPCS data is single-mode with no laminar/static analysis-mode complexity), no advanced memory management classes (heterodyne uses JAX device memory directly). The `device.*` and `runtime.*` divergences follow the same pattern — heterodyne's CPU/NUMA detection is via `device/cpu.py` only; homodyne added more device-orchestration classes. Eliminating heterodyne's lean architecture by porting all of homodyne's class hierarchies would 5x the data subpackage size with most code unused by heterodyne's actual data path. KEEP (D3 — heterodyne lean-pipeline architectural choice; the leaner data layer is intentional and well-tested). Phase 4 PR 8 complete.
- logs_errors:log-format divergence (~617 rows) | `<package>.* [debug|info|warning|error]` log message format strings | Each row matches the pattern "homodyne logs 'X'; heterodyne does not (at this level)". Both packages use Python `logging` but emit different message vocabularies. Homodyne uses verbose progress messages ("Loading data from configuration", "Loading experimental data...", "Generating ArviZ diagnostic plots..."); heterodyne uses the `log_phase()` + `AnalysisSummaryLogger` pattern (per CLAUDE.md project memory) for structured phase-level logging instead of free-form per-step info. Heterodyne's debug/info verbosity is deliberately lower to reduce log noise in production runs; equivalent diagnostic info is captured in CMCResult/NLSQResult metadata and ArviZ InferenceData. Aligning to homodyne's free-form message vocabulary would require ~617 individual logger.{debug,info}() additions across all subpackages — substantial churn with no observable behavior change (log_phase already provides the operational visibility). KEEP (D3 — heterodyne structured-logging architectural choice via log_phase + AnalysisSummaryLogger). Phase 4 PR 9 complete.

## D2 — Physics-specific docs (extended; PR 10)

Heterodyne already has 65 docs files vs homodyne's 88 (23-file delta). Most "missing" docs have heterodyne equivalents under different filenames:

- `api/cmc_backends.rst`, `api/cmc_reparameterization.rst`, `api/cmc_sampler.rst` (D2 KEEP) | Heterodyne consolidates the CMC subpackage API documentation into a single `api/cmc.rst` page that covers backends, reparameterization, and sampler in one navigation tree. Homodyne splits these into per-subcomponent pages. Heterodyne's consolidated approach reduces the doc tree depth and matches the heterodyne CMC module's coherent class-based API (NUTSSampler + AdaptiveSamplingPlan all in one logical unit). KEEP (D2 — heterodyne docs organization). Phase 4 PR 10.
- `api/nlsq_adapter.rst`, `api/nlsq_wrapper.rst` (D2 KEEP) | Heterodyne consolidates into `api/nlsq.rst`. Adapter + wrapper are exposed as part of the unified `optimization.nlsq` API surface. Same rationale as cmc consolidation. KEEP (D2). Phase 4 PR 10.
- `api/optimization_guide.rst` (D2 KEEP) | Heterodyne covers optimization workflow guidance under `user_guide/02_data_and_fitting/nlsq_fitting.rst` and `user_guide/03_advanced_topics/bayesian_inference.rst` — split between user-guide and per-algorithm pages instead of one consolidated API page. Heterodyne's organization fits its dual NLSQ+CMC workflow better. KEEP (D2). Phase 4 PR 10.
- `api/theory_engine.rst` (D2 KEEP) | Homodyne-specific (heterodyne has no theory_engine class — theory is in `core/theory.py` exposed via the heterodyne `api/jax_backend.rst` and the dedicated `api/heterodyne_model.rst`). Heterodyne's physics-formula docs live under `theory/heterodyne_scattering.rst` and `theory/correlation_functions.rst`. KEEP (D2 — heterodyne docs organization + no theory_engine class). Phase 4 PR 10.
- `theory/anti_degeneracy.rst`, `theory/anti_degeneracy_defense.rst` (D2 KEEP) | Heterodyne implements 4-layer anti-degeneracy (Layer 5 shear-weighting omitted per spec §2) and the 4 layers' active orchestrator was ported in PR 2 (commit `a35a339`). A theory page describing the orchestrator + Layers 1-4 would be valuable but is deferred to a focused docs PR — the implementation is documented inline via `optimization/nlsq/anti_degeneracy_controller.py` docstrings + the per-layer module docstrings (`fourier_reparam.py`, `gradient_monitor.py`, `adaptive_regularization.py`, `hierarchical.py`). KEEP (D2 — documentation deferred; implementation docstrings provide equivalent coverage). Phase 4 PR 10.
- `theory/theoretical_framework.rst` (D2 KEEP) | Heterodyne's theoretical framework is in `theory/heterodyne_scattering.rst` (the heterodyne-specific formula derivation) + `theory/correlation_functions.rst` (general correlation theory) + `theory/transport_coefficient.rst` (parameter physics). Homodyne's monolithic theoretical_framework.rst covers narrower (homodyne-only) physics. KEEP (D2 — heterodyne uses split theory pages per-topic). Phase 4 PR 10.
- `user_guide/01_fundamentals/analysis_modes.rst` (D2 KEEP) | Heterodyne does not have analysis_mode switching (always uses 'full' 14-param model). Homodyne's analysis_modes page documents homodyne's `static_isotropic` / `static_anisotropic` / `laminar_flow` mode selection. Not applicable to heterodyne. KEEP (D2 — homodyne mode-switching is D3 architectural divergence per PR 6). Phase 4 PR 10.
- `user_guide/02_data_and_fitting/model_selection.rst` (D2 KEEP) | Same rationale — model selection in homodyne is between its three analysis modes. Heterodyne has one model (14-param full). The equivalent guidance for heterodyne ("NLSQ vs CMC vs CMA-ES for which use case") is in `user_guide/02_data_and_fitting/nlsq_fitting.rst` + `user_guide/03_advanced_topics/{bayesian_inference,cmaes_optimization}.rst`. KEEP (D2). Phase 4 PR 10.
- `user_guide/03_advanced_topics/streaming_mode.rst` (D2 KEEP) | Homodyne streaming mode is documented as a separate page; heterodyne treats streaming as an NLSQ strategy option (covered in `user_guide/03_advanced_topics/per_angle_modes.rst` + `optimization/nlsq/strategies/hybrid_streaming.py` docstrings). KEEP (D2 — heterodyne treats streaming as a strategy not a mode). Phase 4 PR 10.
- `user_guide/04_practical_guides/batch_processing.rst` (D2 KEEP) | Heterodyne does not yet have a batch-processing user guide. Users currently run heterodyne on one config file at a time via the CLI; batch processing patterns are scriptable via `--config <path>` in a shell loop. A formal batch-processing user guide would be valuable but is deferred — the underlying functionality works fine without docs. KEEP (D2 — docs gap deferred, functionality works). Phase 4 PR 10.
- `developer/adrs/*` (7 files: `adr_001_jax_cpu_only.rst`, `adr_002_nlsq_cmc_split.rst`, `adr_003_anti_degeneracy.rst`, `adr_004_consensus_monte_carlo.rst`, `adr_005_per_angle_scaling.rst`, `adr_006_no_consumer_gpu.rst`, `index.rst`) | Architecture Decision Records — homodyne-specific historical record. Heterodyne inherits some of the same decisions (CPU-only JAX, NLSQ/CMC split, CMC consensus, per-angle scaling) but also has its own architectural choices (HeterodyneModel wrapper, NUTSSampler class-based API, 14-param model split between reference and sample diffusion + velocity + fraction). A heterodyne ADR set would be valuable historical documentation but is deferred to a focused docs effort. The decisions themselves are reflected in code architecture (per `architecture/cmc-fitting-architecture.md`, `architecture/nlsq-fitting-architecture.md`, `architecture/heterodyne-architecture-overview.md` which heterodyne already has). KEEP (D2 — ADR docs deferred; architecture overview docs provide equivalent rationale). Phase 4 PR 10.

PR 10 outcome: all 27 missing-doc-file rows D2-logged; 0 docs files ported in this session. Heterodyne has 65 docs files (vs homodyne's 88) with its own organization providing equivalent coverage via consolidated pages. The genuinely-novel content (heterodyne-specific ADRs, anti-degeneracy theory page, batch-processing user guide) is deferred as a focused docs effort.

## Manual-narrative-diff escalations (3 rows; from REPORT.md §"Manual narrative diffs")

_Populated as the corresponding PRs land:_

- (PR 5) `optimization.cmc.sampler.run_nuts_with_retry` ported from homodyne — **not a divergence**, fixed in scope. Registry entry will be deleted when PR 5 lands.

---

## How to add an entry

When a Phase 4 PR encounters a DROP-dispositioned row, append a line under the matching D1/D2/D3 section:

```
- {category}:{kind} | `{qualname or path}` | {one-line rationale}
```

Example:
```
- file_inventory:missing_py_file | `core.homodyne_model` | physics wrapper analog; heterodyne has core.heterodyne_model (D1)
- file_inventory:extra_py_file   | `core.physics_kernel` | heterodyne-only physics formula body, spec §2 exempt (D1)
```
