Skip to content

0.7.0 — 2026-07-19

← Back to the changelog index

Fixed

  • Distorted-camera pose was silently wrong (correctness). The production single-tag pose path (the weighted LM refine_pose_lm_weighted) and the three board pose solvers minimized a pinhole reprojection error even when the CameraIntrinsics carried a Brown–Conrady / Kannala–Brandt distortion model — only the image-less corner-LM fallback was distortion-aware. On a distorted camera this biased every emitted pose (in a synthetic Brown–Conrady test the weighted LM converged 8.6° off ground truth; the distortion-aware fix recovers it exactly). Both the normal-equations builder and the Nielsen step-evaluation cost (huber_mahalanobis_cost, which was still pinhole and made the gain ratio inconsistent) now route through the shared distortion-aware pose::projection_and_gradient. The board path is made distortion-consistent end-to-end for the same reason (its three LM sites, RANSAC branch scoring, inlier evaluation, and per-observation d² all reprojected pinhole; all now use distort_normalized, and the DLT/IPPE seed now undistorts observations to ideal normalized coords before homography fitting). Pose::project is likewise made distortion-aware (it was pinhole despite its doc, and biased the detector's reprojection-error telemetry on distorted cameras). Undistorted (DistortionCoeffs::None) results are unchanged — all pinhole regression snapshots (render-tag, ICRA, board-hub, ROC, distortion-hub) pass byte-for-byte (the projection differs from the removed pinhole arithmetic only by floating-point reassociation of the pixel scaling, which moves no fixture). Covered by weighted_lm_minimises_distorted_reprojection (the regression_distortion_hub suite only exercised routing/decode, never pose accuracy).
  • Board pose LM convergence (accuracy). The three board.rs pose solvers previously paired a camera-frame (Convention-1) rotation Jacobian with a non-rotated translation update (Convention-2), an inconsistency that let the LM take suboptimal steps and stop short of the optimum. The right-perturbation migration makes Jacobian and update consistent, substantially improving board rotation accuracy on the hub golden set — e.g. ChArUco board rotation error mean 0.107°→0.053° (−51%) and p99 0.651°→0.218° (−67%); AprilGrid mean 0.073°→0.037° (−50%) — with board translation neutral and no new singular covariances. Board pose snapshots re-baselined accordingly.
  • refine_pose_lm rotation-Jacobian sign bug. The unweighted corner-pose LM built its rotation Jacobian rows with the wrong sign (the negative of the canonical ∂proj/∂ω used by the weighted LM and projection_jacobian), making Jᵀr an ascent direction for rotation. Its trust region therefore rejected every rotation step, so the solver refined only translation and returned IPPE's rotation unrefined. This path is reachable only via the image-less estimate_tag_pose(corners, None) API — the full detection pipeline always supplies an image and so uses the weighted (already-correct) LM, which is why the bug never affected pipeline detection and shipped undetected. Fixed to the canonical sign; the corner normal-equations assembly was extracted to a shared corner_normal_equations with a committed finite-difference gradient check (corner_normal_equations_gradient_is_descent) and a rotation-recovery test. Behavior change for API consumers: callers of the image-less estimate_tag_pose(corners, None) / estimate_tag_pose_with_diagnostics(..) path now receive a rotation-refined pose (previously the returned rotation was IPPE's, unrefined); translation and every image-backed pipeline result are unchanged. The shipped-profile detection snapshots (render-tag, ICRA, board-hub) and all 253 default-feature tests are byte-identical. One bench-internals-gated regression snapshot does move as the direct, intended consequence of the fix: regression_pose_consistency_roc (synthetic-isotropic) — with rotation now actually refined on the fallback path, the χ²(2) pose-consistency gate's realized false-positive rate tracks the modeled FPR far more closely (e.g. 0.169 → 0.101 at a modeled 0.1), so the recorded snapshot was regenerated to the better-calibrated values.

Changed

  • high_accuracy profile now ships with model-edge pose refinement on. The high_accuracy profile sets pose.pose_edge_refinement_enabled = true, turning the opt-in stage (added below) on by default for the metrology profile. On the render-tag suite (Accurate pose, intrinsics + tag_size) this tightens the whole rotation distribution — p99 −47 % to −76 % at every resolution (1080p 0.561° → 0.290°, 2160p 1.387° → 0.277° on the regression fixtures) — and reprojection RMSE improves at every resolution (mean −13 % to −22 %), with recall, precision and 2D corner RMSE unchanged (the stage only refines an already-accepted pose; it never writes batch.corners) at ~+1 ms/frame. The win is larger still on the degraded-imagery robustness sets (rot p99 −56 % to −84 %). The regression_render_tag accuracy_baseline snapshots are re-baselined accordingly; board-hub and ICRA snapshots are byte-identical (the stage is confined to the single-tag Accurate pose path). standard and grid stay off — on standard's corner baseline the stage is a bulk win but does not fix its catastrophic rotation-p99 tail and slightly regresses the high-resolution reproj/translation tail, because standard lacks the pose-consistency χ² gate that re-validates each refined pose (it would trade the render-tag tail); see docs/engineering/benchmarking/model_edge_refinement_20260715.md.

  • Pose LM convergence: unit-sound function-tolerance gate. The shared nielsen_lm secondary convergence gate is now a relative-cost-reduction test ((cost_prev − cost_new) / cost_prev < func_tol, func_tol = 1e-6, the Ceres function_tolerance default) in place of the mixed-unit ‖δ‖ < 1e-7 step gate. The old gate summed translation-m² and rotation-rad² into one scalar — a dimensionally-unsound quantity whose binding point drifts with the arbitrary choice of length unit. Two committed diagnostics quantify the change: step_gate_unit_rescale_invariance poses the identical pixel problem across a 10⁴× translation-scale range and shows the old gate drifts 15→20 mean iterations (and on far/small tags fails to bind, running to max_iters), while func_tol is exactly scale-invariant at ~7.8 iterations; step_gate_board_battery asserts the new gate reaches the converged rotation to <5e-3° on a 36-point board. The switch is accuracy-neutral: every gate — old, new, and a fully-converged reference — yields identical rotation p99 (the reprojection objective is flat in rotation long before either gate binds, so the earlier "loosening risks the rotation tail" concern was empirically falsified — the old gate's real cost was unbounded, unit-dependent iteration counts on the AV long-range regime). Single-tag snapshots (render-tag, ICRA, ROC) stay byte-identical; the board hub snapshots move only at the convergence-point-noise floor (mixed-sign, sub-1 % on p99 aggregates, shipped high_accuracy ChArUco rotation essentially unchanged) and are re-baselined. grad_tol remains the primary stationarity gate.

  • Shared Nielsen trust-region LM core. The three pose LM loops (corner unweighted/weighted and the board damped LM) are unified onto one pose::nielsen_lm core parameterised by a NielsenConfig. This removes three hand-rolled λ/ν loops and fixes a latent board inconsistency (it previously used a plain λδ predicted reduction that did not match its own Marquardt damping); the canonical form uses D = max(diag(JᵀWJ), floor) for both the damping and the Madsen gain-ratio predicted reduction. The weighted corner LM and the board LM also now share one mahalanobis_huber robust kernel and one MAHALANOBIS_HUBER_K constant (previously a third hand-copy of the Huber math + a duplicated literal). Single-tag results (render-tag, ICRA, ROC) are byte-identical; the board snapshots move only at the FP-reorder floor (max 8.5e-7 relative, net neutral / slightly better medians) and are re-baselined. A nielsen_config_tuning_sweep diagnostic evidences the constant choices (the step gate was revisited in the function-tolerance entry above).
  • Pose LMs migrated to right (body-frame) SE(3) perturbation. All four pose solvers — pose::refine_pose_lm (unweighted corner), pose_weighted (weighted production), and the three board.rs LM sites — now parameterise the update as T' = T·exp(δ) (Pose::retract) with the matching body-frame Jacobian, unified onto shared accumulators. Because the object points are symmetric about the body origin this decouples rotation from translation in JᵀWJ (~3.4× better conditioning; rot↔translation coupling ~0.999→0.06) and converges in fewer iterations. Breaking: the emitted 6×6 pose covariance is now expressed in the body frame (previously camera/left). It remains ordered [t, ω]; consumers that need the old camera-frame covariance can convert it with the new Pose::covariance_body_to_camera. Single-tag accuracy is unchanged — render-tag and ICRA regression snapshots hold within bounds; see docs/explanation/coordinates.md §5.1. Supersedes the earlier "Pose::retract extracted (left-perturbation)" note below.
  • Pose::retract. Extracted the SE(3) left-perturbation update exp([t|ω])·pose into one method, replacing four inline copies across the pose LMs (byte-identical). First step of the LM-consolidation series, completed by the body-frame perturbation, distortion-aware, shared-Nielsen-core, and function-tolerance-gate changes above.
  • Unweighted corner LM cost caching. corner_normal_equations now returns the Huber cost at the linearization point alongside (JᵀWJ, JᵀWr) (bit-identical to a standalone huber_cost), and refine_pose_lm adopts the weighted LM's needs_rebuild caching so it re-projects only after an accepted step instead of every iteration. Pure efficiency/consistency refactor — byte-identical results, and brings the unweighted loop structurally in line with pose_weighted ahead of the shared-core phase.

Added

  • Model-based edge pose refinement (opt-in, Accurate mode). A new post-decode stage (pose.pose_edge_refinement_enabled, default off) aligns a decoded tag's ~40 internal bit-grid edges + border to the image and refines the 6-DoF pose against them (rotation from the distributed interior edges; translation re-anchored to the 4 corners). It adds information — the decoded interior — rather than reshaping the 4 corners, so unlike the trade-bound corner-level levers in the 2026-07-14 study it improves rotation without giving up Locus's best-in-class translation. The stage rides the shared body-frame Nielsen trust-region core (nielsen_lm + BodyFrameNormalEquations): each 1-D edge-normal residual enters the same distortion-aware accumulator as a corner residual weighted by the rank-1 projector n·nᵀ, under a robust Huber loss so the clean interior edges (not a few latched/biased ones) control the rotation. Edge scans that leave the image or project behind the camera are rejected/penalised, and the refined pose is re-validated against the pose-consistency χ² gate so it can never weaken false-positive suppression (recall and precision stay 100 %/100 %; mean corner RMSE is untouched — the stage only refines the pose). On render-tag it tightens the whole rotation distribution — mean −39…−55 %, p99 −47…−76 % at every resolution (1080p 0.600° → 0.249°, p95 0.385° → 0.180° — well under OpenCV apriltag's 0.376°) — keeping translation best-in-class in mean and p99, against a small bounded p95 trade at high resolution (the gated edges→rotation / corners→translation split). Reprojection RMSE improves at every resolution (mean −13…−22 %), confirming a genuine image-space accuracy gain; 2D corner RMSE is unchanged. +~1 ms/frame. Requires camera intrinsics + tag_size; shipped profiles leave it off, so detection snapshots stay byte-identical. See docs/engineering/benchmarking/model_edge_refinement_20260715.md and tools/bench/model_edge_eval.py.
  • Pose::adjoint + SE(3) covariance reframing. Pose::adjoint returns the 6×6 SE(3) adjoint ([t, ω] ordering); Pose::covariance_camera_to_body and Pose::covariance_body_to_camera convert a pose covariance between the camera (left) and body (right) tangent frames exactly (Σ_cam = Ad_T Σ_body Ad_Tᵀ). Plus Pose::inverse. Validated by adjoint_relates_body_and_camera_tangents and covariance_reframe_roundtrips.
  • Corner-refinement variant benchmark + rotation-tail study. New tools/bench/refine_variants_eval.py runs the shipped high_accuracy detector with only its large-marker refinement route swapped (config-only; shipped profiles and snapshots untouched). The 2026-07-14 study (docs/engineering/benchmarking/refine_variants_20260714.md) shows apriltag-style edge-line refit (GWLF) reaches the rotation p99 target but regresses translation 3.4× (not a Pareto win), and that the residual rotation-p99 gap is 2–3 EdLines arc-partition failures rather than distributed refinement noise. No library behavior change — the study's EdgeLineGated prototype was reverted.
  • Config field-set parity tripwire. A locus-core unit test (config::schema_parity_tests) now asserts the serde profile shim (ProfileJson + nested *Json structs) exposes the exact same JSON key set as the referee schemas/profile.schema.json. Because the schema is already CI-locked to the Pydantic model, this transitively pins Rust ↔ Python config field-set agreement — the one seam none of the value-oriented tripwires (profile_loading.rs, test_profiles.py, the schema-diff job) asserted. Adding/removing a knob on only one side is now a loud red build. The *Json shim structs gained Serialize (and ProfileJson a derived Default) to support the reflection; no behavior change.
  • Generated locus/locus.pyi type stub + drift gate. The hand-maintained stub (whose silent drift motivated this pass) is now generated from the annotated pyo3 surface via pyo3-stub-gen: cargo run --bin stub_gen (re)writes it, and a CI step (stub_gen --check, in the Profile Schema & Stub Parity job) fails on drift. Together with the field-set tripwire and the schema-diff job this closes the loop Rust ↔ Pydantic ↔ schema ↔ .pyi. pyo3-stub-gen is an optional dependency behind a non-default stub-gen feature, so the shipped wheel and the cargo deny graph never pull it (or its parser / unmaintained-unicode transitive deps) — the production build is byte-identical to before, as the #[gen_stub_*] annotations compile out under default features. pyo3's extension-module became a toggleable default feature so the generator binary can link libpython and run standalone.

Changed

  • Detector config crosses the Rust↔Python FFI as JSON, not a 40-field struct. The Pydantic DetectorConfig is now shipped to Rust as its model_dump_json() string (parsed by DetectorConfig::from_profile_json) and read back the same way (DetectorConfig::to_profile_json), making the shipped JSON profile format the single contract on both sides of the boundary. This deletes the PyDetectorConfig pyclass, its 40-argument constructor, both From impls, Python's _to_ffi_config, the quad.extraction_policy enum-flattening discriminator hack, and the hand-written .pyi block — ~300 lines of pure transcription, and the FFI seam that had no runtime coverage. Behavior-neutral: render-tag / ICRA snapshots byte-identical. PyDetectorConfig is removed from the locus.locus extension surface (internal FFI glue; it was never in __all__).
  • OpenCV benchmark baseline bumped to 5.0.0 and re-tuned. Bumped opencv-python-headless 4.13 → 5.0.0 (fixing a detectMarkers ids-shape change in the comparison adapter) and re-tuned the cv2.aruco baseline for its best 5.0 config. The reported OpenCV numbers now show two operating points: fast subpix (tuned detection thresholds: render-tag trans p99 141 → 66.6 mm for free) and best-accuracy apriltag refinement (55.3 mm / best rotation tail, ~2× slower). apriltag is now the shipped OpenCVWrapper._DEFAULTS. On ICRA its recall rises to 52.6 % (from 33.2 %) with the recall-tuned subpix config. All render-tag latencies re-measured single-threaded on verified hardware. See docs/engineering/benchmarking/render_tag_sota_20260713.md; the OpenCV version is now captured in benchmark provenance.

Removed

  • Dead / unused detector config knobs. Pruned three knobs across all representations (flat Rust DetectorConfig, serde profile shim, Pydantic model, PyO3 FFI, .pyi stub, JSON schema):
  • threshold.enable_adaptive_window — plumbed through every layer but read by no algorithm code (genuinely dead). Removed from the 3 shipped profiles and the test fixtures that carried it.
  • quad.edlines_phase3_erf — set by no shipped profile (parabolic-vertex is the only production path). Removing the knob let us delete the dead per-micro-ray ERF branch in edlines.rs, the fit_erf_step_lm kernel and its unit tests, and the enum's dual-format legacy bool/string deserializer.
  • extended-tests Cargo feature — redundant with extended-bench; collapsed.

Breaking (Python config API): the threshold.enable_adaptive_window and quad.edlines_phase3_erf fields and the EdLinesPhase3ErfPolicy enum are gone. No shipped profile relied on either, so detection output is unchanged (render-tag / ICRA snapshots byte-identical). - max_recall_adaptive profile (shipped presets 4 → 3). The opt-in "Phase 1 sentinel" carried a placeholder PPB threshold, minimal coverage, and was excluded from the SOTA eval; the shipped set is now standard, grid, high_accuracy — matching what the docs already described. Breaking (Python config API): "max_recall_adaptive" is no longer a valid profile= / DetectorConfig.from_profile() argument. The AdaptivePpb machinery it shared with high_accuracy is untouched; its distortion-routing regression test was retargeted to build the policy inline so that dispatch coverage survives. - Phase C.5 post-decode corner re-refit (post_decode_refinement). Removed its only consumer (max_recall_adaptive), so the feature — the post_decode_refinement.rs module, the decoder.post_decode_refinement config field across all representations, and the now-orphaned RefineConfig::post_decode_style / ErfEdgeFitter::line_jtj helpers — is deleted. Breaking (Python config API): the decoder.post_decode_refinement field is gone. No shipped profile set it, so detection output is unchanged. - Phase 0 rotation-tail diagnostic harness (bench-internals-only, always marked throwaway). Its stated revert precondition — the Phase 0 memo landing — is met, and the single-frame rotation line of inquiry is closed. Deleted the dual-branch LM / leave-one-out refit / per-iteration IRLS telemetry entry points in locus-core (pose.rs, pose_weighted.rs), the mod bench FFI surface and locus.bench Python sub-module in locus-py, and the tools/bench/rotation_tail_diag/ + tools/bench/pose_cov_audit.py tooling built on it. The permanent foundation is untouched: the bench-internals feature itself, compute_image_noise_floor, the branch_chosen field on PoseDiagnostics, bench_compute_corner_covariance, and the shipped pose-consistency / outlier-drop telemetry columns on DetectionBatch.

Fixed

  • Detector.config() now round-trips every field. The Python readback previously reconstructed only ~29 of 39 config fields by hand, silently substituting Pydantic defaults for the pose_consistency_* / outlier_drop_d2_threshold knobs and the adaptive quad.extraction_policy on any detector whose config set them. config() now reparses Rust's serialized effective config (see the JSON-boundary change above), so the round-trip is total. Covered by the rewritten test_profiles.py full-config comparison.
  • pose_consistency_min_decisive_ratio = math.inf survives the JSON FFI boundary. The documented f64::INFINITY value (which disables the χ² escape clause) has no JSON number form; it now round-trips as null in both directions — Rust's serde shim maps ∞↔null and Pydantic parses null back to math.inf — instead of raising ValueError at Detector construction. Other non-finite handling is unchanged.

Added

  • Per-instance comparative benchmarking + visualization (tools/bench/compare/, Python-only). Compares locus-tag against tuned OpenCV ArUco / pupil_apriltags image-by-image and per tag instance to find which library wins on which strata and which specific images are Locus's improvement levers. New CLI:
  • bench compare-instances — end-to-end: runs four series (locus:tuned, locus:shipped, opencv_aruco:tuned, apriltag:tuned) from a bench tune pareto/ dir across the render-tag datasets → per-instance report → rerun deep-dive. Split into bench compare-generate (the combined Tier-1 parquet) and bench compare-report-instances for iteration.
  • Analysis is polars-native (compare/analysis.py): per-instance winners, per-stratum win-rates, and a worst-Locus lever table. Figures are matplotlib→SVG (plots/{winrate_heatmap,error_ecdf,error_violin, paired_scatter,delta_hist}.py) with a two-section HTML bundle + a docs markdown report. A rerun deep-dive (compare/deepdive.py) overlays each library's corners (separate, toggleable entity paths) + GT + per-library error scalars on a scrubbable timeline over the worst-Locus cases.
  • Reuses the tuning wrappers (from_params), Collector/records, pareto/*.json and stratification; the tune executor's frame loop was extracted to drive_cell + run_search_records (same crash-isolation) rather than duplicated.
  • Fixed a cross-library measurement bug: pupil_apriltags labels corners in a different order than the GT/Locus/OpenCV convention (a fixed [1,0,3,2] relabel, verified sub-pixel), so the collector's corner RMSE was ~200 px for apriltag and incomparable. AprilTagWrapper now normalises corners with that fixed adapter; the metric stays order-preserving (a genuine wrong-orientation detection must still surface as a large error), only the deterministic convention offset is fixed.
  • Parallel tuning + comparative-benchmarking harness (tools/bench/tune/, Python-only, no Rust or runtime change). New CLI subcommands drive it:
  • bench sweep — fan a declarative search space (tune/spaces/*.json) for Locus and competitors (OpenCV ArUco, pupil_apriltags) across cores; each worker is pinned to a single thread (RAYON_NUM_THREADS=1 / cv2.setNumThreads(0)) so N workers never oversubscribe, and the accuracy sweep is byte-identical regardless of worker count. A native crash in a competitor detector is isolated to one errored cell instead of failing the sweep.
  • bench tune — select the Pareto frontier over (maximize recall, minimize p99 pose error) under hard precision/latency gates, then verify latency serially (each frontier config in its own un-pinned subprocess: production threading for trustworthy timing, plus native-crash isolation so a corrupting competitor config can't take down the tune) and guard against regressing the render-tag tail/mean vs shipped Locus profiles. Writes pareto/<library>.json.
  • bench compare-report — knob-sensitivity heatmap (which lever moves which metric) + per-stratum tuned-Locus-vs-best-competitor deltas + HTML bundle.
  • Competitor detector wrappers are now fully parameterizable (from_params), so comparisons tune all libraries fairly. Optional Bayesian search via the [tune] extra (pip install -e '.[tune]'); grid/random need no extra deps.
  • Results persist as tidy tune_results.parquet / tune_configs.parquet reusing the existing Tier-1 ObservationRecord substrate and stratification.

Changed

  • Bench metric plumbing consolidated (behaviour-preserving, verified byte-identical): the detection↔GT matcher (tools/bench/matching.py), the percentile helper (metrics.percentiles), and the Pareto-frontier primitive (tools/bench/pareto_core.py) are now single implementations shared by the headline aggregator, the Tier-1 collector, and the plot layer.

Tests

  • Numerical test hardening (tests-only, no production-code change): replaced weak/smoke assertions with real numerical invariants and pruned redundant tests across the Rust and Python suites. Highlights: the homography SoA test now checks the canonical-square→quad reprojection instead of data[0] != 0.0; test_decimation_accuracy gates full-res and decimated corner error at 0.01 px (was an unasserted err1 plus a loose err2 < 1.5) and asserts decimation invariance (the observable +0.5-pixel rule); the GWLF synthetic test gates a measured 0.2 px cross-seed bound (was 1.0 px) and gains an idempotence check; two robustness/geometry.rs no-op (prop_assert!(true)) degenerate-quad tests now assert the solvers return None or finite (never NaN/Inf) output, verified over thousands of pathological quads; a new cross-thread/run-to-run detection determinism test and an image-edge robustness test were added; the tautological DetectorConfig default/builder tests were replaced by a builder-isolation invariant; and the duplicated test_vectorized_poses was de-duplicated and tightened from a tz > 0 sign check to the full pose (tz = 0.80 m, near-identity quaternion). No public API or runtime-behavior change.

Changed

  • Lint-suppression modernization: migrated production #[allow(...)] lint suppressions to #[expect(..., reason = "...")] (stable since Rust 1.81) across locus-core/locus-py, so a suppression that stops being needed now fails the build (unfulfilled_lint_expectations) instead of lingering silently. The migration surfaced and removed ~20 stale or redundant suppressions — item allows duplicating the workspace lint table, and too_many_lines/too_many_arguments/similar_names allows on functions that no longer trip the lint — and deleted the dead apply_adaptive_threshold_fast/apply_adaptive_threshold_with_params integral-image path. Exported-API-sensitive lints (unused_self, struct_field_names) intentionally stay #[allow] because their firing is Cargo-feature-dependent. Comment/doc touch-ups in pose.rs, threshold.rs, and segmentation.rs favour why-over-what. No public API or runtime-behavior change.

Documentation

  • Pose-covariance Phase 4 postmortem (docs/engineering/pose_covariance_phase4_postmortem_2026-05-31.md): records the empirical falsification of Phase 4 (per-corner ERF residual-MSE covariance inflation), the last covariance-calibration lever the 2026-05-21 sweep had not ruled out. Measured end-to-end on branch feat/activate-per-corner-empirical-noise (PR #290): KL stays at the baseline 13.93 against the §4 gate of < 0.5, unchanged even at the theoretical upper bound, because a per-corner diagonal σ_n² rescale cannot fix the off-diagonal structure of (JᵀWJ)⁻¹; the empirical column is also all-zero in production (no low-PPB tags route through ERF on the hub corpus). The ~700 lines of SIMD/FFI/SoA plumbing are not shipped (recoverable on the branch per the §6 precedent); the audit-harness knobs and empirical bound are the salvageable artifacts. pose_covariance_followup_2026-05-22.md §4/§5 amended with status banners. All four covariance phases are now closed.
  • EdLines SOTA follow-up postmortem (docs/engineering/edlines_sota_followup_postmortem_2026-05-25.md): records two empirical falsifications of EdLines-architecture changes attempted on 2026-05-25 — a continuous arc-balance Phase-1 mode selector (replacing the static >40 %/<16 % axis-imbalance gate) and the design memo's Improvement #2 (Phase-5 8-DoF line parameterisation). Both reverted on the same day after regression_render_tag rejected them. The postmortem catalogues the failure mechanisms (decoder-rejection invisibility for the selector; variance amplification through the line-intersection Jacobian for Phase 5) and the re-attempt conditions that would have to be resolved before either bet is worth re-opening. edlines_sota_design_2026-05-03.md amended with status notes on F4, F6, and Improvement #2.