Skip to content

feat: lintle verify + interactive wizard (verify epic #148)#159

Merged
elfensky merged 9 commits into
developfrom
feature/wizard
Jul 10, 2026
Merged

feat: lintle verify + interactive wizard (verify epic #148)#159
elfensky merged 9 commits into
developfrom
feature/wizard

Conversation

@elfensky

Copy link
Copy Markdown
Owner

lintle verify + interactive wizard — the verify epic (#148)

One branch lands the whole epic: the sgp4-free verify auditor (Increment 1), the four
false-positive fixes found by the full-corpus acceptance run, the interactive wizard with
remembered paths, and the docs.

Closes #143, #145, #146, #147, #151, #154, #155, #157, #158.

lintle verify (#143)

Post-run correctness auditing of a clean run's <out-dir>/cleaned/ (never mutated):

  • re-validate every cleaned record against the one validator (tle.py);
  • contradiction — no two records may share (catalog, epoch, element-set) yet carry a
    different orbital state; comparison is over parsed orbital values, not raw bytes;
  • source byte-diff (with --source) — every cleaned line must be a sanctioned edge edit
    of a real source line; interior-column changes are corruption.

Constant memory: the group-by-satellite pass uses an external merge sort that spills to disk.
Deterministic suspects.jsonl + summary.{json,md} under <out-dir>/verify/. Exit 0/1/2.
An import-graph test bars the clean/validate/repair path from importing lintle.verify or
sgp4.

Acceptance-run fixes (found by the #152 full-corpus run)

Interactive wizard + .lintle.json (#145)

Bare lintle on a TTY opens a configure/clean/verify/report menu; source/output paths are
remembered in ./.lintle.json (precedence: explicit arg → stored config → default). Off a TTY
it prints help and exits 2 — nothing that pipes lintle ever blocks. New config.py
(stdlib-JSON leaf) and wizard.py (thin front-end over cli.main); the cli ↔ wizard cycle
is broken by a lazy import on both sides.

Docs (#146, #151)

ARCHITECTURE.md §2 documents the verify/ package, config.py, wizard.py, and the
acyclic-import invariants; README.md documents lintle verify, the wizard, and .lintle.json.

Verification

uv run pytest753 passed; ruff check clean; ruff format --check clean.

Note: the confirming R4 full-corpus re-run is deferred with #156 (stream suspects to disk); the
element-set semantics are proven on real bytes (catalog-5 04257.54105851, element sets
9083/9096) and by the unit + end-to-end census tests here.

🤖 Generated with Claude Code

elfensky and others added 9 commits July 6, 2026 15:45
…4-free)

A post-run auditor of a clean run's cleaned output, dispatched like the other
read-only consumers (diff/explain/report). Three exhaustive, sgp4-free checks:

- re-validate every cleaned pair through `tle.validate_record` (goal 3);
- flag any `(catalog, epoch)` shared with different element bytes (goal 3),
  detected over a constant-memory external merge sort that spills to disk;
- confirm every cleaned line is a *sanctioned* edit of a real source line —
  the exact five edge tidy-ups repair.py may apply — via a bounded-window
  source alignment; any interior-column change is a hard suspect (goal 1).
  The sanctioned transform is re-implemented independently (never calls
  repair.py) so a repair bug can't hide behind its own output.

Output is byte-deterministic: suspects sort by a stable key and serialize as
compact ASCII JSON under `<out-dir>/verify`. Exit 1 on any hard suspect, 0 on
clean-or-soft-only, 2 on operational error. Verify is a pure consumer — it
reuses the one validator and never re-defines validity; an import-graph test
bars the clean/validate/repair path from importing `lintle.verify` or `sgp4`.

Physics-based orbit consistency (goal 2, sgp4) is a later increment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er at runtime"

The guard was never about sgp4's size (0.55 MB, no numpy). It protects two
correctness rules: (#4) "perfect" is defined once in tle.py, and sgp4 is
permissive enough to become a divergent second definition of validity if the
clean path could consult it; and sgp4 measures physical position, not record
validity — an orthogonal concern kept in separate code. So the rule is that the
clean/validate/repair path never imports sgp4 or lintle.verify (now enforced by
an import-graph test), NOT that sgp4 is barred from the process entirely.

sgp4 becomes the physics engine of `lintle verify` — a consistency *measurer*,
never a validity authority (validity there still routes through
tle.validate_record). It is being promoted from dev-only test oracle to a
verify-scoped runtime dependency; the packaging move lands with the verify
physics pass (Increment 2), while the wall is already in place.

Reworded consistently across CLAUDE.md (tech stack, veto list, dependency
policy) and ARCHITECTURE.md (Critical Rule #4, §7 policy + invariant bullets +
considered table). Explicit and in history rather than smuggled in with code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elds

Real-data testing (a 232M-record run) showed `find_conflicts` flagged normal
space-track re-issues as hard `VRFY-EPOCH-CONFLICT`: the same satellite at the
same epoch with *identical* orbital elements, differing only in the element-set
number (line 1 cols 65-68), the revolution number (line 2 cols 64-68), or the
recomputed checksum. A 20k-record sample raised 30 such suspects — all false
positives; extrapolated to the full corpus that is ~350k bogus hard flags, the
exact noisy-verifier failure that gets a tool muted.

Compare orbital-state slices only — `line1[:64]` and `line2[:63]` — masking the
element-set/revolution numbers and both checksums. A genuinely different orbit
still differs. Admin-field corruption from cleaning is still caught: the goal-1
byte-diff sees those columns as interior mutations, and a bad checksum fails
re-validation. Tests added for both re-issue shapes (element-set + revolution
number) and the real-contradiction case is retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es (#154)

The #152 full-corpus run flagged ~3.2M false-positive VRFY-EPOCH-CONFLICT hard
suspects (~1.4% of 232M records). Root cause: find_conflicts compared the
orbital slice (line1[:64], line2[:63]) byte-for-byte, but TLE signed-exponential
fields are not byte-canonical — space-track re-issues the identical orbit with a
leading space vs an explicit "+" on a zero-valued field (nddot / B*), so
line1[:64] differs and the re-issue reads as a contradiction. Same noisy-verifier
failure bc8dbdc fixed for element-set/rev churn, one layer deeper (inside [:64]).

Confirmed on catalog 5 (Vanguard 1), epoch 05121.93225607: line 2 byte-identical,
line 1 differs only in the nddot sign encoding (' 00000-0' vs '+00000-0', both 0).

Fix: compare parsed numeric orbital *values* (_orbital_state) instead of raw
bytes — encoding-independent, admin fields (element-set / rev number / checksums
/ ephemeris type) simply absent from the tuple, with a raw-byte fallback for any
unparseable field so two genuinely different orbits can never collapse. Column
slices kept in lockstep with tle.py (Rule #4; validity still routes through
validate_record). A real orbital-value difference still flags.

Also unblocks #142 — dedup reuses the same masked orbital-identity comparison.

Tests: sign-encoding re-issue is not a conflict (reproduces #154), a genuine B*
value difference still conflicts; 727 -> 729 passing, ruff + format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#157)

The #152 run produced ~50K catalog=-1 VRFY-EPOCH-CONFLICT suspects — a distinct
false-positive class from #154. Root cause: space-track writes low catalog
numbers space-padded (' 2768', '  836') rather than zero-padded; cols 3-7 are
ALNUM_SPACE so validate_record accepts them, but tle.extract_norad_id demands 5
pure digits (a plain-int contract for the clean path's broken-id output) and
returns None -> verify assigns catalog=-1. Every such record then buckets under
one pseudo-satellite, and different satellites sharing an epoch_key look like
contradictions.

All 49,965 catalog=-1 suspects are space-padded numerics (no Alpha-5 — that needs
catalog >= 100000, absent from a 2004-2020 corpus).

Fix: verify-local catalog_of() strips the field before parsing (' 2768' -> 2768)
so records group under their real catalog. Scoped to verify —
tle.extract_norad_id is left untouched (its third caller is the clean path's
byte-deterministic broken-noradids.ndjson; changing it there is a separate
concern). Combined with #154's value comparison, the ~50K spurious conflicts
collapse; any residual is a genuine same-catalog/same-epoch difference.

Note: checks._anchor (the byte-diff resync identity) has the same divergence and
still uses extract_norad_id — a lead for #155, left for that investigation.

Tests: space-padded catalog recovered (reproduces #157), unparseable line still
sentinels to (-1, -1.0); 729 -> 731 passing, ruff + format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #152 run flagged 1,805 VRFY-INTERIOR-MUT hard suspects, all in tle2019
across 21 catalogs. Triage shows they are verify false positives, not clean
bugs: a region of tle2019 source has a stray blank line between line 1 and line
2 (and 68-char lines missing their checksum). clean handles this — its pairing
skips the blank and reconstructs the checksums, so the cleaned records exist and
are valid — but SourceAligner requires line 1 and line 2 to be adjacent in its
window (buf[i], buf[i+1]), so the interposed blank breaks the pair match and the
anchor fallback misreports INTERIOR_MUT.

Confirmed on real bytes: both source lines sanctioned-match their cleaned
(checksum-reconstructed) form; only the blank between them causes the miss
(catalog 10629 and 16007 both show the pattern; source ~line 11.9M).

Fix: _refill skips blank / whitespace-only source lines — never part of a TLE
record, exactly as clean's pairing treats them — so the real line-1 / line-2
pair is adjacent in the window again. Non-blank junk lines (the quarantine-gap
resync path) are unaffected.

Tests: a blank line between a checksum-reconstructed pair is now a clean match
(was INTERIOR_MUT); 731 -> 732 passing, ruff + format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inator (#158)

The contradiction check keyed only on (catalog, epoch) + orbital state, so
every space-track re-issue — a refined orbit solution republished at the same
epoch under a new element-set number — read as a hard VRFY-EPOCH-CONFLICT. The
R3 full-corpus run left 369,705 such flags across 34,388 satellites, all
benign, burying the ~0 genuine clashes and inflating peak RSS.

Add the element-set number (line-1 cols 65-68) as the discriminator, matching
space-track's own re-issue key:

  - different element-set, different orbit -> a benign re-issue: counted into
    an `epoch_reissues` census (a single summary number, per #147), never a
    per-record finding;
  - same element-set, different orbit -> a hard VRFY-EPOCH-CONFLICT (one
    element-set names exactly one orbit; a genuine integrity clash, ~0 expected).

`find_conflicts` now returns `(hard_conflicts, reissue_count)`; `run_verify`
threads the count into the `checked` census. Constant memory unchanged: only
the current group's element-set->state map is held. Side effect: the suspects
list no longer holds the 369K, dropping peak RSS further.

The decision and its structural rationale (clean already quarantines
different-catalog line-mixes via validate_record's catalog cross-check, so the
contradiction pass's unique target is narrow) are documented in issue #158.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running `lintle` with no subcommand on a TTY now opens a rich menu
(configure / clean / verify / report / quit). The chosen source and output
directories are remembered in a project-local `./.lintle.json` so the everyday
commands run without repeating paths:

- new `config.py` leaf — stdlib-JSON load/save of the two known keys, never an
  authority over an explicit arg;
- new `wizard.py` presentation leaf — a thin front-end that builds an argv and
  calls `cli.main`, reimplementing no orchestration (cli imports it lazily, so
  no import cycle);
- `clean`/`verify`/`report` resolve unset paths with precedence
  explicit CLI arg > stored config > built-in default; stored paths are
  re-checked and re-prompted when they have vanished.

Off a TTY (scripts, CI, pipes) a bare `lintle` keeps the old behaviour — prints
help and exits 2 — so nothing that pipes lintle ever blocks on a prompt. rich
is already a runtime dep and the wizard only runs on a TTY, so no new dependency
and no impact on structured/byte-deterministic output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…DME (#146, #151)

ARCHITECTURE.md §2 (#146): add the `verify/` package, `config.py`, and
`wizard.py` to the module tree and table, plus an acyclic-invariant paragraph —
the one-way `verify/` internal DAG (__init__ → checks/grouping/records/report,
records → epoch, checks → tle), the `cli → verify` edge that is never reversed,
the import-graph wall keeping the clean path free of `lintle.verify`/`sgp4`, and
the lazy-on-both-sides break of the `cli ↔ wizard` cycle.

README.md (#151): a "lintle verify" usage subsection (the three sgp4-free
checks, `--source` / `--no-source-diff`, the `<out-dir>/verify/` report, exit
codes 0/1/2) and an "Interactive wizard & .lintle.json" subsection (bare
`lintle` opens the TTY menu; remembered source/output paths; precedence
explicit arg > stored config > default; off-TTY prints help and exits 2).

CHANGELOG.md: sharpen the unreleased `verify` bullet to the post-#158 shape —
`(catalog, epoch, element-set)` contradiction over parsed orbital values, with
benign same-epoch re-issues counted in a census rather than flagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant