Doctoral Research · Space Robotics Inspection with a Free-Flying Space Manipulator
A Doctoral Research Journal Aerospace Engineering

Core code review — Inspection/GNC + Inspection/analysis

Whole-tree quality review (read-only, no code edited) using the new clean-review rubric skill + ponytail-ultra, run as an ultracode workflow: 20 review units (opus on the load-bearing files, sonnet on the mechanical pairs), each briefed with the rubric, its tree’s governing contract, and an INSIGHTS quirk-check, followed by an adversarial verification pass in which every bug/footgun/drift claim was handed to an independent refuter instructed to default-refute. 89 agents total (~7.9M subagent tokens). 54 findings survived verification (11 were refuted and discarded); ~35 modularity/style observations pass through as unverified judgment calls, marked ⊘ below. Findings cite clean-review rule IDs.

Verdict: the core is sound — the equations are implemented faithfully and the architecture is load-bearing, not ceremonial — but the review found 7 confirmed bugs (2 live in shipped numbers, 5 latent), a band of change-detonated footguns concentrated exactly where the thesis is headed (singularity handling, risk-aware Monte Carlo), and doc drift in current_sota.md that would defeat a pencil-and-paper re-derivation in 3 places.

1. Confirmed bugs

Every entry here survived independent adversarial refutation; the two live ones I additionally verified by hand.

1.1 cumulative reduction integrates with dt = 1.0 — LIVE in shipped reports

analysis/data_analyzer.py:1211 (G5). SignalAnalysis.window_reduction calls reduction.apply(signal, threshold=...) without threading dt, and Reductions.CUMULATIVE defaults dt=1.0. The view knows the real dt (self.log_dt(), ~0.03 s); it just is not wired through. Every cumulative row is therefore ~33× the physical time-integral. I verified the live impact myself: YAMLs_by_domain/metrics.yaml:107 lists cumulative on control_effort.default_control, inherited by the force/torque/wrench metrics that reports render. The 1e-12 parity test cannot catch it — measure and the sweep are wrong in lockstep. Fix: pass self.log_dt() into reduction.apply. Note when fixing: every historical cumulative value shifts by ~33×; re-pin anything gated on it.

1.2 Aim-clock floor double-advances per step (POSE + analytic-FF)

GNC/guidance/ee_guidance.py:679 (N7; reported footgun, verifier upgraded to bug). _aim_arclength reads like a getter but mutates _s_aim_prev, and the POSE + analytic-feedforward path calls it twice per step (orbit_path_pose at :688, then analytic_desired_nu_e_feedforward at :277 via breve_controller.build_desired_for_step). With aim_floor_rate>0 or aim_lead_max>0, a stalled floor climbs at 2× the configured rate, and the commanded pose and its feedforward twist are evaluated at different arclengths within one step. The verifier found this exact knob combination in a real campaign spec (analysis_YAMLs/mission_analytic_ff_guarded_jun19.yaml), and validation/legacy/test_aim_clock.py never exercises the double-call path. Fix: memoize per step_idx, or advance the floor once in add_ee_goal.

1.3 One hung box crashes the whole dispatch loop

analysis/dispatch.py:301 (G). run_ssh/rsync pass timeout=30 to subprocess.run, which raises TimeoutExpired on a connected-but-stalled command — and nothing in the chain catches it (_poll_and_route catches DispatchError; TimeoutExpired is a SubprocessError, not an OSError). The 2-strikes box-down resilience design is bypassed by the single most common LAN failure; the controller aborts mid-cycle and abandons every in-flight run. Fix: catch TimeoutExpired in run_ssh/rsync and return non-zero rc so the existing transport-error path handles it.

1.4 cmd_status reports healthy idle boxes as UNREACHABLE

analysis/dispatch.py:1215 (G9). _poll_box returns the TRANSPORT_ERROR sentinel whenever zero status lines parse, so a reachable box with no runs under today’s dated dir (“no runs found…” matches no status regex) reads as UNREACHABLE — every box at once on a morning after midnight rollover. The if not result: (idle) branch is dead. Also confirmed adjacent: FINISHED stems land in an invisible finished counts bucket the summary never prints, while the hardcoded collected/exhausted buckets can never be produced (dispatch.py:1225). Fix: distinguish rc==0-with-no-stems from transport failure; add a finished bucket.

1.5 The log-driver broadsheet silently drops the written narrative

analysis/upstart_reporter.py:382 (bug, empirically reproduced by the verifier). summary_from_md splits on blank lines, so the exact shape the log-driver SKILL.md template mandates (## Heading immediately followed by its paragraph) swallows the body into the <h3> and renders zero <p> tags. demo()’s fixture blank-line-separates everything, so the self-check never catches it. Fix: parse line-wise on ## boundaries.

1.6 Multi-key overlays lose every key after the first in the broadsheet

analysis/upstart_reporter.py:336 (bug). _metrics() takes keys[0] per overlay block and ignores secondary_keys entirely — a documented, smoke-tested Orchestrator feature (multi-key overlay, twinx) silently renders incomplete. Currently masked because the one wired stem uses single-key blocks. Fix: loop over Orchestrator.overlay_keys(fields) + secondary_keys.

1.7 OrchestrationResult.relative_figure_paths is a one-shot generator in a tuple-typed field

analysis/orchestrator.py:1090 (G16; confirmed independently by two units). run() assigns a generator expression to a field declared tuple (dataclasses don’t enforce), while fake_report_inputs assigns a list — so smokes mask it. The only live consumer iterates once today, so nothing fires yet; any second reader silently gets an empty sequence and drops every figure. Fix: tuple(...).

2. Latent footguns — GNC

These are correct today and break under a likely change. The first three sit directly on the singularity-handling priority and the risk-aware phase.

3. Latent footguns — analysis pipeline

4. Contract & doc drift

The math sheet first — these three defeat a pencil-and-paper re-derivation, which is the defense-critical failure mode:

  1. Eq 5.2 prints the opposite feedforward shapeGNC/equations/current_sota.md:472 vs com_guidance.py:320-328 (confirmed independently by two units). The doc’s a_ff = a_cd·exp(−t/τ) decays to zero; the code is a warm-start blend a_seed + (1−e^{−t/τ})(a_c − a_seed) that RISES to the live centripetal demand. The code is the intended behavior per INSIGHTS; the printed formula is stale. Fix the sheet.
  2. Eq 4.1’s J_x̃e omits the coupling block the code carriescurrent_sota.md §4.1 vs breve_controller.py:320 (confirmed by three independent traces, including my own read). Code sets J_12 = cross(e_p) — the physically-correct rotating-frame term for a body-frame position error; both the sheet and Giordano eq 24 print block-diagonal. Per the house provenance bar, an uncited correct term is still a drift: derive and cite it in §4.1 (or confirm-and-remove).
  3. Eq 6.2 documents only the full-Γ Tikhonov solve — the live 6-DOF UR3 path (z_a is None) runs a hierarchical solve (damped arm sub-solve + exact closed-form base-linear row, preserving v_c near singularities, breve_controller.py:526-541). A reader reproducing 6.3’s single solve will not reproduce live velocities near a singularity. Add the 6-DOF branch to the sheet.

Pointer bit-rot (all verified against disk):

5. SOTA map — current_sota.md ↔︎ code (the re-up)

Traced by a dedicated opus unit; each row re-read at the cited lines. Live path: validation/run_mission.py::runBreveController.run_allControlLoop.run_control_loop.

Doc eq Meaning Code Verdict
1.1–1.7 twists, adjoint, coupled M/C, Jv_bar, v_c utils/robot.py:277-285, :150-213 AGREE
2.1–2.2 nu_e composition, G maps utils/robot.py:171-179, :285 AGREE
2.4 / 2.6 / 2.7 Γ, closed-form Γ⁻¹, Γ̇ utils/robot.py:294-341 AGREE
2.5 force map F = Γᵀ G com_controller.py:150-151 AGREE
3.1–3.4 M̂/Ĉ, M̆/C̆ blocks, decoupled CoM utils/robot.py:345-374, com_controller.py:161 AGREE
4.1 (x_b) base attitude error + J_x̃b breve_core_controller.py:51-66 AGREE
4.1 (x_e) EE pose error + J_x̃e breve_controller.py:164-173, :316-326 DIVERGES — doc omits the [e_p]^ coupling block (see §4.2)
4.3–4.8 stacked error, task laws, corrected sign, actuator distribution breve_core_controller.py:68-93, breve_controller.py:277-312, com_controller.py:150-160 AGREE (eq31 sign fix honored)
4.10–4.12 working reduced RHS breve_controller.py:404-438 (RHS34b) AGREE
4.11 joint-rate recovery block order utils/robot.py:180, breve_controller.py:529-530 AGREE (eq34d block-order fix honored)
4.13 integral fold breve_controller.py:217-230 AGREE
4.14 cruise-lag floor analytic_feedforward.py:144-158, breve_controller.py:353-383 AGREE
4.15–4.16 Lyapunov proof NOT-IMPLEMENTED (analysis only; expected)
5.1 / 5.3–5.7 startup ramp, centripetal, helix, SLERP, POSE standoff com_guidance.py, utils/orbit.py:46, ee_guidance.py:684-697 AGREE
5.2 accel-FF shaping com_guidance.py:320-328 DIVERGES — doc decays, code warm-starts (see §4.1)
5.8–5.18 analytic FF twist chain analytic_feedforward.py:7-141 AGREE, term-by-term
6.1 / 6.3–6.5 threshold cascade, λ_Γ, derate ramp, 3-tier damped J⁻¹ sampling.py:75-84, robot.py:217-256, breve_controller.py:521,540 AGREE
6.2 Γ recovery breve_controller.py:516-541 DIVERGES — live 6-DOF hierarchical branch undocumented (see §4.3)
7.1–7.3 coverage, ANCHOR score, versine target_finder.py, palace POINTING_ERROR AGREE (coarse)

The gnc-small unit additionally traced analytic_feedforward.py line-by-line against eq 5.7–5.18 and 4.14: every formula matches, including both published-typo corrections. Between this table, §4, and the three-way-confirmed coupling-block derivation, the sheet needs exactly three edits to be defensible end-to-end.

6. Test-coverage gaps (the ones that already bit or guard live claims)

7. Modularity — ponytail-ultra ⊘ (unverified judgment calls, all safe deletions per zero-caller greps)

The architecture verdict first, because it’s the headline: the structure is load-bearing, not ceremonial. ControlLoop has-a COMController; BreveCoreController is a genuine Template Method whose per-subclass forks (RHS34b, calc_posture, v_breve_dot, reconstruct) are deliberate and documented; the guidance chain is a real 3-deep tower. The prior byte-identical duplication was already resolved by the base-class extraction. What remains is accreted dead weight:

8. AX/UX — the happy path (why one-offs keep happening, and what makes rigor fast)

The AX/UX unit’s verdict: the palace hot path is correct by construction, and the skills (lab-report → sim-runner → log-driver) need no change — the friction is concentrated in the one-call surface, and each gap below is a place where the rigorous path is measurably slower than a hand-roll, which is exactly the pressure that births shadow scripts. Prioritized (P1 first):

  1. P1 — v_sat is the one catalog metric measure() cannot touch (data_analyzer.py:2325): it needs a per-run v_max and the API has no way to pass it, so clip-rate questions force either 6+ lines of DataSummary plumbing or the banned hand-roll. Add v_max=None to measure/series.
  2. P2 — load-once + batch: every measure(path, ...) call re-reads the NPZ and re-parses the metric catalog; k reductions cost k disk loads. Memoize _measure_view by path and add measure_many (or accept a reduction list) — and flip measurement.md’s copy-paste examples to lead with a load-once LogView, because the taught pattern currently models the reload-per-call antipattern.
  3. P3 — series() has no mask= while measure() does, so band-restricted signals for plots push agents back to hand-built masks. Mirror the parameter.
  4. P4 — fix back_half_pkpk composition (§3).
  5. P5 — tuple() the figure-path generator (§1.7).
  6. P6 — the two PONY swaps (scipy trapezoid; shared DataSummary in pairwise_divergence).

This is the palace answer to the AX/UX collision: don’t lower the rigor bar, shorten the rigorous path until it wins on speed too.

9. What is healthy (so the above reads in proportion)

The measurement hot path (z_b rms, p_e p99, frac_below) traces correctly end-to-end and parity holds by construction. LogView windowing composes masks correctly and returns NaN rather than crashing on empty windows. The mode-enum discipline is clean — zero magic strings in guidance. analytic_feedforward.py matches the sheet term-for-term. runner.py‘s controller map is complete; the # noqa: F401 guard on metric_alignment still holds all 12 names. Plotter honors the boundary (renders only, computes nothing, imports no analysis module) and render_line_panels has not crept. The reporters’ minipage/conclusions contracts are honored in both engines. Dispatch’s length is genuinely load-bearing, not bloat. The batch-1 primitives are NaN-aware and empty-guarded at every boundary the review probed except the four named in §3.

10. Method, provenance, and limits

Addendum — fix wave 1 applied (2026-07-02, user-directed)

The five live bugs plus the one-liner are FIXED (facade CLAIM honored via AGENT_COMMS ping; pathspec commits only): §1.1 cumulative now integrates with the view’s real dt (SignalAnalysis.window_reduction threads log_dt()); §1.3 run_ssh/rsync fold TimeoutExpired into the rc≠0 transport path (rc 124); §1.4 _poll_box returns {} for a reachable idle discovery poll (garbled output with expected stems stays a transport error) and cmd_status grew the finished bucket, dropping the impossible collected/exhausted ones; §1.5 summary_from_md parses line-wise and demo() now pins the adjacent-line template shape; §1.6 _metrics walks all overlay keys + secondary_keys; §1.7 relative_figure_paths is a real tuple.

Validation: 5 targeted synthetic-data checks pass (_heredocs/test_fix_wave_2026_07_02.py, transient); the analysis smoke suite’s 18 logic tests pass unchanged — its 7 failures are the pre-existing missing-fixture aborts documented in §6, upstream of every edited line. ⚠ Post-fix, any historical cumulative value is ~33× off relative to the corrected definition — re-derive before comparing old and new control-effort rows. Still open: §1.2 (aim-clock, GNC), the §2 thesis-path footguns, the three current_sota.md edits, and the §8 palace punch list.

Addendum — fix wave 2 applied (2026-07-02, user go; 29 agents across two workflows, every code change adversarially verified)

Analysis half (facade CLAIM honored via AGENT_COMMS ping; commits b2f64c9e, 7f8aa142, a8dff5b3, 3cdecc22): §8 P1–P3 landed additively (v_max= on measure/series; mtime-keyed load-once + measure_many; series(mask=)) — verifier re-ran the golden parity suite through the new cache, green (validation/tests/test_signal_measurement.py asserts abs=1e-12). §6 test gaps closed: per-function @needs_golden (5 fixture-free tests now always run) + pins for raw_series_values 0-d reject and units_for_item missing-units→"" (probed: a literal NaN does NOT coalesce — bool(nan) is True — so the pin asserts the real missing-units path, correcting this report’s §6 wording). §4 pointer bit-rot fixed across gnc.md, ANALYSIS.md, measurement.md, helpers.md (formatted_dict truly lives in utils/printing.py). All 12 GNC wiki pages reconciled (17 fixes; notes/clean_review_wave2.md).

GNC half (user unlocked Edit(Inspection/GNC/**) session-only; lock restored after):

New contradiction surfaced (wiki reconciliation): YAMLs_by_domain/parameters.yaml:34 sets controller.com.integral.enable: true, contradicting §2’s “the CoM integral is OFF in the adopted default” masking claim for the anti-windup asymmetry — verify against the BUILT baseline config before trusting either (boarded). Also: the committed fragility_table.md is stale-or-mislabeled (notes/uncertainty_implementation_trace.md §8). Resolved 2026-07-03: the contradiction dissolved into wording — enabled-but-frozen (see the §2 G28 addendum + notes/anti_windup_verification.md).

Still open after wave 2: the anti-windup masking verification, §2 guidance-rollout CoM parity, the three validation/legacy/ EEGuidance stubs, and the §7 ⊘ modularity cleanups.

Elevator pitch

The machine is sound: equations faithful (two published typos correctly fixed, one uncited improvement found), architecture earning its keep, measurement palace correct on the hot path. The review’s real yield is seven confirmed bugs — two already in your shipped numbers (cumulative ×33, broadsheet narrative/overlay drops) and five waiting on config flips — plus a cluster of footguns sitting precisely on the singularity and risk-aware work you’re about to do (un-derated logged wrench, shared RNG, double-advancing aim clock), three current_sota.md edits that make the math sheet defensible again, and a six-item palace punch list that makes the rigorous path also the fast one.