What changed—and why
Every version of NOVA's judgment, and the measurement that drove it. This is NOVA's engineering history, in public. Historical records may call the builder Henry; public pages use Jeff Henry or Jeff.
Autoprocess Workflow Changelog
Human-readable mirror of workflow_history.json (the machine source of truth). The current version is stamped into every run's run.log (workflow_version) and inherited by each saved critique, so the batch evaluation can group runs by the pipeline version that produced them.
Semver policy (scoring pipeline):
To bump: append a new entry to workflow_history.json (set current and add to versions), mirror it here, and record which critiques drove the change. Code reads the latest version automatically — no other edit needed.
Pinned engine versions (updated 2026-08-31)
| Engine | Version | Role | |--------|---------|------| | PixInsight Core | 1.9.3 Lockhart | plate solve, SPCC, StarXT/NoiseXT, register/drizzle, curves | | Siril | 1.4.3 (native siril-cli) | calibrate, register, seqplatesolve, stack | | SetiAstro Suite Pro | 1.21.0 (setiastrosuitepro, venv; 37 refreshed model files) | Image MM deconv stack, stat/GHS stretch, ADBE, Cosmic Clarity | | RC-Astro local CLI | 2.6.6 / BlurXTerminator ML5 | local BXT/NXT/SXT execution | | RC-Astro RunPod | pre-2.6.6 live endpoint | 2.6.6 image built but not validated or cut over | | GraXpert | 3.0.2 Umbriel | AI background/gradient extraction | | ASTAP | CLI + D20 star DB (/opt/astap, installed 2026-07-02) | astropy-consistent plate solve at ingest; blind solve for hint-less/pre-EQ-spoofed headers; also unblocks SASpro's own solver |
SASpro 1.15.0 → 1.18.0 promoted 2026-06-11 via the canary protocol below: 1.8.0 re-baseline (M 51 / M 42 / M 13) → isolated-venv API check (all 21 pipeline-imported symbols present, signatures identical; abe_run gained a backward-compatible correction_mode kwarg) → live upgrade (zero dependency churn — numpy/numba/scipy/astropy unchanged) → canary check PASSED with all three golden finals bit-identical to baseline. Not a semver bump (no pipeline code changed). Laptop worker still on 1.15.0 until its next deploy. Unlocks for future feature work: NBExtract (1.17), SSSC (1.18), ADBE linear fix (1.16.3).
These are pinned pipeline dependencies, not apps to keep current: a minor update can silently change a headless process, property default, or behaviour with no error output (PI 1.9.3 DrizzleIntegration no-ops in automation mode; Siril 1.4.2 crashed on seqplatesolve, fixed in 1.4.3). Treat each version as part of the pipeline contract.
Update protocol (highest risk: PI > SASpro > Siril): 1. Re-process the golden targets (M 51 / M 42 / M 13) on the current engines so each target's latest run is fresh, then python -m nas_server.canary snapshot to freeze critiques/golden_baselines.json. 2. Install the new engine to a side-by-side path; keep the old binary for rollback. 3. Re-process the golden targets on the new engine. 4. python -m nas_server.canary check — diffs the new finals vs the snapshot and exits non-zero on drift. Promote only if it passes.
Updating an engine is not a semver bump on its own (these are environment state); bump only if pipeline *code* changes in response to the new engine.
2026-08-31 provenance exception: at Henry's direction, workflow 1.26.0 creates a run-version boundary for the promoted local BXT ML5 and SASpro 1.21.0 model stack even though pipeline code did not change. This is warranted because model changes can alter output pixels and critiques need to distinguish runs made before and after the promotion. It does not claim that the RC-Astro RunPod 2.6.6 image is live.
---
1.26.0 — 2026-08-31 (minor)
Record the promoted local BXT ML5 and SASpro 1.21.0 model stack in run provenance.
The VM now runs RC-Astro CLI 2.6.6 with BlurXTerminator ML5 and Seti Astro Suite Pro 1.21.0 with 37 refreshed model files, including the main, walking-noise, and aberration-correction V2 bundles. Henry requested a workflow-version boundary so runs from this model stack are distinguishable from earlier outputs. The processing steps and scoring model are unchanged, so comparisons remain valid and this is a minor bump.
This promotion is local only. The separately built RC-Astro 2.6.6 RunPod image has not been validated against the live network volume or cut over on the endpoint; remote execution therefore remains outside the 1.26.0 engine claim.
1.25.0 — 2026-08-29 (minor)
Use a second, conservative NXT pass for standard post-stretch noise reduction.
The existing pre-stretch linear NXT pass remains unchanged. Workflows that already contain noise_reduction now call denoise_nxt again after stretch at strength 0.65 and two iterations. That wrapper uses RunPod RC-Astro first when enabled and the local RC-Astro CLI otherwise; it never silently substitutes Cosmic Clarity.
Cosmic Clarity, Prism, and alternate NXT strengths remain explicit Experiment Mode choices. seestar_galaxy still omits the nonlinear step. The change follows the 2026-08-29 M31 broadband benchmark, where local full Cosmic Clarity spent about 27.4 minutes producing a candidate the objective gate ultimately rejected. Henry explicitly selected the two-NXT architecture. The step and scoring model are unchanged, but the production engine and parameters change output, so this is a minor bump.
1.24.8 — 2026-08-08 (patch)
Persist the stretch-pick decision rank.
candidate_scores carried pref from the generic _STRETCH_PREF_ORDER, but the galaxy branch decides with _GAL_PREF — computed locally and never recorded. Any consumer reading pref as "the rank that chose the winner" was wrong for galaxies: on M 66 the two orders disagree on 5 of 6 survivors, showing veralux_strong 9th where the galaxy order ranks it 2nd. That produced a false claim on the episode's stretch-grid panel (PR #317, found by Codex).
_emit() now records decision_rank and decision_basis from the list the branch actually sorted, so galaxy, nebula and blended-cost branches each report their own real ordering. Dropped candidates get no rank — they were removed before the ordering.
Instrumentation only. No sort key, threshold, or winner-selection line changed; verified by diff and by replaying M 66's own candidate data, which reproduces the recorded winner mas.
1.24.7 — 2026-07-20 (patch)
Stacker pre-EQ spoof fix — the 16-hour failure. The SH 2-298 LP restack failed catastrophically: 3717/3717 frames failed seqplatesolve over 8 hours, the fallback registration re-attempted platesolve and failed AGAIN over another 8 hours — 16 hours, zero output. Root cause: every individual sub's header carries the pre-EQ ALP location spoof (Dec off by 22° from catalog truth), so Siril's -radius=25 search fetches up to ~297k Gaia stars per frame trying to compensate — and still can't converge. stack_target() now compares one copied frame's header RA/DEC to the target's catalog position; on >3° mismatch it rewrites RA/DEC on every copied frame to the catalog truth before Siril ever runs. Validated on a 9-frame test set: 9/9 solved in 1.3s (catalog fetch collapsed to 2.3k stars). Extrapolated full run: ~9 minutes, not 16 hours.
1.24.6 (filter separation deploy, delayed) — now live
/stack?filter=auto|<name>|all (committed 2026-07-19, this restart is its first live deploy): groups qualifying frames by FILTER, stacks the dominant one by default, logs what was left out. Root fix for mixed-filter stacks like SH 2-298's original 6.6h-IRCUT-labeled/11.8h-LP-hidden blend.
1.24.6 — 2026-07-19 (patch)
Correction (credit: Henry — "framing mode doesn't change pixel scale"). The 1.24.4 "framing-mode 4.78″/px" conclusion was wrong: the SH 2-298 stack is native 2.39″/px. ASTAP's -fov is field height; the ladder computed width × scale, so the native rung failed and the solve landed only through coincidental arithmetic on a portrait crop. Ladder now uses NAXIS2 × scale. The fix that actually rescued SH 2-298 was 1.24.5's catalog override of the pre-EQ spoofed position.
1.24.5 — 2026-07-19 (patch)
Completes the 1.24.4 pre-solve: color_calibration now actually passes target= to spcc() (the catalog-hint block was silently gated on an empty string), and the catalog position overrides header RA/DEC on gross mismatch (>3°) — pre-EQ southern captures carry fake header coordinates (ALP location spoof); SH 2-298's Dec 2024 stack claimed Dec +9 for a Dec −13 target, so the scale ladder searched 15° around a lie.
1.24.4 — 2026-07-19 (patch)
SPCC ASTAP pre-solve: catalog position hint + scale ladder. SH 2-298 exposed SPCC failure mode #4: a WCS-less framing/mosaic-mode stack (~4.78"/px — twice native on the same 1080×1920 canvas). The pre-solve assumed native scale and found no solution in any search window. It now writes the target's catalog RA/Dec into the temp copy and tries FoV hints for all three S50 scales (2.39 / 4.78 / 1.19 "/px) before falling back blind — the right hint solved SH 2-298 in 1.3 s.
1.24.3 — 2026-07-18 (patch)
Small-target recenter crop is now WCS-first: center on the target's catalog RA/Dec projected through the crop-stage WCS (trustworthy since the ASTAP audit + 1.24.2 CRPIX fix), sized from the folio angular size × measured pixel scale. The brightest-extended-blob detector is demoted to fallback — a bright star's halo survives the median filter and can out-peak a low-surface-brightness galaxy, which cropped M 102 out of its own frame (the 3.5 run; caught by Henry's screenshot). WCS(hdr, naxis=2) required for 3-axis color headers with SIP.
1.24.2 — 2026-07-18 (patch)
Small-target recenter crop now shifts CRPIX with the crop. It copied the header verbatim — full-frame reference pixel on a 480px cutout — so every recentered target (folio <11') shipped a WCS pointing ~0.5° off-sky and SPCC queried Gaia at the wrong position: the second (fast-fail) SPCC mode. Explains the rescue-batch split: M 85/97/102 (recentered) still fell back after 1.24.0 fixed the PC-form mode for M 88/91/109.
1.24.1 — 2026-07-18 (patch)
Post-guard re-score fix: 1.24.0 replaced the final evaluator's scores with a physics re-grade when channel_crush_guard fired — physics grades on a different scale (M 109: a 7.2-class run recorded 6.5 with color_balance 1.0), breaking cross-run comparability. The evaluator's final_scores stay authoritative; the shipped-pixel physics grade now attaches as final_scores.post_guard_physics.
1.24.0 — 2026-07-18 (minor)
Batch eval 2026-07-16 fixes (M 85/88/91/97 galaxy regressions, 3 urgent issues).
- SPCC mutilated-WCS rescue: the crop step's astropy WCS rewrite emits the PC-matrix + CDELT=1.0 header form; PI 1.9.3 reads CDELT literally → 3600"/px seed → solver fails → SPCC always fell back to generic CC (pink nuclei, unlinked stretch).
- Galaxy stretch sky-ceiling gate (
_GAL_SKY_CEIL = 0.10, relative): the
detail-first galaxy branch now refuses winners whose sky exceeds sky_mute's
healthy pull range when an under-ceiling candidate exists. Thin stacks handed
the mute 0.11+ skies and it over-crushed the result.
- channel_crush_guard re-score: the guard mutates pixels after the final evaluation; the shipped image now gets a physics re-grade so
spcc() now normalizes PC→CD on a temp copy (pure header
math, no re-solve); absent/insane WCS gets an ASTAP pre-solve instead.
Verified on M 88: SPCC OK in 26 s (was 2×150 s fail → fallback). A
keyword-level sanitize+reseed belt also landed in pi_postprocess.js.
run.log's final
score describes the shipped pixels (root cause of the "final ≠
best_publishable / evaluator scored the wrong image" flag).1.23.1 — 2026-07-17 (patch)
Instrumentation only (channel/episode Phase 1) — no pixel-path change.
- Stretch candidate scores persisted:
_physics_pick_stretchnow emits every
candidate's physics metrics (cost, bg_dist, p99, under, blown, bg_noise, grain,
winner flag, dropped reason for grain-DQ / grain-cap cuts) via a - Hard-veto reasons persisted: a step that ends skipped after an artifact-class hard veto (decon undershoot, halo output contract, …) now records
scored_out
out-param; the stretch_pick step_record stores them as candidate_scores.
skip_reason="hard_veto: <reason>" + veto_metrics instead of the generic
"no improvement".Both land in run.log so the episode generator (make_episode.py) can render the 9-candidate grid and failure-beat shots from real data instead of re-measuring.
1.23.0 — 2026-07-15 (minor)
Small-target compositional recenter-crop (M 1 under-framed). The coverage crop only trims blank borders, so a target much smaller than the S50 frame ships as a speck (M 1 6' filled ~10% of a 77'x43' field; the processing was good at 7.0, the *framing* was poor). New _small_target_recenter_crop wraps the crop fn when the folio size < 11' (25% of the 43' short axis) and there's no saved/reviewed crop (a user's manual framing stays authoritative). It finds the target as the brightest extended (non-star) blob, measures its radius, and crops to 4.5x that radius centered on it, with a 240px native-res floor. Sizing from the measured blob is scale-free — necessary because drizzled finals (~1.19"/px) aren't inferable from frame dimensions. Triggers on M 1, M 57, M 76, M 27, M 97, NGC 6826, M 105, small galaxies; skips M 51 (11.2') and larger. Validated: M 1 Crab centered, 2032x3712 -> 784px, well-framed.
---
1.22.2 — 2026-07-15 (patch)
Grain-DQ scoped to galaxies (SH 2-273 regression fix). The 1.21.0 grain disqualifier (bg_noise > 0.10 drops a candidate) was branch-neutral, but on faint nebulae the correct brighter stretch is *necessarily* grainier — the DQ killed it and left a conservative dark pick. Measured: SH 2-273 regressed 6.8 → 4.5 (DQ dropped stat_bright, dark ghs_soft won, "faint nebulosity almost entirely absent"). The galaxy branch needs the DQ (it ignores grain by design); the nebula branch already treats grain as a placement-equal tiebreaker — the right discipline for faint signal. Fix: apply the DQ only when is_galaxy. Validated: SH 2-273 → stat_bright (restores 6.8); M 108/NGC 4565 keep clean picks with veralux_strong still DQ'd. Caught by the reprocess campaign (1 of ~20 wave-1 reprocesses regressed).
---
1.22.1 — 2026-07-14 (patch)
Frame-fill false-positive fix (M 1). M 1 — the 6' Crab remnant in a dense Taurus star field — triggered frame_fill=True (coverage 0.93); the smoothed "coverage" was unresolved stars + noise, not emission. The dark-anchor over-brightening then amplified a residual gradient into a broad green band (grader 3.2, "severe gradient contamination"). The pixel tests can't separate it — M 1 and IC 1805 both measure corner/floor = 1.04 pre-stretch. Fix: an angular-size gate — a target < 30' cannot fill the 43'×77' frame, so it's frame-fill-ineligible regardless of pixel coverage (folio size threaded from _folio_info). M 1 (7') ineligible; NGC 7000/IC 1805 still fire; unknown size falls through to pixels. Note: the horizontal bands Henry saw are in the SeeStar EAA live-stacks only — Siril sigma-clip rejection removes the satellite trails from the pipeline stack (verified clean, 3.1σ); the green band was created in processing, not inherited.
---
1.22.0 — 2026-07-13 (minor)
Batch-eval HIGH fixes, Bump 2 (docs/plan_batch_eval_20260713_impl.md) + 07-07 carry-overs.
- sky_green_rebalance signal-green cap: the sky-only design correctly leaves object colour alone — which let colour_boost/curves-amplified SIGNAL casts sail through while the step reported "no excess" (NGC 281 G p99 lead 7.9%). New opt-in signal mode: when the signal-region
- Star-layer grain gate (P4+P5 unified): combine doubled final noise via star- layer speckle (NGC 2359 ×2.1, NGC 281 ×2.0; starless clean). Pre-combine, star- layer corner σ > 2× starless → re-stretch stars one factor step gentler (single retry). Retro-validated: fires on both measured cases (2.4×/2.1×). Deviation: the eval's post-combine denoise fallback was dropped — post-combine corner σ is star-contaminated (healthy M 51 final measures 0.084), the 0.08 threshold would misfire.
- 07-07 carry-overs: pi_globular_core_rolloff inversion guard (revert when p99 rises — it brightened cores while darkening midtones); denoise_linear[nxt] near-identity labeling (<10% rms reduction →
(G_p99 − max(R,B)_p99)/G_p99 > 0.06, cap G
toward max(R,B) inside the signal mask (bounded 0.6, never-invert machinery
shared with the sky pass). **Folio prior gate on the FIRST dominant_colors entry
only** — whole-list substring matching wrongly blocked NGC 281 and M 42 (secondary
OIII accents); Thor's Helmet's teal-dominant first entry correctly blocks.
Retro-validated: NGC 281 fires, NGC 2359 blocked, M 42 clear. The 07-07 pre-gate
half already exists (min_gr self-gate + max(R,B) cap postdate the v1.14.2 floods).
near_identity step record).---
1.21.0 — 2026-07-13 (minor)
Batch-eval CRITICAL fixes, Bump 1 (docs/plan_batch_eval_20260713_impl.md).
- Stretch picker grain disqualifier:
bg_noise > 0.10drops a candidate before
any branch ranking (keep-cleanest fallback). The galaxy detail-first branch had no
grain guard at all — veralux_strong won M 108 at σ 0.198 (26× the cleanest) and
NGC 4565 at σ 0.143. Replay: exactly the 3 grainy live picks flip to clean - Galaxy post-mute crush guard: additive lift back to the band floor when the combined-tail sky lands below
- halo_suppression output contract — the step was CRITICAL in two consecutive evals with three distinct modes. Hard-veto unless: |sky delta| ≤ 0.020, noise ratio ≤ 1.5× (1.18.0 guard kept), p99.9 rise ≤ 0.01; veto reason carries the mode (sky-crush / sky-raise / p99.9-rise / aliasing).
- (Rides along, from 1.20.1-era: SPCC log wording — "LP data — SPCC deliberately skipped" vs real failure; log-string only.)
mas;
every clean pick reproduces (2 apparent flips proven replay-depth artifacts).
band_lo − 0.005 (measured finals 0.031–0.045 vs
the 0.05 floor). Fixes the downstream crush, not the deliberately-bright 1.7.0
pick.
reduction_level capped at 2 on
sparse fields (<1000 stars — the level-3 sky-raise inversion). Unit-validated 7/7
against the measured failures from both evals.
---
1.20.1 — 2026-07-12 (patch)
Frame-fill picker cost hole fix. The NGC 7000 live-proof run (20260712_175526) failed exactly where 1.20.0's cost was blind: a flat-grey stf won (dark floor 0.160 / p50 0.177 — tonal spread 0.017 vs the manual reference's 0.107), graded 4.2 post-stretch, final 2.5 with a non-degradation restore. Henry flagged it live. The 1.20.0 placement charged floor position + p50 brightness but not spread, so a contrastless wash scored best on both. New frame-fill placement term:
- ceiling-only floor charge — a floor above band-hi is unfixable grey wash; a floor *below* band is not charged (the bg clamp lifts it additively, structure intact);
- spread deficit ×1.5 —
max(0, spread_target − (p50 − floor)), target 0.06–0.10 by
depth; flatness is the one defect nothing downstream repairs;
- crushed-floor exception — floor <0.005 with dark σ <0.0015 is CLIPPED (faint nebula destroyed, not parked low) and pays 2× the band distance.
A/B: 7/7 controls unchanged; NGC 7000 → stat_bright (spread 0.096); IC 1805 keeps its live mas (8.2); IC 1396/SH 2-273 reject clipped veralux variants for ghs_soft.
---
1.20.0 — 2026-07-12 (minor)
Frame-fill detection — dark-anchor sky for frame-filling nebulae (docs/spec_frame_fill_detection.md). The IC 1805 pixel autopsy proved the manual 8.2 beats the pipeline's best 7.8 on midtone placement alone: manual p50 0.244 / corners 0.190 vs pipeline 0.075 / 0.068, with the same absolute grain, comparable saturation, and *higher* pipeline midscale contrast. On a frame-filling target the corners are faint nebula, not sky — so every corner-anchored mechanism (band charge, picker cost, clamp, feedback, grades) was muting the object. This is the hard-number proof of the faint-nebula-too-dark feedback.
- Detector (
_frame_fill_detect, once per run on the post-BGE pre-stretch starless):
32×32 box-smoothed luminance; structure ≥100× smoothed noise floor, corner separation
≥0.15 of smoothed range, coverage ≥0.65. Smoothing is load-bearing — raw level stats
cannot distinguish a uniformly filled frame from a uniformly empty one post-BGE.
Type-gated to emission/SNR/generic nebula. Library sweep: IC 1805, NGC 7000, IC 1396,
NGC 6888, M 16, SH 2-273 TRUE; M 42/M 17/M 1/NGC 2359/M 8/M 20/SH2-101 and all
galaxies/PNe/globulars FALSE.
- When frame_fill:
bg_level/bg_noiseanchor on the darkest-percentile pixels
(p1–p3 — the manual's dark anchor measures 0.137, comfortably in band) instead of
corners; new p50 midtone target (0.15–0.20 by depth) charged into the picker cost and
nebula placement bucket; folio band charge skipped (folio bands are corner-authored).
Detector output stamped into - Offline A/B (
scripts/frame_fill_ab.py, real depth per the faithful-A/B protocol):
7/7 controls unchanged; every frame-fill pick moved brighter or un-crushed —
IC 1805 stat→mas (p50 0.072→0.151), NGC 7000 stat→stf (0.073→0.209 vs manual 0.244),
M 16 & NGC 6888 escaped crushed-floor stf, SH 2-273 dropped a crushed grainy
veralux_strong for in-band mas.
run.log (top-level frame_fill + stretch step_record).
---
1.19.0 — 2026-07-08 (minor)
narrowband_hoo — HOO-preserving palette step (Henry-requested alternative to narrowband_norm's Equalize/SHO look). Keeps Ha the dominant red; OIII a bounded blue/teal accent: R=Ha, G=0.15·Ha+0.9·O, B=O with O = min(OIII, oiii_cap·Ha). The local cap guarantees Ha dominance and forces O→0 where there's no Ha — auto-killing the field-flood a naive OIII surface produces on Ha-dominant fields (both RGB-proxy and raw xp_oiii flood ~half the frame without it). OIII from run_dir/xp_oiii.fit (Ha ref from xp_ha.fit at matched stretch) or an RGB blue-cyan-excess proxy. Knob: oiii_cap (0.6 near-pure-red … 1.0 max teal), soft/strong variants. Force via force_hoo; SCNR skipped on this path. Limitation: needs spatially-distinct OIII to show teal — on HII regions where OIII co-locates with Ha (IC 1805) the overlap reads gray; best for PNe / Veil / OIII-rich bicolor targets. Force-only, so no change to existing runs.
1.18.0 — 2026-07-07 (minor)
halo_suppression dense-field aliasing guard (batch-eval Pattern 1). On LP data with 16k+ stars the ratio-based colour subtraction flags nebula next to overlapping halos as "halo colour" and paints vivid magenta/cyan/green rings frame-wide — bg_noise +129/157 %, p99.9 pegged to ~1.0 on both v1.17.2 IC 1805 runs (these were the star rings mis-attributed to deconvolution during the session). Now hard-vetoed when post bg_noise > 1.5× pre or p99.9 jumps past 0.995 from < 0.98. No false-positive on the clean IC 1805/NGC 7000/M 42 runs.
1.17.3 — 2026-07-07 (patch)
Hard-veto honored in step acceptance. The 1.17.2 guard fired on both IC 1805 runs, but if new_sum > best_sum promoted the damaged output anyway — ringing *sharpens* stars, so the physics delta (+6.48) outvoted the objective FAIL and both finals shipped with the artifacts the guard had caught (magenta/cyan star rings, violet blob). The guard now sets hard_veto=True and best-result promotion skips hard-vetoed attempts. Normal marginal-case iteration unchanged.
1.17.2 — 2026-07-07 (patch)
BXT local-undershoot guard. On IC 1805, deconvolution dug large black holes with magenta rims into smooth nebula while whole-frame metrics *improved* (FWHM 2.59→2.14 px, SNR ×1.43) — the objective gate is structurally blind to local ringing; Henry caught it in the step preview. New _bxt_undershoot_check: 8 px cell grid, veto when a connected blob (≥40 cells) of output local minima drops below 0.5× the input's local floor. Calibrated on labeled pairs (fires on the 86-cell artifact run; quiet on clean IC 1805/M 42/NGC 7000). Veto keeps the pre-decon image.
1.17.1 — 2026-07-06 (patch)
pi_curves silent no-op fixed (critique-batch cross-run finding). _clean_curve_points merged any control point within 0.03 of x=0 *into* the [0,0] black anchor — on dark images (NBN sky ≈0.05 → point at x≈0.027) this destroyed the x=0 endpoint, PI rejected the whole curve (CurvesTransformation.K(): the instance is not valid), and the JS saved the input unchanged: bit-identical output, exit 0. Not a version regression — stats-triggered (curves input sky < ~0.06), made common by the darker LP/NBN path; the batch's 1.15.0→1.16.1 window was scheduling coincidence (curves worked in M 42/M 51 1.16.0 runs; measured no-op only in the two dark NGC 7000 runs). Fix: endpoints can no longer be merged away and are re-pinned post-clean. Verified PI-valid on the failing input.
---
1.17.0 — 2026-07-06 (minor)
background_extraction frame-fill exception. On frame-filling nebulae (NGC 7000) every "sky" cell is nebula, so the sky-gradient metric wobbles without meaning — the gate vetoed GraXpert on a +0.014 move (0.062→0.076) even though it measurably fixed the real corner wash (TL 1.029→1.006 vs median) with no faint-structure loss (central IQR/med 0.112→0.126 — improved). Henry compared renders: "with background extraction looks better." New rule: a trivial absolute gradient move (<0.05) at low levels (<0.25) with SNR held (≥0.90×) is accepted — rejection requires evidence of harm, not metric noise. Big gradient jumps or SNR drops still veto.
---
1.16.2 — 2026-07-05 (patch)
Mosaic-aware FoV for astap_solve(). The FoV hint is now derived from header geometry (NAXIS2 × scale, clamped 0.4–8°) instead of the fixed 1.3° single-panel default. Found on the first mosaic processed with the ingest solve on: NGC 7000's 60 MP mosaic solve FAILED silently → old WCS kept → XP matching stuck at 10 stars → NBN palette fell back to the approximation path (Ha amber/brown instead of red). The failing call now solves in 6.8 s. Single-panel stacks derive ≈ the old default — no behavior change for them.
---
1.16.1 — 2026-07-04 (patch)
astap_ingest_solve default ON + golden re-baseline. F4 audit complete: golden trio (M 51 / M 42 / M 13) all scored 7.8 at 1.16.0 with 0 orientation flips and plate-solved crop positions exact to the saved boxes; M 13's 1.14.0 blue-corner-glow regression confirmed gone; Henry signed off 2026-07-04. Canary baselines re-frozen to the three 1.16.0 runs. Code default flipped False→True so nodes without the settings key (laptop worker) solve at ingest too — otherwise their Siril-convention WCS + the migrated (ASTAP-convention) crop boxes land ~36′ off (the round-5 failure mode). Deploy note: laptop picks this up on its next git pull.
---
1.16.0 — 2026-07-04 (minor)
LP standard-chain colour routing (Henry: "PI CC look, keep it simple"). SSSC is spectrally faithful — but on dual-band data that renders the nebula green-teal (M 42 bright-nebula G/R: SSSC 1.21 vs PI CC 0.71 = the approved 8.2 look; SCNR can't trim it because the cast is green-*cyan*, G < (R+B)/2 pointwise). And SPCC's broadband white reference is wrong for dual-band — it only ever "worked" by failing into the PI CC fallback. Changes: 1. spcc() attempts SSSC only when allow_sssc=True — passed by the pipeline only for NBN/palette runs (narrowband_norm/nb_palette forced), the branch SSSC was built for (1.9.0). 2. On LP data the standard chain now skips SPCC entirely and goes straight to PI ColorCalibration with the .spcc_failed sentinel semantics (unlinked stretch + SCNR enabled) — the proven 8.2 recipe made explicit instead of failure-dependent.
---
1.15.0 — 2026-07-03 (minor)
F4 colour-path completion (SSSC on LP/dual-band). Three coupled changes — items 1–2 shipped 2026-07-03 unbumped (discipline violation, corrected here): 1. xp_stars.measure_stars_rgb local-annulus photometry. The global SEP background can't follow bright structured Hα nebulosity at star scales, so embedded stars carried residual per-channel background that biased colour ratios and the solved SSSC gains (M 42: star R/G 0.30 vs SPCC's 1.49 — red crushed ~4×, the "green and brown" finals). Now subtracts a per-star local-annulus (2.5r–4r) residual per channel; stars drowned in nebula are dropped. Fixed gains physically sensible: R=1.756 G=1.0 B=1.098, Stage 3, RMS 0.25. 2. SSSC drizzle gate removed. The "drizzle photometry bias" was the nebulosity contamination above, fixed at root. Henry approved the fixed SSSC colour on drizzled M 42 vs SPCC side-by-side. 3. SCNR allowed when the calibrator was SSSC (.sssc_applied). SSSC solves star-based per-channel *gains* — it can't rebalance dual-band OIII emission landing in G, so the nebula body stays green through the linked stretch. Every good M 42 run (8.2, colour 7.5–7.8) had SCNR; all three SSSC-succeeded runs that skipped it scored 5.5–6.2 with "dominant green cast" as issue #1. PI-SPCC-success runs still skip SCNR (linked-WB protection); NBN/nb_palette runs still skip it (OIII/teal protection).
---
1.14.2 — 2026-07-02 (patch)
Idle-worker per-sub solving: ASTAP-first. solve_subs_astap() copies each sub to tmp, solves there (~0.1–0.6 s measured; 4/4 on 10 s M 71 subs, scale 2.38″ ≈ native 2.37), and writes DB-only (solved_ra/dec/rot/scale) — library FITS are never modified (the ingest-solve lesson applied). Siril batch solve remains the automatic fallback. Unblocks the ~106k-sub backlog stalled on the offline laptop; pre-EQ-spoofed subs auto-blind-solve. PATCH — feeds alignment-outlier exclusion only, zero pipeline-output blast radius.
---
1.14.1 — 2026-07-02 (patch)
Ingest ASTAP solve DISABLED by default (settings.astap_ingest_solve, default false). The golden rerun regressed (M 42 5.8 dim mids; M 13 6.2 new blue corner glow; M 51 fine at 8.2): Henry identified cropping as the failure mode, and the run videos show previews flip-flopping orientation between steps. Root: the corrected WCS changes what every downstream consumer computes — crop/canonical framing picked wrong regions (implicitly calibrated to the old convention), and PI steps re-solve internally, rewriting a WCS without the PLTSOLVD marker, so the renderer alternated flip rules step-to-step. Kept: D20 DB, astap_solve() (incl. blind mode), the SSSC sanity gate (green-cast protection), renderer marker logic. Reintroduction checklist: audit target_crop/crop_analysis/canonical_frame WCS usage; preserve/strip markers consistently through PI rewrites; verify preview-rule consistency across all steps; then golden validation. Lesson: validated components ≠ validated system.
---
1.14.0 — 2026-07-02 (minor)
ASTAP re-solve at ingest — the root WCS fix. Siril/PI plate solutions are written in a convention astropy misreads (axis-inverted; drizzled stacks also offset), breaking every astropy consumer: SSSC/XP wrong-star matching (SH2-101 green cast), mirrored previews, drizzled XP no-op. Installed the ASTAP D20 star DB (/opt/astap); seti_astro.astap_solve() re-solves the working copy at ingest (~0.2 s), and the astropy-consistent header propagates through all steps (WCS-preservation now carries the PLTSOLVD marker; renderer uses the pure theoretical flip rule for those headers, legacy rule for old files).
Validated: SH2-101 XP matching 39/120-at-any-orientation → 119/120 identity vs 4–5 flipped; M 42 (drizzled+offset) 0–4 → 101/120 matches, true scale 1.1876″/px. Solve failure is non-fatal (1.13.2 sanity gate remains the backstop). Also unblocks SASpro's own ASTAP-first solver. (The Gaia XP libraries are *spectral* data — SASpro has no positional solver on them; its backends are ASTAP + astrometry.net web.)
---
1.13.2 — 2026-07-02 (patch)
SSSC output sanity gate (SH2-101 green-cast post-mortem). Measured chain: stack signal G/R 1.43 → post-SSSC 3.01 (R halved) — SSSC injected the green; the stretch only revealed it. Cause: with the WCS axis/offset mismatch vs the data, the XP matcher pairs detected stars with wrong catalog stars; sparse fields fail closed (M 42 → SPCC fallback) but dense fields (Cygnus) pass the RMS gate with wrong-star colours → garbage gains. New gate in spcc(): signal-region G/R before/after SSSC; a move sharply away from neutral rejects SSSC (clears .sssc_applied) → legacy SPCC/CC fallback. Ingest-time WCS axis repair was prototyped (xp_stars.verify_and_repair_wcs) and rejected: XP matching is non-discriminating in dense fields (39/39/37/40 under all four orientations) and M 42 matches zero everywhere (translational offset, not a flip). PATCH.
---
1.13.1 — 2026-07-01 (patch)
Fix 1.13.0 contrast-recovery regressions (found by the critique batch). Two bugs: (1) target_sky was 0.05 — below the nebula band floor 0.06 — so recovery *crushed* the sky (M 42 0.121-in-band → 0.050-crushed, faint-signal loss) and amplified grain (std/bg up to 1.24–2.13). Now targets the band floor + margin (~0.08) and only fires on a genuinely-soft sky (never darkens an already-in-band sky). (2) The body expansion blew the cores (M 42 Trapezium white mass, p99.9=1.0) — added highlight protection (identity above knee 0.80). Gain 1.2→1.15. Validated: M 42 sky 0.121→0.077 (in-band), contrast 0.171→0.193, sat 0.306→0.346, p99.9 0.973 (cores safe), 0% faint crush. The critique showed the two best batch runs (NGC 2244 8.5, M 31 8.4) were the ones where recovery did NOT fire — this retune stops it hurting the ones it touches. ⚠ Re-baseline golden M 42 (1.13.0 froze a crushed baseline).
---
1.13.0 — 2026-06-30 (minor)
Sky-adaptive, signal-aware contrast recovery for nebulae (docs/spec_contrast_recovery.md). The 1.12.0 colour-first pick (mas) has a brighter sky that softens contrast (Henry: "vibrancy better but contrast less"). This downstream step — in the combine tail, where galaxies get sky_mute and nebulae were previously skipped — pulls the black point to darken the sky (M 42: 0.125→0.045) and expands contrast around the nebula-body midtone (body-pivoted, gain ×1.2 — Henry's blind-tuned sweet spot) with the faint floor (sky+3σ) protected (identity below it), so the faint Ha/OIII is not crushed (validated: 0.0% faint crushed, 0.2% blown; M 42 contrast 0.163→0.186, saturation 0.406→0.508). Hue-preserving (channels scaled by the luminance ratio). Note: an earlier floor-pivoted smoothstep was the wrong lever (barely moved contrast — steepened low-mids while compressing highlights); body-pivoted expansion is what delivers punch, and it's not depth-scaled (noise is below the protected floor → safe on thin stacks). Gated to bright-sky/soft picks (sky ≥ 0.08; no-op on dark stf-type). MINOR — gated downstream behaviour, scores comparable; nebula only.
⚠ Canary: changes nebula finals — re-baseline the golden nebula + sign-off; bundle with 1.12.0/1.12.1 on the next restart.
---
1.12.1 — 2026-06-30 (patch)
WCS preservation extended to every step (preview-flip fix, round 2). 1.11.1 fixed only color_calibration, but PI-based steps (deconvolution/BXT, NXT, star_sharpen) *also* drop the astropy-readable WCS — so the preview renderer flips the image (the "preview flipped after deconvolution" Henry caught mid-run; confirmed: deconvolution output had NO WCS while color_calibration had it). The main step loop now re-injects the input WCS into any step output that lost it (generic), and bxt_deconvolve does it directly. Also covers forced steps (denoise_linear/star_sharpen/curves via PI bypass the main loop) and a final safety net (copies WCS from an early WCS-bearing intermediate onto final.fit if lost), so the final preview always renders orientation-consistent. Metadata only; pixels/canary unaffected. Metadata/preview only; scored pixels unchanged. PATCH.
---
1.12.0 — 2026-06-30 (minor)
Narrow colour-swap in the nebula stretch picker — "maximise colour SUBJECT TO clean". The picker already honoured *clean* (it lands on the colourful-clean stat variants Henry likes) but never *reached* for colour when it was freely available — so it could ship a desaturated stretch (M 42 = stf) when a comparably-clean, more-colourful one (mas) existed. After the physics pick, it now SWAPS only to a candidate that is comparably clean (σ ≤ 1.2× the winner — grain stays a hard constraint; Henry consistently rejected grainy veralux even when more colourful), well placed, and meaningfully more saturated (mean signal sat ≥ winner + 0.05).
Grounded in measurement + 7 blind labels: colour is largely *irrecoverable* downstream (M 42 color_boost closed <30% of the stf→mas gap) while grain is a hard reject. The rule reproduces 6/7 blind labels and changes only M 42 (stf→mas), leaving the 5 correct picks untouched. Galaxy branch untouched (already mas-first). MINOR — changes stretch selection, not the scoring model.
⚠ Canary: M 42 is the golden nebula baseline — this flips its final (stf→mas). Needs a canary re-baseline + Henry sign-off after the full-pipeline confirmation run.
---
1.11.2 — 2026-06-30 (patch)
Bounded mid-run stretch vision tiebreak — DEFAULT OFF pilot (settings.stretch_vision_tiebreak, default false). When enabled, the nebula stretch picker — on a *genuine close call* (top two variants share the same sky and exposure bucket, so grain alone separated them) — makes one cheap Haiku vision compare of the two previews and lets it break the tie. Any failure / physics-only mode keeps the physics pick. Targets exactly the aesthetic call physics is weakest at (the M 57 grain-vs-placement case) without re-scoring any step.
Default behaviour unchanged (flag off → zero API calls, identical picks). Uses a cheap vision API call, deliberately *not* the headless-skill mechanism (which is reserved for the deferred weekly batch-eval — see below — to avoid hot-path latency + Max-session-quota contention). This is the mid-run half of the critique feedback system; the deferred half is the new weekly /critique-batch-eval (meta-analysis of accumulated critiques → proposed physics-rule fixes, on the shared memory-tidy.timer).
To pilot: set stretch_vision_tiebreak: true, run a few nebula targets, compare finals + verify the tiebreak picks against the faithful harness, then decide on default. PATCH — opt-in, no default-run output change.
PILOT RESULT (2026-06-30): FAILED the bar — stays default-off. Phase 1 (scripts/stretch_tiebreak_phase1.py): 33% fire rate, 0% fallback, 3/4 disagreements → gates passed. Phase 2 (blind randomized stretch-preview A/B): 0 win / 1 tie / 1 loss — Henry preferred the physics pick on NGC 6334 (a thin stack, depth 0.00; the tiebreak chose grainier for "better sky", same failure mode as M 57). Not promoted; code stays as a harmless opt-in. The real fix remains the noted depth-aware grain refinement. See memory stretch-picker-ab-faithful.
---
1.11.1 — 2026-06-29 (patch)
Preserve the celestial WCS through color_calibration (orientation fix). The PI color-cal path drops the astropy-readable WCS from its output; the preview renderer flips to north-up using the WCS (CDELT2 sign), so a WCS-less color-cal output renders vertically flipped vs the WCS-bearing background_extraction stage — the "image looks wrong after color calibration" bug (caught on M 42). M 42 hits it because it's drizzled (1.19"/px) → SSSC no-ops → PI SPCC/CC fallback (the WCS-dropping path).
Fix: _preserve_celestial_wcs(src, dst) re-injects the input WCS into the color-cal output when missing (no-op if a real WCS is present), called at all three spcc() success returns. Empirically verified: WCS-injected color-cal preview matches the bge preview as-is (corr 1.000) instead of flipud (1.000). Final scored pixels unchanged (WCS is header + preview); golden M 42 is drizzled so downstream xp/nb no-ops regardless. On native nebulae this restores a usable post-color-cal WCS (latent improvement — monitor). Takes effect on next seestar.service restart (deferred; M 42 canary + 9 requeues mid-run on old code).
---
1.11.0 — 2026-06-29 (minor)
Nebula stretch-picker: separate sky-placement from highlight-health so grain actually breaks ties. The nebula _neb_key lumped under+blown (highlight health) into the same "placement" term as sky-band distance at 0.02 resolution. On faint LP nebulae the large, variable under term spread candidates across many buckets, so the grain tiebreaker never fired and the most aggressive/grainiest in-band stretch won (e.g. IC 1805 veralux_strong grain 0.808 beating stat 0.165 at equal sky placement).
New key: (sky_bucket@0.02, health_bucket@0.05, grain, pref) — sky placement primary (Ha corners can't be pulled back downstream), highlight health a coarse secondary, grain the real tiebreaker.
Surfaced by the daily /critique-pending pass. VALIDATION CORRECTION (2026-06-30): the initial A/B harness used depth=1.0, which mispredicted picks on thin stacks (the picker's under term scales with integration depth) — only 1/9 matched live runs, so the original "7 cleaner / 2 grainier" numbers here were unsound. Re-validated with a FAITHFUL harness (scripts/stretch_picker_ab.py, now recomputes real depth via _stack_depth_factor and self-checks that the new-key replay reproduces each run's recorded pick). On the trustworthy (faith=OK, current-code) runs 1.11.0 is net-positive: 5 cleaner (NGC 6914 0.58→0.16, NGC 2244 0.40→0.14, IC 1396 0.53→0.27, SH2-101 0.14→0.08, NGC 7000 0.18→0.17), 1 grainier (M 57, a thin stack depth 0.29, where sky/health placement legitimately overrode grain), rest neutral. Golden M 42 pick unchanged (stf). Galaxy/globular branches untouched. MINOR — changes stretch selection, not the scoring model/steps. See memory stretch-picker-ab-faithful. Open refinement: depth-aware grain weighting so thin stacks don't pick a much-grainier variant.
---
1.10.3 — 2026-06-29 (patch)
S50 plate scale corrected in image_analyzer.py (2.55 → 2.37 arcsec/px). The native SeeStar S50 plate scale is 2.37"/px (measured from plate-solved WCS on native M 13/M 51 stacks; physics: 2.9µm IMX462 px / 250mm FL ≈ 2.39; drizzle = 1.19"/px). psf_arcsec/psf_diameter (consumed by tool_params.py for BXT nonstellar PSF) is now ~7% smaller and more accurate.
Surfaced by the weekly memory-tidy pass, which flagged the plate scale as "wrong" based on a stale reference-s50-pixel-scale memory claiming 2.9"/px. Ground truth reversed that: 2.9 was the pixel size in microns, not the plate scale. agent.py's 2.4 was already correct; the memory was fixed.
---
1.10.2 — 2026-06-29 (patch)
SyQon Prism Mini added as experiment variant for noise_reduction. Wires syqon_prism_denoise() in seti_astro.py — pure in-process numpy path via prism_denoise_rgb01(); no subprocess, no GUI objects needed. Model weights (376 MB) downloaded to ~/.local/share/SASpro/runtime/py312/models/syqon_denoise/prism_mini.pt from the SASpro 1.18 GitHub release. Smoke-tested on a synthetic 128×128 image (CPU, variant=free, output shape confirmed).
Two experiment variants registered in the ontology: prism_mini_gentle (strength=0.65) and prism_mini_standard (strength=0.85). Standard runs are unchanged — cc_denoise_inprocess (CosmicClarity) remains the seti_astro_fn default.
Parallax (SyQon's BXT/deconv replacement): the architecture in SASpro 1.18.0 is still a placeholder passthrough (_build_PlaceholderParallaxNet()) — no real weights can be integrated yet. The PI hold on BXT remains.
---
1.10.1 — 2026-06-21 (patch)
SSSC registered as an experiment-only color_calibration variant. Phase C3 validation ran SSSC vs SPCC on five broadband stacks at the color-calibration stage:
| Target | Type | XP stars matched | SSSC ran? | Corner sky B/R (SPCC → SSSC) | |--------|------|------------------|-----------|------------------------------| | M 13 | globular | 84 (Stage 2) | yes | 1.012 → 1.003 | | NGC 188 | open cluster | 45 (Stage 1) | yes | 1.003 → 0.974 | | M 3 | globular | 31 (Stage 1) | yes | 0.960 → 0.988 | | M 51 | galaxy | 15 | no — SPCC fallback | — | | M 42 | emission nebula | 5 | no — SPCC fallback | — |
SSSC broadband only fires on star-rich fields (the ≥20 XP-match gate); galaxies and nebulae — the bulk of broadband targets — fall back to SPCC. Where it does run it ties SPCC's sky neutrality (~0.01–0.03), no improvement. So the **broadband default stays SPCC; the LP/dual-band default stays SSSC** (shipped 1.9.0, where SPCC's broadband white reference is physically wrong). SSSC is registered as a color_calibration experiment variant (cc_sssc, alongside cc_spcc/cc_pi/cc_none) so it can be A/B'd on star-rich targets, but it is not the broadband default.
Classified PATCH: experiment_variants are consumed only when a color_calibration experiment is explicitly run, so standard runs are byte-identical (canary: golden trio no drift). This is an additive optional capability, not a default flip — a future broadband flip to SSSC would be a separate MINOR + canary re-baseline. Henry chose "experiment only" 2026-06-21.
processing_ontology.json:color_calibration.experiment_variantsadded; step note
updated to document broadband=SPCC / LP=SSSC defaults and the experiment-only status
of
cc_sssc.---
1.10.0 — 2026-06-13 (minor)
background_neutralize blue-cast fix. IC 1805's 1.9.0 final carried a measured dark-corner blue cast (sky B/R 1.26) that desaturated the Hα toward brownish-red. Two compounding bugs let it through:
1. Wrong measurement region. The completeness pass sampled the single auto_rect_50x50 patch. On frame-filling targets (the Heart fills the centre) that patch lands on an already-neutral spot, so the step was a no-op while the dark corners still carried the cast (IC 1805 corner B/R 1.25 survived). It now measures the four dark corners — the same region image_analyzer gates the step on — and subtracts the per-channel offset that drives each corner median onto the darkest channel's, neutralising the corner sky to grey. Verified: IC 1805 corner B/R 1.25→1.00, G/R 0.95→1.00 in all three pivot modes. 2. Colour step gated on gradient. _objective_check lumped background_neutralize with background_extraction and required a gradient drop (grad_drop > 0). A colour correction doesn't change the gradient, so when the gradient was already flat the step was always rejected (IC 1805 log: "gradient 1.000→1.000, 0% drop → rejected"). It now has its own branch gated on sky-cast improvement (cast_drop > 0.02, SNR hold). background_extraction keeps the gradient gate and the introduces-a-cast reject guard.
M51/M13 corners are already neutral post-SPCC (no-op), but **M42 carries a mild residual corner cast** (imbal 0.07–0.14; its neutralize was *skipped* in baseline) that the now-working step corrects — a deliberate improvement that shifts the M42 golden baseline. Hence a minor bump with a canary re-baseline (Henry chose the general scope over an IC-1805-only surgical fix); M42 before/after signed off before the new baseline is committed.
1.9.0 — 2026-06-12 (minor)
**NBExtract + SSSC — true narrowband workflow for LP dual-band data + spectrophotometric color calibration** (the two SASpro 1.18 flagship features, unblocked by the 2026-06-11 engine promotion and the Gaia XP spectral library install, 16 files / 62 GB, G<14).
The SeeStar "LP" filter is dual-narrowband (Hα 20nm + OIII 30nm), but until now the NBN branch only *approximated* channel separation (Hα≈R, OIII≈B via ha_dominance_ratio), and LP data had no valid color calibration — SPCC's broadband white reference is wrong for dual-band light.
Changes:
1. xp_stars.py (NEW) — shared Gaia-XP plumbing: SEP star detection → WCS px→sky → XP-library match (reprojected dist² < 9 px gate) → SASpro raw-aperture RGB photometry → spectra enrichment for both consumers. probe <fits> and --selftest CLI (synthetic mixing-matrix recovery corr 1.0; synthetic R(λ) solve RMS 0.0015). 2. NBExtract — xp_channel_extract (two-phase): the mixing matrix A is fit on the star-full post-color_calibration image, then NNLS-applied to the starless linear image at the stretch boundary, so the extracted xp_ha/xp_oiii channels are nebula-only. Fit deliberately happens after color calibration: SSSC rescales channels per-pixel, so fit and apply must see the same color domain. 3. nb_palette (NEW step + module) — composites the true channels into palettes: HOO, Foraxx (dynamic G mix), synthetic SHO (S = 0.8·Hα). Force-only aesthetic step with 4 experiment variants (nbp_hoo, nbp_foraxx, nbp_sho_gold, nbp_hoo_aggressive); never score-gated. Replaces the narrowband_norm approximation path when forced. - Black-sky compositing (required — Henry, 2026-06-12): each channel carries its OWN background pedestal (NNLS dumps the B-channel sky offset into OIII), so a shared blackpoint left a COLORED sky (navy/teal wash). The compositor now anchors per channel — low-percentile sky → 0 before the stretch, re-anchored to a small floor after — and solves the midtone from the signal high-percentile (→ 0.80), not the sky median (anchoring the median targets the sky, which both tinted the background and under-stretched the nebula). Synthetic bicolor proof: sky RGB = [0,0,0] with structure preserved. - Extreme-flux-ratio fallback (NB_PALETTE_MAX_FLUX_RATIO = 20): a bicolor palette needs a real OIII channel. On strongly Hα-dominant fields (IC 1805: ratio 157) NNLS correctly attributes ALL structure to Hα and leaves OIII a flat noise pedestal (contrast 0.89 vs Hα 3.06, no morphology) — compositing it can never produce a black sky. Above the threshold nb_palette is skipped and the proven narrowband_norm path (Hα≈R, OIII≈G/B + o3_boost) handles the field. nb_palette is for genuine bicolor targets (Rosette, Veil) only. 4. SSSC — sssc_calibrate — solves the camera system response from matched XP star spectra (SASpro Stages 1–3) and applies the Stage-2 quadratic gains. Real Bayer throughput curve shapes are mandatory — identity curves degenerate to a gray-world fit that forces R=G=B (found in design; selftest proves the corrected math). Broadband = SONY_COLOR_SENSOR_*-UVIRCUT; LP = SONY_CMOS_*-UVIRCUT × OPT. L-eNhance (closest published analogue to the SeeStar's internal dual-band). SessionResponseCache seeds each solve from the previous one (verified live: IC 1805 second run seeded from prior ctrl_points). Quality gate: applied-stage (Stage-2 sigma-clipped) RMS < 2.0 — mirrors SASpro's own prior-trust threshold; the headline residual_rms is the unclipped all-star value and is outlier-dominated on nebula-rich fields. 5. LP routing (new default) — color_calibration on LP data tries SSSC first; any failure falls through to the legacy PI SPCC/CC path unchanged. SSSC success writes .sssc_applied and counts as calibration success for the SCNR-skip and linked-stretch rules (preserve-linked-color holds). **Broadband SSSC ships experiment-only**: the M 13 side-by-side showed a green core shift vs SPCC's neutral — the default flip waits for C3 validation (separate bump + canary re-baseline). SSSC results are exempt from the whole-frame SNR objective gate (and get the lenient catastrophic-only stats-veto floor): the solved per-channel gains (k_R ≈ 2.26 on LP data) legitimately shift the SNR metric, which vetoed a correct calibration on the first live integration run (IC 1805, SNR×0.81 vs the 0.90 gate). The real quality check is the solver's applied-stage RMS < 2.0 gate, mirroring the crop precedent of not second-guessing a deliberate transform with whole-frame stats. 6. Latent gate bug fixed — LP detection now happens ONCE on the source stack header (_is_lp_run): the crop step strips FILTER from run-dir intermediates, so the old mid-pipeline header sniffs (xp hook, spcc_lp_filter param) would have silently no-opped on every real run.
Verified headless: xp_stars selftest; IC 1805 LP end-to-end (280 XP stars, Stage 3, applied RMS 0.888, 60 s, cache-seeded on rerun); M 13 broadband (84 stars, Stage 2, RMS 0.586, 6 s). Old-drizzle stacks (1.19″/px WCS/row-order mismatch) fail the star-match gate closed and no-op the XP machinery by design.
Critiques: none — feature work from the 2026-06-11 plan, not critique-driven.
1.8.0 — 2026-06-10 (minor)
NBN vibrancy restored: fix the silent revert of the nbn saturation pass. The 1.7.0 NBN runs on IC 1805 (5.8) and NGC 2244 (7.2) rendered steel-grey instead of the pathfound vibrant gold/teal SHO variants (IC_o3_1p5_sat / RO_o3_1p3_sat, trials 2026-06-08). The pathfound chain *did* run — narrowband_norm then color_boost preset=nbn — but the saturation pass was thrown away by two compounding bugs, with the step still logging "applied" (output bit-identical to input on both runs).
Root cause. NBN's step input was a non-float working copy, so the pi_postprocess.js "Ensure Float32" block fired — but it used SampleFormatConversion.To32Bit, which is 32-bit unsigned integer, so PI saved the NBN output as full-range uint32 (0..4.29e9). The nbn color_boost (the SOLE saturation pass — hoo_boost=0 by design) then ran correctly, but its midtone _lum_mask_blend computed the mask on the raw uint32 luminance: every pixel sat far above the 0.85 upper bound, the mask was 0 everywhere, and the blend replaced the boosted output with the original. NBN itself balances Hα/OIII to near-neutral (bright-region saturation 0.402 → 0.198), so losing the restore pass is exactly what produced the muted palette. (The earlier critiques mis-framed this as an NBN palette inversion — the SHO palette is the intended aesthetic; aesthetic steps are not score-gated.)
Changes: 1. pi_postprocess.js — ensure-float block: To32Bit → ToFloat (IEEE 32-bit float). 2. experiments.py _lum_mask_blend — min-max normalize original/processed to [0,1] when out of range before computing the mask and blending, so an integer-scaled input can never zero the mask again. 3. auto_process.py — the nbn color_boost preset is exempt from the midtone lum-mask blend: the pathfound look was generated unmasked, and the mask's upper bound (~0.85) would exclude the bright cores where the restored saturation matters most. Galaxy/nebula presets keep the mask. The skip is logged. 4. seti_astro.py narrowband_normalize — safety net: any non-float or out-of-range PI output is rewritten as normalized float32 before returning to the pipeline.
Critiques: 20260610_114522_seestar_nebula (IC 1805), 20260610_101238_seestar_nebula (NGC 2244) — both to be revised: palette intended, defect was the reverted saturation pass.
Also in 1.8.0 — the three approved 2026-06-10 critique recommendations:
5. hdr_core_blend — masked-core HDR blend (new step; M 42 + M 31 critiques). Global hdr_compression recovers the blown core but crushes faint signal — the "no improvement" objective veto fires and the step never lands. New seti_astro.hdr_core_blend runs the same compute_wavescale_hdr engine but blends it only under a feathered core luminance mask (M = gaussian(clip((gaussian(L,6)-thr)/(0.97-thr)), 20); thr 0.72 nebula / 0.80 galaxy, set from object_type). Blend is chroma-preserving (luminance-ratio), with a clipped-hue refill: above L≈0.78 the stored pixel ratios are already washed near-neutral (M 31 bulge measured R/G 1.00 at the center vs 1.06–1.10 at r≥100 px), so dimming alone reveals grey — chroma is refilled from the surrounding well-exposed region via normalized convolution (σ=100, weights = 1−clipness). Added to seestar_galaxy and seestar_nebula optional_nonlinear_steps (galaxy path previously had no HDR step at all); reuses the existing core-clip-fraction gate; exempt from the whole-frame stats veto (touches 0.15–0.9% of frame — same precedent as crop). Verified: M 42 wisp/dark-lane recovery matches the approved prototype; M 31 bulge dims to structure with warm hue held.
6. Folio-aware stretch pick (nebula branch) (M 42 critique). The 1.7.0 detail-first picker chose mas for M 42 with corner sky 0.122 — "in band" for the wide per-type emission range (0.06–0.16) but 42% over the folio's bg_level_range [0.05, 0.09]; cost −0.2. _physics_pick_stretch now takes folio_band and charges the distance outside the folio band into the nebula placement key. Galaxy branch unchanged (deliberately ignores sky; sky_mute_masked handles it). Verified on the M 42 run's 9 real variants: pick flips mas (0.122) → stf (0.050, in-band, lower grain).
7. sky_green_rebalance — sky-only masked green cap (new step; M 31 + M 42 critiques). The SPCC-success rule skips SCNR, so small residual sky greens go unpolished (M 31 sky G/R 1.12, M 42 1.06). New seti_astro.sky_green_rebalance neutralizes green only in a sigma-clipped sky mask, leaving object signal and SPCC star colors untouched. Design notes from testing: the cap is max(R,B) not the SCNR-style average (average inverted M 31's weak-blue sky to magenta, G/R 0.84), and the excess field is computed on σ=6-smoothed channels so the pointwise cap can't clamp just the high noise tail and drag the median below neutral. Added to both galaxy and nebula optional lists. Verified: sky G/R 1.045→1.012 (M 42), 1.061→1.010 (M 31); bright-region max diff ≤0.012. Full SCNR on SPCC data remains forbidden (IC 434 precedent).
---
1.7.0 — 2026-06-10 (minor)
Galaxy workflow: detail-first stretch selection + masked sky mute. Henry's directive from the M 51 mas review: *"the key is to pick the good galaxy stretch and not worry about the sky."* Galaxy detail looked excellent on PI MaskedStretch (mas), but the picker could never choose it — and the downstream sky handling dimmed the very outskirts the good stretch preserved.
(1) Galaxy branch in the physics stretch picker. For object_type containing "galaxy", _physics_pick_stretch now ignores all sky terms (bg_dist, bg_noise, rel_grain, and the 1.6.0 grain-cap) — every one of them worked against the faint-preserving stretches: mas/veralux leave the sky high (big bg_dist) and the grain-cap cut them on sky-corner σ, crowning ghs_strong, which crushes faint arms and tidal bridges. Selection is now highlight health only (under + blown, bucketed 0.05) + the dead-channel reject, tie-broken by a faint-preserving preference order (mas > veralux_strong > veralux > stat_bright > stat > stf > ghs family). **Physics only — no Claude API call** (Henry's explicit constraint). Validated against an in-session vision ground truth on 4 galaxies: M 51 / M 81 / M 101 → mas, NGC 4565 → veralux family (its mas is nearly black; the health gate rejects it) — 4/4 reproduced offline. Nebula and non-galaxy branches unchanged.
(2) MAS output normalized to 0–1. PI MaskedStretch writes int32-scaled FITS (BITPIX=32, BZERO=2147483648, values to 4.27e9). Previews auto-normalize so it *looked* fine, but stats read p99≈2.8e9 — mas had never actually been selectable. _run_mas now divides by max (when max > 1.5), clips to 0–1, writes back float32 with BZERO/BSCALE/DATAMIN/DATAMAX stripped.
(3) sky_mute_masked replaces the flat sky mute on the galaxy path. The old _mute_sky scaled the whole frame down, dimming faint outskirts along with the sky. The new seti_astro.sky_mute_masked auto-tunes a luminance mask: σ-clipped sky estimate (5 iters, clip at med+2.5σ), smoothstep mask from sky+1.5σ → sky+5σ, gaussian feather (σ3), dilation blend to protect halo edges, then a black-point lift (bp = sky−1σ) darkens only the sky; the galaxy composites through untouched. M 51 test: sky 0.018 → 0.001, galaxy coverage 11.6%. Step record stays sky_mute; non-galaxy types keep the flat mute.
(4) color_boost — no change needed. The existing _lum_mask_blend midtone mask (lower = clamp(median×1.3, 0.08, 0.30), computed from the post-stretch image) already luminance-gates saturation: measured on mas M 51, mask lower 0.194 vs sky 0.149 — only the galaxy's ~10% of pixels receive the boost, so faint outskirts aren't chroma-amplified.
(5) color_boost delta smoothing — posterized color fix. Henry's zoomed M 51 report: "the color is too blotchy and abrupt." Stage-by-stage comparison localized it to color_boost: hue is noisy in faint regions, so the per-pixel hue-Gaussian delta flipped adjacent pixels between the +0.30 blue boost and the −0.05 yellow damp — a posterized orange/purple patchwork. The saturation delta field is now smoothed (gaussian_filter(σ=4)) before applying — luminance detail untouched, boost transitions spatially gradual. Verified on the M 51 mas stretch: speckle gone at σ4, blue arm boost preserved (σ2 leaves residual speckle, σ6 adds nothing).
(6) Manual crop editor apply fix. Both of Henry's manual crops (M 31 −17°, M 42 0°) landed misaligned: the editor preview is flipped vertically for north-up stacks (CDELT2 > 0) and Cropper.js reports x/y in the rotated bounding-canvas frame, but apply-crop mapped both straight into the unflipped/unrotated array. New target_crop.cropper_box_to_array un-rotates the canvas box center, un-flips the row, negates the rotation angle in array space, then scales to full resolution (main.review_apply_crop now uses it). Poisoned saved crops for M 31/M 42 cleared — both targets need a fresh manual crop + re-queue.
(7) Sky-only gradient metric — background_extraction was being wrongly rejected. On M 81, GraXpert flattened the sky *perfectly* (row-band span 0.00022 → 0.00003), yet the gate rejected it: the all-cells gradient metric is dominated by grid cells sitting on M 81/M 82, and SPCC's bg_anchor drops the denominator pedestal, so the metric read 0.176 → 0.248. The uncorrected gradient was then amplified 9× by the mas stretch into the hazy "green cast" top Henry flagged. New gradient_severity_sky (image_analyzer) measures flatness over the object-excluded bg cells only (M 81: 0.085 → 0.006 — correctly accepted); the BE/background_neutralize gate, the physics gradient score, and the generic post-step stats veto now prefer it (the veto was a second independent guard on the old metric — it reverted the M 81 GraXpert result even after the gate fix). M 81's weak sky mute (sky σ ±0.066) was a downstream symptom of the same uncorrected gradient.
(8) green_cap_masked — post-SPCC lime cast on galaxies. With the gradient fixed, M 81 still rendered yellow-green (grader: color_balance 3.5, "bulge should be warm golden/orange"). Trace: SPCC's linked white balance neutralises the sky perfectly but leaves the galaxy *signal* ~10% green-heavy on S50 OSC data (sky-subtracted galaxy G/R 1.04–1.12 post-SPCC; an Sab bulge should be ~0.9), and the mas stretch amplifies that into the lime wash. New green_cap_masked (seti_astro) runs on the linear starless layer right after star split, galaxies only: feathered luminance mask + sky-subtracted green cap G_sig → min(G_sig, (R_sig+B_sig)/2) at amount 0.7 (Henry's A/B pick: G/R 1.10 → 1.00, warm cream bulge with blue arm separation). The sky pedestal is byte-identical on output and star colors keep SPCC calibration, so the SPCC-success SCNR-skip rule (IC 434 sky-crush) is not violated. Self-gates — passes through unchanged when measured signal G/R ≤ 1.02.
(9) Hα/HII knot boost in the galaxy color_boost preset. Henry: "Are we able to boost the Ha signal more in the galaxy?" The galaxy preset only boosted blue (240°/280°) plus a tiny 10° red band, leaving the pink HII knots washed out next to the Reddit 42 h reference. Added a (340°, 25°, +0.22) pink-magenta saturation band plus an 8% red-channel lift where that band fires — computed on the *original* hue field with the same σ4 delta smoothing as the main boost, so it cannot re-introduce posterized edges. Henry's pick from the M 51 arm-zoom A/B (+0.22 + red lift).
Minor bump: gate/param tuning + an implementation swap on the existing sky_mute slot; no scoring step added/removed, scores remain comparable.
1.6.1 — 2026-06-09 (patch)
Three crop/queue bug fixes — no expected change to good-run output.
(3) A user-chosen crop was being discarded by the objective SNR veto. Henry asked "did it discard my crop?" for M 51 — its coverage edge-crop (no rotation) was force-applied by geometry, then the generic post-step stats-veto reverted it because whole-frame SNR dropped 102.7→59.0 (0.57, below the 0.85 guard), logging "crop — no improvement, skipped." Cropping reframes the image (different pixel population), so a before/after SNR comparison is meaningless. Per Henry's directive ("Don't veto crop. I am choosing it"), crop is now fully exempt from objective gating: _objective_check("crop") returns ok/improved=True, and the post-step stats-veto skips step_name == "crop". A crop chosen in the manual review is always applied — the pipeline never second-guesses framing with stats.
(1) Manual crop rotation left black corner wedges. Henry added a small rotation in the Cropper.js editor and the crop "wouldn't reset to a rectangle." The old apply-crop path cropped the axis-aligned box *first*, then scipy.ndimage.rotate(reshape=True, cval=0.0) rotated that box inside a larger canvas — leaving zero-filled triangular wedges in the corners. Fixed with rotate-then-crop: new target_crop.rotated_crop(data, cx, cy, cw, ch, rotate_deg) uses scipy.ndimage.affine_transform to inverse-map each output pixel into the rotated source, so the whole cw×ch box is filled with real pixels. Shared by the /review/{id}/apply-crop endpoint and the saved-crop fractional fallback (target_crop._apply_frac_crop). Rotation direction preserved (matches the prior scipy.ndimage.rotate(-rotate), Cropper.js +angle = CW; verified ≤1px). The WCS sky-box reuse path was already correct (folds rotation into PA), so this only touches the immediate winner FITS and the no-WCS fallback.
(2) Park-on-review didn't advance the queue. When M 31's crop review grace elapsed it parked (released its queue slot), but its auto_process thread kept holding PIPELINE_LOCK while blocked at crop_review ev.wait(). The worker popped the next target (M 51), which logged "starting" then stalled ~40 min on the lock until M 31 fully finished. Fix: PIPELINE_LOCK now tracks its owner thread (auto_process.mark_pipeline_lock_held / clear_pipeline_lock_held / release_pipeline_lock_for_park / reacquire_pipeline_lock_after_park); crop_review._await_decision releases the lock when a review parks and re-acquires it before resuming, so the next local job runs immediately while the idle parked review waits — real PI execution stays serialized. The laptop worker calls auto_process without the lock, so the release is owner-guarded (no-op there).
---
1.6.0 — 2026-06-08 (minor)
NBN vibrancy overhaul — real OIII boost on the HOO palette. Surfaced by Henry's investigation into why auto nbn runs (IC 1805, NGC 2244) came out muddy grey-tan instead of the vibrant blue-OIII / warm-Ha palette he gets by hand. Two latent no-op bugs were the cause: (1) the pipeline set nbn.normalizationMode, which does not exist in PI 1.9.3 — so o3Boost sat at its 1.0 default and the teal OIII core never surfaced; (2) the color_boost step ran with the galaxy preset because the standard param build reads ontology parameters while the key is actually params. Both fixed: o3Boost is now auto-scaled from Hα dominance and a dedicated warm-gold/teal nbn saturation preset is routed in. NBN is an aesthetic step and is not score-gated, so the scoring model is unchanged — 1.5.0 and 1.6.0 critiques remain comparable for non-nbn runs. Validated on IC 1805 (ratio 2.54 → o3Boost 1.50) and Rosette (2.16 → 1.35) trials matching Henry's reference image.
- NBN o3Boost lever (
pi_postprocess.js): the NarrowbandNormalization block now
sets - Ratio-driven o3Boost (
auto_process.py): narrowband_norm auto-scales - NBN color_boost preset (
seti_astro.py+auto_process.py+ ontology): new - Threaded params:
nbn_o3_boost+nbn_hoo_boostthrough
nbn.palette=0 (HOO) and nbn.o3Boost=job.nbn_o3_boost (the real OIII-boost
property), dropping the dead nbn.normalizationMode assignment. o3Boost is the lever
that surfaces the teal-blue OIII core; left at 1.0 it produced muddy yellow-brown.
o3Boost = clamp(0.50 + 0.395·ha_dominance_ratio, 1.25, 1.7) — a more Hα-dominant
target gets more OIII boost to surface its weak teal core. Curve fitted to IC 1805
(2.54→1.50) and Rosette (2.16→1.35) visual sweet spots. Manual overrides
nbn_o3_soft(1.3)/nbn_o3_strong(1.7). Measured ratio + chosen boost logged per run.
nbn preset (gold/copper dust 35°/+0.40, amber bleed 18°/+0.22, teal OIII core
195°/+0.32, global_sat_lift 0.06) routed in explicitly whenever narrowband_norm is
forced — fixing the latent params/parameters key-mismatch that left nbn runs on
the galaxy preset. This is now the sole saturation pass for nbn; the in-JS HOO
ColorSaturation restore is disabled (hoo_boost=0) to avoid double/triple saturation.
pixinsight.run_postprocess; o3_boost + hoo_boost through
seti_astro.narrowband_normalize. The /narrowband_norm branch endpoint method=
now means auto (ratio-scaled) | soft (1.3) | strong (1.7) instead of the dead
MaximumStars/Equalize.Stretch picker + crop review (folded into 1.6.0, no separate bump). Added in the same uncommitted, not-yet-run 1.6.0 window. Both are aesthetic/framing choices outside the score-gated model, so comparability is unaffected.
- Nebula stretch picker (
auto_process._physics_pick_stretch): for nebula
object_types the pick is now dominated by sky placement — the variant whose
background sits closest to the per-type band with the least under-exposed/blown
clipping wins ( - Galaxy/non-nebula stretch grain-cap (
auto_process._physics_pick_stretch): before
the blended-cost sort, drop any candidate whose grain ( - Crop is now a remembered manual review (Claude veto removed): on a target's first auto-process the crop step opens a manual review (
- Crop clear/redo controls: the saved crop can be cleared from the target page ("Clear & redo crop" →
key = round((bg_dist+under+blown)/0.02)), and relative grain only
breaks ties within that bucket. Previously grain could win outright and select an
STF-overshoot variant that washed the sky bright. Galaxy/other types use the blended
cost plus the new grain-cap below.
bg_noise + 0.25·rel_grain)
exceeds 1.5× the cleanest IN-BAND candidate's grain, then take the lowest cost among
survivors. Fixes the M 51 6/9 regression: veralux (grain 0.248) beat the cleaner
stat_bright (0.145) — both ON-TARGET sky — because veralux's brighter midtones lowered
its under-exposure term more than noise_w·bg_noise charged its grain; final noise
tanked to 5.5 and a non-degradation restore shipped an earlier checkpoint. Reference
grain comes from in-band candidates only, so a uniformly-grainy field (no clean in-band
option) is untouched. Opposite intent to the nebula change above (nebula corners hold
real Ha → de-weight grain; galaxies → discipline it). Critiques
20260609_021519_seestar_galaxy + 20260604_143439_seestar_galaxy.
crop_review.py)
presenting every crop_multi candidate (artifact-trim / canonical / coverage /
intersection / LIR), each with an STF preview, plus a Cropper.js editor to draw or
rotate a custom crop. The choice is persisted per target (target_crop.py /
target_crops table) as a WCS sky-box (center RA/Dec + on-sky size + PA, primary)
with fractional bounds + rotation as a no-WCS fallback, and reused on every later run
by reprojecting onto the saved box — identical framing every session regardless of
mosaic canvas growth. The old _claude_confirm_crop / _claude_crop_clips_subject
vision veto is deleted. The review honors the standard grace→park→wait queue pattern
(5-min grace, then releases the queue and waits indefinitely).
POST /target/{target}/clear-crop) so the next run re-opens
the review, and a per-run "Re-crop (force fresh crop review)" queue flag (re_crop on
/queue + the queue-view add-job checkbox) forces a one-off fresh review without
clearing the stored crop. Manual-crop geometry (incl. rotation) is persisted from the
apply-crop endpoint, which knows the exact applied box.---
1.5.0 — 2026-06-07 (minor)
Alignment outliers are dropped from stacking. Surfaced by the IC 1805 panel-drop investigation: one pre-EQ sub (RA 69.98/DEC 20.27, ~50° off-pointing) was a spoofed-coordinate frame polluting the max-frame mosaic. Each sub's TRUE plate-solved sky position/rotation is now stored in the DB and an off-cluster sub is excluded at stack-selection time. Frame selection changes for off-pointing data; the scoring model and every processing step are unchanged — no scoring step added or removed — so 1.4.0 and 1.5.0 critiques remain comparable for well-pointed data.
- Per-sub plate-solve capture:
light_filesgainssolved_ra/dec/rot/scale+ - Alignment-outlier flagging (
sub_solver.flag_alignment_outliers): per
(target/panel) group, a robust circular-median solved position; any sub > 2.0° off
is stamped - Stack-time outlier exclusion (
stacker.py): subs withsolve_status='outlier'
are dropped from - Rotation visibility (
alignment_summary): per-grouprot_median+rot_spread
(max deviation from median, deg) from
solve_status (ok|failed|outlier|NULL) + solved_at. Populated three ways —
harvesting the WCS the stack pipeline already solves (near-zero cost), an
idle-worker eager-solve, and on-demand POST /solve/{target}. Keys
association/alignment QA on TRUE solved position instead of the spoofable/colliding
header OBJECT name.
solve_status='outlier'. Does NOT touch the manual exclude column.
qualifying_paths after the exptime filter, gated by setting
exclude_alignment_outliers (default true). An off-pointing sub no longer enters
registration/integration.
solved_rot, so subs rotated relative to each
other are inspectable. Informational only — alt-az field rotation is expected and
corrected by registration, so rotation does not auto-exclude.---
1.4.0 — 2026-06-07 (minor)
A new artifact-trim crop candidate becomes the preferred crop. Surfaced by Henry's annotated review of IC 1805 (mosaic) and NGC 6334: the strict LIR clipped the subject on tilted mosaics, canonical over-sized bordered targets, and the existing coverage maps didn't even match the stack dimensions. The crop step is unchanged in identity and the scoring model is unchanged — no scoring step added or removed — so 1.3.0 and 1.4.0 critiques remain comparable.
- Artifact-trim crop candidate (preferred):
crop_multiadds
_artifact_trim_bounds — STF-stretches a luminance, then per edge scans inward and
trims while the band is anomalous by EITHER brightness (median below 0.55×plateau
= falloff/black band, or above plateau by 0.18 = residual gradient) OR
high-frequency noise (above 1.6× interior = grainy low-frame-coverage border).
Stops after a clean run, capped at 35% per edge, with a degenerate-box guard that
falls back to the full frame. Coverage-map-independent (works on linear stacks and
manual XISF masters). It keeps the FULL target while removing edge stacking
artifacts and is now chosen by default over canonical/LIR; canonical/coverage/lir
are still generated for the record and remain the fallback. Validated on IC 1805
master (full Heart kept, ~10% ragged bottom trimmed) and NGC 6334 (top 4% / bottom
25% / left 8% / right 13%). See [[feedback-crop-edge-artifacts]].---
1.3.0 — 2026-06-07 (minor)
Four fixes from Henry's review of the 1.2.0 reprocess batch. Gate/threshold/param tuning plus one conditional step skip — the SCNR skip is gated on the SPCC-success path (mirrors the existing narrowband SCNR skip), the scoring model is unchanged, and no scoring step is added or removed, so 1.2.0 and 1.3.0 critiques remain comparable.
- Crop over-crop floor (#102):
crop_multienforces a native-resolution floor
(short side ≥ - Crop under-crop gate (#103): the post-stretch crop-force check now measures corner brightness on a stretched luminance (1/99.5 percentile + gamma) instead of linear data, and adds a black-edge band detector (>30% near-black in a 2% edge band) — recovers vignetted/stacking-border finals (NGC 6914, IC 1805, M 83) the linear-threshold gate silently passed.
- Stretch background clamp (#104): after the stretch winner is picked, an additive sky-level clamp pulls the background into the per-object-type band when a variant (e.g.
- SCNR-on-SPCC skip + channel-crush guard (#105): SCNR is skipped on the SPCC-success path (
max(900px, 0.45 × native short)) and falls back to the largest-area
candidate when the chosen candidate (usually canonical, sized from folio angular size)
would downsample a small target below S50 resolution — fixes the ~76px M 57 / ~442px
M 109 thumbnails. See [[feedback-no-overcrop-s50]].
veralux_strong) overshoots above it, so the final sky lands in-band instead of
washed bright.
color_calibration applied and no .spcc_failed sentinel) because its non-linked
Average-Neutral green clip breaks SPCC's linked white balance and crushes the neutral
sky toward black (IC 434 / SH 2-273 R=0.0 / G=0.0019 "G/R=140" cast); SCNR still runs
when SPCC failed or never ran. A belt-and-suspenders _guard_channel_crush at final
save re-neutralises any residual coloured near-black pedestal left by a downstream step.---
1.2.0 — 2026-06-07 (minor)
Eight further quality fixes from a second pass over the same 20-critique batch. Gate/threshold/param/default-fn/prompt tuning only — the deconvolution default swaps engine (cc_sharpen → BXT) but the step itself is unchanged, the scoring model is unchanged, and no scoring step is added or removed, so 1.1.0 and 1.2.0 critiques remain comparable. (Recommendation #7, post-GHS sky pull, was already implemented in 1.1.x; #10 two-path HDR is deferred as a separate MAJOR initiative.)
- Crop gate (#1): force a crop when measured corner
spread/bg > 0.40
(vignette / uneven corners), not just on low coverage — recovers vignetted finals
(NGC 6914, M 83, NGC 6334).
- dark_enhance gate (#2): target-aware shadow-SNR floor (5 for galaxy/nebula vs 12 otherwise) so faint outer-halo / reflection veil is lifted instead of blanket-vetoed (IC 4592, M 109, NGC 4565, NGC 6914, SH 2-273).
- noise_reduction gate (#3): keep a denoise that holds SNR when the result is still grainy (
- deconvolution default (#4): standard-mode default is now
bxt_deconvolve(full
BlurXTerminator), best-in-class for SeeStar; - Scale hints (#5): inject S50
FOCALLEN=250mm+XPIXSZ/YPIXSZ=2.9µminto manual
XISF→FITS masters that lack them so ImageSolver/SPCC gets a scale seed within the
factor-2 window (IC 1805).
- halo_suppression cap (#6): cap
reduction_levelat 1 in dense star fields
(clusters or >1000 detected stars) so aggressive halo reduction doesn't eat stars /
darken the field (NGC 6914 Cygnus).
- Grader colour context (#8): feed the measured per-channel corner-sky R/G/B balance into
- NBN neutralize completeness (#9):
background_neutralizeadds a residual-cast
top-up that equalises sky-patch per-channel medians when a cast remains after the pivot
shift (NGC 2244 NBN sky B/R 1.46).
σ/bg > 0.18) instead of reverting on a marginal linear "no improvement"
(NGC 7635, SH 2-273).
cc_sharpen_inprocess remains the
automatic fallback when PixInsight is unavailable and an explicit experiment variant
(NGC 6914 star-ringing halos post-stretch).
assess_stacked_image so the vision grader stops missing real sky colour casts
(NGC 2244 NBN blue cast B/R 1.46).
---
1.1.0 — 2026-06-07 (minor)
Seven quality fixes from the first critique batch (19 critiques, runs 2026-06-05/06). Gate/threshold/param/prompt tuning only — no scoring step added or removed and the scoring model is unchanged, so 1.0.0 and 1.1.0 critiques remain comparable.
- HDR gate (#84): also fire on a small intensely-blown core (near-white fraction
- Stretch picker (#85): add a relative-grain (
bg_noise/bg_level) tiebreaker,
guarded to backgrounds above 0.04, to break ties away from grainy - Background gate (#86):
_objective_checknow rejects a background step that
*worsens* per-channel sky balance ( - Workflow routing (#87): resolve
object_typefrom DBtargets.typebefore the
name heuristic, fixing substring collisions (e.g. - NBN palette (#88): post-NarrowbandNormalization HOO
ColorSaturationrestore in - Grader context (#89): feed the measured background band into
- Data-integrity QA (#90):
_data_integrity_flagssurfaces wrong-field, pre-EQ
pointing-spoof, and manual-source provenance issues to log/Telegram and
lum≥0.98 ≥ 0.10%), not just a ≥1% top-decile fraction — recovers HDR on compact
blown cores like the M 42 Trapezium.
veralux_strong.
sky_b/r, sky_g/r) and flags residual cast for
a harder retry.
M 109 matching m 10 → globular).
pi_postprocess.js so genuine Hα/OIII signal isn't left muted grey-tan (measured
emission B/R 0.86–0.97 on M 42 / NGC 2244 NBN).
assess_stacked_image so it stops calling measured-correct backgrounds "too dark".
run.log so a
run isn't graded for problems it didn't cause.---
1.0.0 — 2026-06-05 (baseline)
First versioned reference point for the critique-driven iteration loop. No code changes — this establishes the line that subsequent runs are compared against.
- Critiques evaluated: none (baseline).
- Changes: none.