feat: lintle verify + interactive wizard (verify epic #148)#159
Merged
Conversation
…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>
This was referenced Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
lintle verify+ interactive wizard — the verify epic (#148)One branch lands the whole epic: the sgp4-free
verifyauditor (Increment 1), the fourfalse-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):tle.py);(catalog, epoch, element-set)yet carry adifferent orbital state; comparison is over parsed orbital values, not raw bytes;
--source) — every cleaned line must be a sanctioned edge editof 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.verifyorsgp4.Acceptance-run fixes (found by the #152 full-corpus run)
(leading space vs
+,00000-0vs+00000+0) read as a clash — ~3.2M false positives.Now compares parsed orbital values.
under the
-1sentinel.catalog_ofnow recovers them.false
VRFY-INTERIOR-MUT. Blank lines are now skipped (matching clean's pairing).flagged as hard contradictions (369,705 in R3). Now keyed on element-set: a new element-set
is a benign re-issue, counted in a
checked.epoch_reissuescensus; only a same-element-setclash is hard (~0 expected). Subsumes the verify: surface benign epoch re-issues as a summary census count #147 census.
Interactive wizard +
.lintle.json(#145)Bare
lintleon a TTY opens a configure/clean/verify/report menu; source/output paths areremembered in
./.lintle.json(precedence: explicit arg → stored config → default). Off a TTYit prints help and exits 2 — nothing that pipes
lintleever blocks. Newconfig.py(stdlib-JSON leaf) and
wizard.py(thin front-end overcli.main); thecli ↔ wizardcycleis broken by a lazy import on both sides.
Docs (#146, #151)
ARCHITECTURE.md§2 documents theverify/package,config.py,wizard.py, and theacyclic-import invariants;
README.mddocumentslintle verify, the wizard, and.lintle.json.Verification
uv run pytest→ 753 passed;ruff checkclean;ruff format --checkclean.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 sets9083/9096) and by the unit + end-to-end census tests here.
🤖 Generated with Claude Code