GitHub Hot Board

GitHub Hot Board · Repository archive

SpaceDataRig/space-data-rig

Autonomous five-seat AI science rig: reproducible exoplanet transit detection on NASA Exoplanet Archive data, with Solana mainnet provenance anchoring. (This description comes from the repository itself.)

Data at October 22SpaceDataRig

Best

#16

Underappreciated Gems

100

Total stars

6

Total forks

1

Days on boards (30d)

README

Space Data Rig

CI Release license: MIT status: live

At a glance

A five-stage deterministic pipeline (data-fetch -> anomaly-scout -> sim-runner -> mission-critic -> reporter) that runs a fully reproducible TESS transit-detection loop: real confirmed-planet parameters from the NASA Exoplanet Archive in, a deterministic Box-Least-Squares search and injection-recovery statistics through, a hash-sealed reproducible bundle anchored on Solana mainnet out. The deciding math is plain code, not a model; the live LLM layer is advisory narration only (about 2% of the code, and removing it changes zero verdicts -- measured at P70).

  • It is a closed-loop self-test, not a planet hunt. It proves the machinery recovers a known signal from noise. discoveryClaim is false and safeToGate is false, everywhere, by design.
  • Honest ceilings, reported as-is. On real TESS data at scale (P82, 425 curves): blind recall 86.2%, accepted-call precision 42.2% (a coherent-systematics wall a bigger sample narrows but does not remove); external blind validation KP 186/234, CP 103/179 (easiest subsets, named); the centroid F-test caught 0/59 labelled blends because 77% of the 500-target checkpoint never localises a cross-sector centroid offset -- pixel-baseline-bound, not a statistic problem. All of P82/P84/P85 were re-run on the corrected sky after an ExoFOP right-ascension unit bug (sexagesimal hours read as degrees) was fixed; the earlier "0/56 / 71%" figures were measured against the wrong field and are withdrawn (see Capability boundaries).
  • Independently auditable -- precisely bounded. scienceHash is the stable content address: the same science manifest -> the same scienceHash. bundleHash is a timestamped commitment that changes on every re-seal, so the bundleHash written into an on-chain memo attests that this bundle existed at that time -- it is not a locator for the currently-served artifact, and a reader who looks for a live file by the memo's bundleHash will not find one. Anyone can re-read the memo text from Solana with no trust in our database. Reading the memo and reconciling it to the bytes this site serves is now itself an endpoint: keyless GET /api/bundles/:id/verify-onchain re-reads the sealed transaction, decodes both the bundleHash and the scienceHash the chain carries, and compares them to the manifest served today -- reporting anchoredScienceDrift when they diverge, and chainReadable:false (never a false "verified") when no RPC answers. Separately, /api/bundles/:id/verify re-hashes the served bytes against a served HASH.txt (internal tamper-evidence only -- it does not query the chain, and HASH.txt is not on the public artifact allow-list). The exact limits these leave open are stated in Capability boundaries.
  • Under review, and answering it. A point-by-point ledger of the audit-10 review -- criticism, fix, verification, and the residual risk that remains -- is in RESPONSE-TO-AUDIT.md.

The long, detailed README below is intentionally kept as auditable context; read it when you want the specifics, this panel when you want the one-page truth.

Capability boundaries

What the current architecture cannot do -- stated as design limits, not backlog items, so the "verifiable science" claim is never wider than the machine. These come from committed measurements (P82/P84/P85), each with its denominator named.

  • Blended / multi-source-per-pixel scenes are out of scope. The centroid-displacement false-positive test caught 0/59 labelled blends on real pixel data (P85, re-run 2026-10-17 on the corrected host coordinates), and P84 shows why: ~77% of the 500 targets never localise a cross-sector centroid offset at all from a single TESS pixel (only 114/500 are testable). This is pixel-baseline-bound, not a threshold problem -- no cut on the existing statistic recovers the capability, because the resolving information is not in the aperture-photometry baseline. Handling blends requires a different photometric method (PSF fitting, or at minimum aperture correction with neighbour subtraction), which is a science-layer rebuild, not a config change. Until that exists, treat any blended system as unsupported, and do not read the 0/59 as "tuned wrong."
  • A provenance correction the previous revision got wrong. The blend numbers ride on a Gaia neighbour census, so until the RA-unit fix they were silently computed against the wrong sky (every host squeezed into a 0-24 deg right-ascension band). Only the pixel-offset numbers survived the re-run roughly intact (testable 109 to 114 of 500; known-planet false-reject 1/53 to 2/55 -- still the low calibration arm the F-test is trusted for). The Gaia-derived numbers were never trustworthy at wrong coordinates and are withdrawn: the old "caught 0/56" and the "some neighbour explains the mean offset in 54/56" ratio are replaced by caught 0/59 and neighbour-explains-the-mean 49/52. The drop from 54/56 to 49/52 confirms the suspicion that the earlier figure was inflated by "in a random field you can always find a directionally-close star." Relaxing alpha to 0.01 recovers only 1/59 caught, at 5/55 planet false-rejects. The conclusion is unchanged -- blends are out of scope for this pixel baseline -- but every P84/P85 denominator cited here is now the corrected-sky one.
  • Accepted-call precision plateaus near 42%. At 425 curves the accepted-precision is 42.2%; a larger sample narrows the interval but does not remove the coherent-systematics wall. Precision, not recall, is the binding constraint -- stated so the 86.2% recall is not mistaken for readiness to claim.
  • The token is not part of the science. The $SDR named-observation payment layer is an isolated commercialisation experiment; no verdict, bundleHash, scienceHash, or discovery claim reads from it. It is listed here so its presence is not mistaken for a scientific credential.
  • Records and commitments are MINTED PER SEAT LABEL, not per physical candidate. The workflow names one finding per TOI-table row (metaFor reads pl_name, src/cf/workflow.ts), so a host with two candidate rows can hold two records of one recovered signal, and the label can disagree with the row's own crossmatch ("HR 858 d" whose body says knownPlanetName: "HR 858 b"). Measured on the live catalog snapshot committed at tests/fixtures/live-catalog.json (captured 2026-10-23T05:42Z; tests/live-catalog-accounting.test.ts pins every number below to that snapshot, and npm run fixtures:capture-catalog re-measures it against live -- the counts are as-of that capture, not a live promise): 103 published rows = 91 recovered signals over 90 host stars (12 pure duplicate rows; accepted 18 rows = 15 signals / 15 stars), and the chain's 20 commitments cover only 17 candidates -- duplicate seats were paid for. Duplicate seats of one candidate also carry different scienceHashes today, because per-seat ephemeris/confidence fields ride in the projection; collapsing them is a re-derivation of anchored semantics and is deliberately gated behind measuring false-merge on real data (HANDOFF next-steps), not done silently. The correct merge exists (src/candidate/dedup.ts::collapseCoincident, ephemeris-linked, flags a cross-host shared ephemeris as one systematic rather than N planets) but is offline-only -- zero production callers, used by research scripts p19/p22. So: the board does not merge, and the chain pays per label. What these passes DID change is disclosure: the served views carry the contradiction as machine-readable data (requestedDesignator / requestedDesignatorKind / detectedDesignator / labelConflictsWithKnown on every finding, labelMatchesKnownPlanet on verify-onchain -- response projections only, no stored row, manifest byte or hash touched), and the board card prints it in plain words ("label claims planet D, its own NASA cross-match matches HR 858 b") so a human reader sees the same fact the API exposes. audit-17 widened the predicate two ways, both read-side and neither touching a verdict or a hash: a trailing TOI candidate number is now DECODED by the ExoFOP convention (.01 -> b) instead of reading as "no claim", which lifts the contradiction count on the served catalog from 5 to 7 (the two added rows, TOI-6054.02 and TOI-4443 c, are both needs-more-data and neither is one of the 20 anchored commitments); and hostKeyOf now keeps a stellar COMPONENT letter ("HD 202772 A b" -> hd 202772 a), so the two stars of a binary can never be falsely merged into one host or flagged as self-contradictory (distinctHosts is unchanged at 90 on the live catalog, which carries no B-component row yet -- the fix closes a latent trap, not a live miscount). requestedDesignatorKind disambiguates a null requestedDesignator ("no claim at all" versus "a candidate number"). The accounting is gated on captured production bytes (tests/live-catalog-accounting.test.ts), and the shared predicates live in src/core/signal-identity.ts.
  • Local verify and chain verify are two endpoints, kept apart. /api/bundles/:id/verify recomputes the served manifest hash and compares it to a served HASH.txt, then re-hashes each artifact from the store: that proves the store was not edited after sealing, but does not read Solana. /api/bundles/:id/verify-onchain does the chain read -- re-reads the sealed transaction, decodes the memo, and matches its bundleHash/scienceHash against the bundle served now (with a keyless daily job re-checking recent memos beside it). The two are deliberately not conflated: one is tamper-evidence of our store, the other is on-chain reconciliation, and the latter returns chainReadable:false rather than a pass when no RPC answers.
  • bundleHash drifts by design, and scienceHash can move when science is re-derived. Because bundleHash wraps a timestamp it differs between a ledger anchor and the bytes served today, so a historical anchor is reconciled to a live artifact by scienceHash, never by bundleHash. And when a finding's science is re-derived -- e.g. the RA-unit coordinate fix re-running the blend pipeline -- its scienceHash changes too: an earlier-anchored finding can now read needs-more-data where it was accepted at anchor time. We do not assert that an anchored finding's current science still equals its science at anchor time -- but that drift is no longer hidden: verify-onchain decodes the anchored scienceHash and sets anchoredScienceDrift:true the moment the chain-sealed science differs from what is served. Surfacing it is the guarantee; re-anchoring on every re-derivation is not, so a flagged finding stays as-posted until an operator chooses to re-seal.

Project health & governance (all verifiable, none of it a claim of size): automated checks run on every push/PR (CI); reproducible versions are tagged and released (releases / CHANGELOG.md); contributions are welcome (CONTRIBUTING.md) under a code of conduct; how the project is run and decided is documented in GOVERNANCE.md; report vulnerabilities privately via SECURITY.md.

Authorship, stated honestly: this is currently maintained by one human owner working with an autonomous AI coding agent; agent-authored commits use an explicitly-labelled automated identity and AI collaboration is disclosed rather than hidden. We do not create extra accounts to imply more contributors than there are -- maturity here is the reproducible engineering (public data in, hash-sealed bundle out), not headcount.

An OpenRig-style, five-seat AI team that runs a fully reproducible transit-detection loop: it pulls real confirmed-planet parameters from the NASA Exoplanet Archive, resolves each target's light curve (real MAST TESS photometry where curated -- or, with the opt-in token-free tier, real TESS-SPOC photometry pulled autonomously from a public S3 bucket -- otherwise a seeded synthetic stand-in), hunts the injected transit with a deterministic Box-Least-Squares (BLS) search, quantifies recovery confidence with injection-recovery simulation, has a critic seat cross-match candidates against the catalog, and packages every verdict into a hash-sealed reproducible bundle anchored on Solana mainnet as tamper-evident provenance.

About the "five-seat AI team" framing, stated plainly. The five seats are stages of a deterministic TypeScript pipeline (data-fetch -> anomaly-scout -> sim-runner -> mission-critic -> reporter); the math that actually decides a verdict is plain code, not a model. A live LLM layer exists but is advisory narration only: src/ai/ is roughly 2% of the codebase, and we measured it (P70, scripts/p70-llm-ablation.ts) -- removing the models changes zero gate or verdict outcomes, and the deterministic verdict is computed byte-identically before and after any model is called. So the "AI team" is a presentation device for a real, reproducible science engine (product = deterministic pipeline + hash-sealed bundle + web dashboard), not a claim that a model is doing the astronomy.

A finding we would lead with (P75). On a planet-free null built by construction -- real TESS-SPOC light curves reflected about their median (inverted) with every catalogued transit masked, so any surviving positive dip can only be noise or systematics -- our production AR(1) significance gate signs off 75% of pure-noise bumps at FAP<0.05, versus a nominal 5% (a model-free block bootstrap flags only 31% and overturns 7 of the 16). On this small calibration sample (n=16) the AR(1) null is measurably optimistic on real systematics tails; we report and re-test it rather than silently recalibrate, and it stays measured-not-gated (threshold unchanged). The null generator is now a reusable, unit-tested product (src/detect/invertedNull.ts), and the dual AR(1)/model-free study is scripts/p75-inverted-fp.ts.

What this is, plainly -- and what it is not. This is a science-grade closed-loop self-test: the pipeline proves it can recover a known signal from noise, and it is not claiming to discover new planets. The data-fetch seat resolves each target's light curve through a fixed priority chain -- (1) a curated registry of REAL MAST TESS products (anonymous Download/file, FITS-parsed), (2) an opt-in token-free tier that autonomously pulls REAL TESS-SPOC PDCSAP photometry from the public AWS stpubdata S3 bucket by deterministically constructing a TIC-sharded key (no MAST token, no AWS account), (3) an optional best-effort anonymous CAOM discovery pass, then (4) a seeded synthetic stand-in built from real catalog parameters (now carrying realistic per-star red, time-correlated noise by default rather than idealized white noise) -- and every bundle and card labels which tier produced its photons (REAL photometry vs seeded stand-in). Tiers (2) and (3) are OFF by default, so a stock run is fully deterministic on the stand-in and its bundle hash never changes; enabling them (Worker var MAST_S3=1, which also narrows autonomous target selection to TIC-bearing rows) lets any SPOC-covered target run on real starlight -- verified end to end at the Cloudflare edge, where an autonomous pass fetched REAL photometry for live-selected planets (e.g. pi Men c, HR 858 b) and the BLS seat independently recovered the catalog period. A "recovery at the true period" on the stand-in path is by construction, not evidence of a find. The critic is explicit about this too: it reports "recovers the catalog transits" only on the real open-loop path and "recovers the injected signal" otherwise. What is unconditionally real: the live NASA TAP catalog query, the detrending/BLS/alias math, the injection-recovery statistics, the bundle hashing, and the on-chain anchoring -- all of it auditable end to end.

Everything is streamed live to a web dashboard, so anyone can watch the agents work, read a plain-English explanation of each verdict, inspect the plots, verify a bundle hash, and see it land on-chain.

This repository is the science engine + provenance layer. The token is launched separately on pump.fun; there is deliberately no custom token contract or Solana program here - on-chain we only need an immutable, publicly verifiable record, which the standard SPL Memo program provides.


Why this exists

Most "AI agent on-chain" demos are theater: agents chat, a token exists, and nothing verifiable happens. Space Data Rig is built to be the opposite:

  • It does real work, on synthetic data. The detection pipeline is genuine BLS + detrending + alias resolution math; the data it runs on is a seeded light curve built from real catalog parameters. Recovering HD 189733 b at its true period therefore validates the machinery, and does not constitute a planet detection.
  • It is honest about uncertainty. Every signal is replayed through fresh simulated noise; the fraction of times the pipeline recovers it is reported as a recovery confidence, and a critic seat can reject or watchlist a weak signal.
  • It is reproducible -- stated precisely. The same manifest content always hashes identically (SHA-256 over a canonical, key-sorted JSON serialization). But the bundle hash is a timestamped commitment, not a content-address id: it folds in run-volatile fields (createdAt, runId), so re-running the same science yields the same numbers yet a different bundle hash by design. For a stable content address we also expose scienceHash (the manifest hash with those run-volatile fields normalised out) -- two runs that recover identical science share one scienceHash while carrying distinct bundle hashes. New on-chain memos append the scienceHash (verification still matches the bundle-hash commitment; cross-run comparison uses the content address).
  • It is independently auditable. The bundle hash is written to Solana mainnet inside an SPL Memo transaction; anyone can re-read the chain and confirm the memo is what we posted, with no trust in our database. That reconciliation is now a keyless endpoint, not only a manual step: GET /api/bundles/:id/verify-onchain re-reads the transaction, decodes the memo's bundleHash and scienceHash, and compares both to the bytes served now -- flagging anchoredScienceDrift when the chain-sealed science no longer matches the served science, and chainReadable:false (never a false pass) when the chain is unreachable. Read the guarantee narrowly: the memo pins that this exact timestamped bundle existed at this time. Because a re-seal changes the bundle hash, the memo is not a handle for locating today's served bytes -- you reconcile a historical anchor to a live artifact through the content address (scienceHash), not the memo's bundle hash.
  • It is understandable. The reporter seat translates every finding into a sentence a non-specialist gets, without dropping the real numbers.

Architecture: the five seats

        +-------------+     +---------------+     +------------+     +---------------+     +----------+
 data ->| data-fetch  | --> | anomaly-scout | --> | sim-runner | --> | mission-critic| --> | reporter |
        +-------------+     +---------------+     +------------+     +---------------+     +----------+
        pull real NASA       BLS transit search    injection-recovery  cross-match vs        bundle + hash
        catalog + build      + odd/even alias      confidence +        known planets +       + plain-English
        observable light     resolution +          ephemeris           verdict + score       narration +
        curve + provenance   detrending            prediction                                SVG plots
        hash
                                             |
                                             v
                                   +-------------------+
                                   |     RigStore      |   shared state + event bus
                                   | tasks / findings  |   (snapshot + restore)
                                   | seats / bundles   |
                                   | events / stats    |
                                   +-------------------+
                                        |          |
                          +-------------+          +--------------+
                          v                                         v
                 +-----------------+                       +----------------+
                 | HTTP API + SSE  |  <-- subscribe -->    | Solana mainnet |
                 | web dashboard   |                       | memo anchoring |
                 +-----------------+                       +----------------+

Every seat talks to the shared RigStore instead of to each other. The store is the single source of truth: the CLI, the HTTP API and the live dashboard all read from and subscribe to the same store instance, so a run started anywhere streams everywhere.

Seat What it really does
data-fetch Queries the NASA Exoplanet Archive TAP service for bright transiting systems, resolves each target's light curve through the curated -> token-free TESS-SPOC public-S3 -> (optional) CAOM-discovery -> seeded-synthetic priority chain (the two real tiers are off by default, so a stock run stays deterministic), and records a SHA-256 provenance hash of the source data. Falls back to a bundled real-value catalog if the network is unavailable.
anomaly-scout Detrends the light curve, runs a Box-Least-Squares period search, resolves odd/even epoch aliases, and emits a transit candidate (period, epoch, depth, duration, SNR).
sim-runner Replays the detected signal through N fresh noise realizations (injection-recovery Monte Carlo) to estimate how reliably it is recovered, and predicts the next transit times (a falsifiable ephemeris).
mission-critic Cross-matches the candidate period against the known-planet catalog (including harmonics), applies physical sanity checks, and issues a verdict (accepted / needs-more-data / rejected) plus a 0-100 composite score.
reporter Writes the plain-English narration, renders the SVG plots (time series, folded phase curve, BLS periodogram), assembles the reproducible bundle manifest, and computes its SHA-256 hash.

The AI deliberation layer (two models, on the side)

Beyond the five deterministic seats, an optional large-model layer adds a plain-language deliberation over each finding. It never touches the science pipeline, the bundle manifest, or the bundle hash, and it degrades to a grounded (model-free) transcript when no API key is set.

  • Two reviewers, two lenses. DeepSeek acts as the statistical critic; Grok (xAI) acts as the adversarial red-team. llm.ts speaks plain OpenAI-compatible Chat Completions, so each provider carries its own base URL + model.
  • Two-round cross-examination. In round 1 each model gives its lens critique; when two or more answer, round 2 shows each model the other's opening and asks it to concede, defend, or rebut in one labeled sentence -- so the AIs genuinely argue instead of delivering parallel monologues. A model that fails is dropped individually; both rounds run concurrently.
  • Sealed + anchored. The transcript's model turns are sealed as a tamper-evident deliberation receipt (deliberations/<id>/receipt.json + HASH.txt) whose hash anyone can recompute via /api/discussion/:id/receipt/verify, and -- on demand -- anchor on Solana mainnet under a separate sdr:delib:1|<id>|<hash> memo. That is integrity for AI commentary, deliberately kept apart from the reproducible science bundle.

Because model output is non-reproducible, this layer is a dashboard/audit feature only: the deterministic run and its bundle hash are computed entirely offline and never depend on a model response.


Production deployment (Cloudflare Workers)

One codebase runs on two runtimes: a Node server (npm run api) and a Cloudflare Workers edge deployment (npm run cf:deploy) live at https://spacedatarig.app. On the edge a Durable Object (rig-state.do.ts) owns the shared state, the anchoring ledger, and the WebSocket fan-out; a Workflow (rig-run) executes a run; R2 stores bundles and deliberation receipts; and the fee-payer keypair lives in the SOLANA_KEYPAIR Worker secret. The runtime-agnostic core (detection, simulation, critic, reporter, bundle hashing, the Solana memo path, and the deliberation seal) is shared verbatim across both, so Node and edge stay at parity.


Autonomous research loop (token-free, opt-in)

The rig can select, fetch and analyze real targets by itself -- with no MAST token and no AWS account. The code defaults are OFF (a stock deploy changes nothing until an operator opts in). This live deployment is opted in (aggressive tier): wrangler.toml ships MAST_S3=1, AUTO_RESEARCH=1 (an autonomous run on the every-15-minute cron, 8 targets), AUTO_RESEARCH_ROTATION=1 (the survey rotates through a growing real pool, see below), and AUTO_ANCHOR=live with AUTO_ANCHOR_DAILY_CAP=100, so accepted findings are sealed on Solana mainnet at a cost bounded to 100 memos/day (about 0.0005 SOL/day). Set AUTO_ANCHOR=off (or AUTO_RESEARCH=0) and redeploy to pause spending.

  • Real data, autonomously. With the Worker var MAST_S3=1, target selection keeps only planets that carry a TESS Input Catalog id, and the data-fetch seat constructs a deterministic TIC-sharded key into the public stpubdata S3 bucket to pull REAL TESS-SPOC PDCSAP photometry (a public reimplementation of the official aws s3 ls --no-sign-request access). If a target is not in the SPOC program the tier 404s and the run falls through to the synthetic stand-in -- it never fabricates data.
  • On a schedule. A [triggers] crons entry plus a scheduled handler in worker.ts can launch an autonomous run unattended, but only when AUTO_RESEARCH=1 (default 0, so the cron is inert). It reuses the same single-run mutex as the dashboard, skipping a tick if a run is already going.
  • Budget-gated on-chain sealing. Autonomous runs may anchor an accepted finding, but only through the DO budget gate: AUTO_ANCHOR is off by default (spend nothing), dry previews the memo, and live spends real SOL only while a per-UTC-day cap (AUTO_ANCHOR_DAILY_CAP, default 0) leaves room, and never repeats an already-anchored hash. On-chain anchoring therefore stays an explicit, cost-capped operator decision.
  • Two anchor counts, because they answer two questions. The dashboard shows sealed as published and chain commitments side by side. The first is stats.bundlesAnchored: findings whose CURRENT bundle bytes still match a commitment, and one that can go DOWN when the rotating survey re-measures a sealed host, because a bundleHash folds the run timestamp and a re-derived bundle is a hash the chain never saw. Stated plainly (audit-12): with science-hash dedup on -- an identical-content repeat is deliberately not re-anchored, so no second fee is spent -- the newer bytes of already-sealed science are never freshly committed, and this property CANNOT hold for those rows while dedup stands. Production has measured 0 of 20 served bundles covered. We do not claim the chain commitment covers the bytes you are reading now; it attests the record it sealed, and the per-finding verify-onchain readout says exactly which of the two states a card is in. The second is stats.ledgerAnchored: ledger rows recording a commitment that landed (status anchored AND a signature), which survives a re-derivation and answers "how many results has this rig put on chain". Production held 7 and 18 at one snapshot and has sat at 0 and 20 under dedup; publishing one number under the other's label was the defect. A landed ledger row is never demoted by a later attempt that failed (mergeLedgerRow, applied by both the Durable Object and the filesystem ledger), so a transient RPC error cannot destroy the evidence for a transaction still live on mainnet. Both counts now carry their full definitions inside the published payload (src/core/stats-definitions.ts), and the board renders them as tooltips on the two ticker chips -- the relationship is readable where the numbers are.
  • A rotating survey, and honest blind candidates. With AUTO_RESEARCH_ROTATION=1 the autonomous run no longer re-runs the same brightest handful and overwrites it in place: it rotates through a broad real pool -- known-planet re-validation rows plus UNCONFIRMED TESS Objects of Interest -- chosen from a persisted pass cursor (src/survey/rotation.ts), so FINDINGS CATALOG grows new rows across passes. Each row is labeled by origin: revalidation (a known planet the pipeline re-derives and cross-checks) or blind-toi (an unconfirmed candidate recovered from real TESS-SPOC pixels with the TOI ephemeris withheld -- the same protocol as p7:toi-survey, which independently recovered 27/70 unconfirmed candidates at a 0.22% median period error). A blind-TOI row is never ACCEPTED -- the critic caps an accepted verdict at needs-more-data, and rows it rejects stay rejected (rejection is the honest outcome for noise, not something to overwrite) -- it is never auto-anchored, and reads in the UI as a gold "BLIND CANDIDATE - UNCONFIRMED" pill -- a candidate for human follow-up, never a claimed discovery. The origin label lives only on the Finding and in DO state, never in the bundle manifest, so it changes no bundleHash/scienceHash and touches no anchored row. A dashboard manual run keeps the historical deterministic brightest-top-N selection.
  • What each public counter counts, stated where the code is read. The rotation surfaces four numbers, and the honest reading of each is now written in src/core/stats-definitions.ts and rendered on the board: surveyPass is passes CLAIMED, not completed (the cursor advances when a run takes the slot, so a pass that hangs still counts once and never twice); distinctHosts counts host STARS among RECORDED results of any verdict -- seat labels reduced by the shared hostKeyOf (a trailing planet designator and/or TOI candidate number stripped), so one star under two seat labels counts ONCE (audit-16: the old rule counted distinct LABELS, reading 103 where 90 stars live) -- distinctRecords publishes the raw-label count beside it, and neither is targets analysed (that is the separate targets tile); and the blind figure is a pair -- blindHeld/blindCandidates -- held for human follow-up over rows assessed, because a critic-rejected dip is noise the pipeline cleared, not a lead. The X digest and the dashboard now share isHeldBlindCandidate, so the phrase "blind candidate" cannot be counted two ways on two surfaces (an earlier board showed 47 where the feed's honest figure was 26, 21 of them rejected). Those definitions are served, not just documented: GET /api/state carries them as statDefinitions from the same constant on both runtimes, and the board's hover text is built from that payload -- the markup keeps no copy of its own, because a hand-maintained label is how a counter and its query part company.

Try the chain without a token: npm run p4:autonomous (add -- --targets 12 --tmag 9.5, or a known SPOC star with -- --tic 1024880 --sector 4).


Scientific-rigness upgrades (self-validating, gated)

Beyond the autonomous loop, the analysis core carries additive primitives that raise it from a demo to defensible science. Each is pure, deterministic and fully unit-tested. They began OFF the default (synthetic) run path so the reproducible bundle hash stayed frozen; the honesty-critical ones (red-noise stand-in, per-candidate calibrated significance, the blind critic, pixel vetting) have since been promoted to ON BY DEFAULT -- detailed in the subsections below. That changes the stand-in bundle hash to reflect realistic correlated noise, but the hash stays fully deterministic and reproducible run to run (same seed => same bytes => same hash):

  • Exact limb-darkened transit model (src/sim/exactTransit.ts). Quadratic limb darkening, computed as a deterministic 1-D radial integral of the occulted intensity. With no limb darkening it reproduces the analytic circle-circle overlap area to better than 2e-3 across the whole transit chord and gives exactly p^2 at central transit -- validated against an independent closed form, not a reference dataset. The geometric model in transitModel.ts still backs the deterministic injection so existing hashes stay stable.
  • Calibrated false-alarm probability (src/detect/fap.ts). Turns the heuristic BLS SNR into a real significance level via a phase-scrambling null: each point's time is jittered by +-P/3, which destroys a transit's phase coherence while leaving the noise distribution intact; FAP is the fraction of surrogates whose BLS max-power reaches the observed power. An injected planet scores at the surrogate floor (0 exceedances) where pure noise scores ~0.19. (A naive circular time-shift was rejected -- it preserves the transits and gave a false planet a spuriously low FAP; the regression tests caught it.)
  • Pixel-level false-positive vetting (src/data/mastS3Pixels.ts, wired into the critic via reviewCandidate(vetting)). For REAL SPOC photometry the critic downloads the 14x14 target-pixel cube and measures the in-transit photocenter displacement against the candidate's own ephemeris; a large shift means the dip is not on the target star (a blend / background eclipsing binary) and downgrades the verdict to rejected. This is now ON BY DEFAULT (VET="0" opts out): letting a large photocenter shift through by default would stake blends on the money path. The synthetic stand-in path has no pixels, so it is unaffected either way, and with vetting absent the review is byte-for-byte identical (a test asserts empty-vetting == no-vetting).

On pi Men c the real in-transit photocenter shift is 0.10 arcsec -- correctly judged on-target. The reject threshold is EMPIRICALLY CALIBRATED, not guessed: npm run p2:vet-calibrate measures the shift for confirmed planets (which genuinely transit their hosts, so must cluster near zero). Across 14 confirmed planets the shift maxes out at 0.57 arcsec, so the critic rejects at >2.0 arcsec -- ~3.5x above every true planet yet far below the multi-arcsec signature of a background EB / blend, so the gate only ever REMOVES accepts, never adds them. Pixel vetting is ON BY DEFAULT in the code (VET="0" opts out) and kept explicitly VET=1 in the autonomous deployment: blends are vetoed before they can be anchored. The only residual cost is operational -- each vetted target pulls a ~5 MB target-pixel file, so a full sweep runs slower.

Two more discriminators are implemented (src/detect/vetting.ts: odd/even transit-depth symmetry and the phase-0.5 secondary-eclipse ratio) and are computed on every real run, but they are logged as DIAGNOSTICS ONLY and deliberately do NOT gate the verdict. Calibration on the same 14 confirmed planets showed why: the odd/even asymmetry hits 100-200% for genuine planets, because a single TESS sector with a catalog ephemeris accumulates enough phase error that alternate transits drift in and out of the fold window. A metric that fails on known-good planets cannot be used to reject candidates (it would silently drop true positives). The clean synthetic unit tests pass, which is exactly the trap: a statistic that works on idealized data but not on real single-sector photometry must be calibrated on REAL data before it is allowed to decide anything. Only the centroid shift -- which is per-cadence and immune to ephemeris phase error -- gates today.

We then built the robust version we had sketched as future work: oddEvenRefined measures each transit at its OWN center (a local search around every predicted epoch) instead of folding all points into fixed windows, so accumulated phase error cancels. Re-running the calibration (npm run p2:vet-calibrate) on the same 14 confirmed planets compares the two directly: the fixed window still peaks at 200% asymmetry and mis-labels the active-star planets HD 63433 c/d, whereas the refit correctly declines to judge ("n/d") on their sparse single-sector coverage. The production light-curve diagnostic now reports the honest refit number rather than the inflated one. Being disciplined about it, though, the refit is STILL not promoted to a gate: it only reaches a confident verdict on 2 of 14 real planets, and one of those -- confirmed planet HR 858 b -- reads 69% asymmetry, so any 0.5-0.7 rejection cut would drop a known-true planet. A statistic that occasionally fails a real planet cannot sit on the money-spending path, so the refit ships as a diagnostic (a strictly more truthful one) and the centroid remains the only light-curve-adjacent rejection. Multi-sector or TESS-wide ephemerides would give the refit enough well-phased events to become gate-worthy; that data is out of scope for a single-sector pull.

Measured operating characteristics (completeness / purity)

The detector's operating point is now quantified rather than asserted. src/sim/roc.ts runs a seeded injection-recovery sweep: known planets give COMPLETENESS (period matches truth AND the BLS SNR clears the threshold); ZERO-INJECTION pure-noise curves (same sampling, planet radius p=0, so no dip anywhere) give the FALSE-ALARM RATE. Reproduce with npm run p1:roc (e.g. -- --inj 60 --noise 60 --baseline 15 --cadence 300 --seed 20260011). A representative run (TESS-like 200 ppm noise, 15-day baseline):

threshold(SNR)  completeness  false-alarm-rate  purity  TP/FP
        5          68.3%          10.0%   87.2%  41/6
        7          68.3%           0.0%  100.0%  41/0
        9          66.7%           0.0%  100.0%  40/0
       11          61.7%           0.0%  100.0%  37/0

Two things this makes defensible: (1) the minSnr=7 decision cut is now DATA-JUSTIFIED -- noise peaks topped out at SNR 5.7, so SNR>=7 threw 0/60 false alarms (the SNR>=5 cut would let 10% of empty fields cry wolf); and (2) the ~68% completeness ceiling is the honest physical limit of period-vs-baseline coverage (periods up to 12 days on a 15-day baseline) and sub-noise shallow injections, not a silent failure. These are synthetic TESS-like benchmark figures; the real-sky complement -- completeness measured by injecting into ACTUAL TESS data -- is below.

Real-sky completeness (injected into ACTUAL TESS photometry)

A survey's completeness must be quoted on its own data, not only synthetic noise. src/sim/injectInto.ts injectTransitInto(lc, {period, depth}) multiplies a physical transit onto a REAL light curve (noise color, cadence and data gaps untouched), and npm run p1:real-completeness runs the same detector on many real TESS-SPOC light curves and records recovery by depth and period. Measured (12 real hosts, 3 trials each, SNR>=7):

depth(ppm)   300   500   1000  2000  4000  8000
recovered    61.8% 72.9% 82.6% 84.7% 84.7% 88.2%
period        1.5d    3d    5d    8d
recovered     97.2% 89.8% 84.7% 44.9%

This is the honest, data-grounded operating characteristic: completeness rises monotonically with planet size (62% at 300 ppm to 88% at 8000 ppm) and FALLS with orbital period -- an 8-day planet only gets ~3 transits in a ~27-day sector, so it is intrinsically harder to catch than a 1.5-day one (97%). The ~88% ceiling (not 100%) is real: active red-noise hosts, data gaps and period aliases cost us the rest, exactly as the noise-color and ROC studies predicted. The core (injectInto.ts) is unit-tested offline (deep injection recovered; depthFrac 0 is a point-for-point no-op; deterministic; completeness monotone in depth); the script itself needs network and is not in CI.

Per-detection parameter uncertainty (bootstrap error bars)

A point period with no error bar is not a measurement. src/detect/uncertainty.ts bootstrapUncertainty runs a circular block bootstrap on the REAL light curve -- subtract a box model at the fitted parameters, resample the residuals in contiguous blocks (preserving each star's own red-noise color, so quiet stars get tighter bars than active ones at the same SNR), refit, and take the 16/50/84 percentiles. npm run p1:uncertainty demonstrates it on real hosts: e.g. WASP-189 b P=2.7266 +/- 0.004 d against the catalog 2.724 (bracketed), while the active star HD 63433 shows a much wider interval -- honest, noise-driven error bars.

We state its limit plainly: the bootstrap conditions on the peak BLS found, so its interval captures MEASUREMENT scatter, not ALIAS selection. On pi Men c the detector locked the 3.14 d half-period harmonic (here) and the bootstrap reported a tiny error around the ALIAS -- a narrow interval is not proof the period is the true one. Alias disambiguation is a separate concern (the significance/odd-even diagnostics, the exact-transit shape fit, and multi-sector phase coverage). Shipping an error bar that overstates confidence would be worse than no error bar, so we document exactly what it does and does not cover.

Because the noise ensemble IS the surrogate, the false-alarm-rate column is itself an empirical false-alarm probability: empiricalFap(noiseSnr, snr) maps any single candidate's BLS SNR to the fraction of planet-free curves that reached it (a Laplace-corrected, resolution-floored value) with no per-candidate resampling. This gives each detection a calibrated significance, not just a heuristic SNR label.

This operating point assumes WHITE noise -- red noise moves it (honest caveat)

The table above uses uncorrelated Gaussian scatter. Real TESS PDCSAP light curves carry time-CORRELATED ("red") residual-systematic noise. src/sim/redNoise.ts adds a stationary AR(1) noise process (lag-1 autocorrelation rho) on top of the same real transit geometry, and runRoc({ rho }) runs the whole benchmark under it (rho=0 reproduces the white table byte-for-byte). npm run p1:roc now also prints a red-noise comparison. Reproduce with npm run p1:roc -- --inj 80 --noise 80 --baseline 12 --cadence 200 --noise-ppm 300 --rho 0.5:

threshold   white comp / FAR      red comp / FAR
SNR>=5      57.5% / 15.0%         55.0% / 46.3%
SNR>=7      55.0% /  0.0%         55.0% / 30.0%
SNR>=9      50.0% /  0.0%         52.5% /  0.0%
max false-alarm SNR:  5.90 (white)  ->  8.91 (red)

The conclusion is uncomfortable but real: correlated noise attacks PURITY, not completeness (completeness barely drops) -- the white-noise "SNR>=7 => 0% false alarms" claim is OPTIMISTIC. Under AR(1) rho=0.5 the same cut lets ~30% of planet-free fields cry wolf, and you need SNR>=9 to push the red-noise false-alarm rate back to ~0%. We do NOT silently raise the production minSnr on the strength of a synthetic rho, for the same reason we refused to ship the odd/even discriminator un-calibrated: a gate on the money-spending path must be set from a measurement on REAL data. The correct next step is to ESTIMATE rho directly from real SPOC PDCSAP residuals (light-curve autocorrelation), then choose the cut so the RED-noise false-alarm rate -- not the white one -- is at the level we publish. This caveat is a feature: the benchmark is sensitive to its own noise assumption. RESOLVED BELOW: that next step shipped -- estimateRho measures the real noise color, the synthetic stand-in now injects per-star red noise by default, and every candidate is graded against a null of its OWN noise color, so the published operating point is no longer white-noise-conditional.

We then MEASURED the real noise color (src/sim/redNoise.ts estimateRho, npm run p1:rho)

Rather than leave rho as a guess, we measure the residual lag-1 autocorrelation directly from REAL, token-free TESS-SPOC PDCSAP light curves: moving-median detrend, MAD outlier rejection, then the lag-1 autocorrelation of the residuals. The measurement unit is the STAR, not the planet -- multi-planet hosts share one light curve and are de-duplicated so a single busy star cannot bias the distribution. Over 16 distinct bright hosts:

rho of real PDCSAP residuals:  min=-0.06  median=-0.00  p90=0.63  max=0.63  (mean 0.09)

This resolves the caveat with data and tells a genuine astrophysical story:

  • The median host is essentially WHITE (rho ~ 0.0). SPOC's own detrending removes most correlation, so for quiet, old stars the white-noise FAR is a FAIR description and SNR>=7 is defensible.
  • The tail is strongly red (rho ~ 0.63) -- and the two worst are exactly the young, magnetically ACTIVE stars in the sample (AU Mic ~24 Myr and HD 63433 ~20-30 Myr), whose real variability the estimator correctly flags. There the white-noise FAR IS optimistic and the red-noise cut (SNR>=~9) applies.

So noise color varies star-to-star, and one global SNR threshold is provably imperfect. The principled follow-up is per-candidate: estimate rho from each target's own light curve and map its SNR to an empirical false-alarm rate computed under THAT noise color. We record the measured distribution rather than silently re-tuning the global cut -- the same discipline applied throughout (money-path thresholds come from real measurements, and honest limitations are published, not hidden).

A lag-1 number is not the whole story: is real residual noise actually AR(1)? (npm run p8:noise-acf)

Everything above (and the per-candidate nulls below) ASSUMES a star's residual noise is AR(1), i.e. its autocorrelation decays as ACF[k] = rho^k. A single lag-1 rho cannot test that assumption -- it says nothing about the SHAPE of the decay. p8:noise-acf measures the FULL autocorrelation function on real, token-free SPOC light curves (detrended + MAD-clipped exactly like estimateRho, deduped by star) and fits the AR(1) prediction to it. Over 36 distinct hosts: 30 are near-white (median rho 0.00) and 6 are red -- and the red ones are again the young active stars (AU Mic rho 0.63 R2 0.91, HD 63433 rho 0.63 R2 0.86, TOI-4641 rho 0.46 R2 0.76, KELT-9 rho 0.45). Crucially, the red hosts' measured ACF decays AT LEAST as fast as rho^k through the mid lags (median fit R2 0.61, and the excess correlation is negative or negligible -- no growing power-law tail): there is no hidden longer-memory structure that AR(1) misses. So the AR(1) null model behind calibratedFap is faithful-to-conservative on real data -- it does not understate the false-alarm tail. A direct, real-data validation of the DECAY SHAPE that lag-1 alone could never provide.

We then BUILT it: per-candidate calibrated significance, shipped ON

src/detect/significance.ts calibratedFap(lc, snr, period) does exactly what the measurement prescribes: it estimates the target's OWN noise color (rho) and residual scatter, generates planet-free nulls matched to them (makeRedNoiseLightCurve, depthFrac 0), and reports the empirical false-alarm probability of the candidate's BLS SNR. It is deterministic (fixed seeds, pure functions of the input flux) -- the same guarantee the pixel vetting relies on for the money path. It is now on the DEFAULT path for EVERY candidate, not just real photometry: both the edge (src/cf/workflow.ts) and Node (src/orchestrator/pipeline.ts) feed vetting.fap into the critic, so a candidate is rejected when its calibrated FAP exceeds the threshold. Real SPOC photometry keeps the certified full-resolution null ensemble (nNull 120) byte-for-byte; the synthetic/stand-in default uses a bin-averaging cost guard (maxPoints 2500, nNull 60) so the ensemble completes deterministically in ~3s inside one workflow step. A subtlety that guard exposed, and we fixed: box-averaging crushes the measured rho (a true 0.5 host reads ~0.05 after 8x binning), which made the null ensemble too WHITE and leaked the false-accept rate to ~13%; estimating rho at NATIVE cadence and binning each null the same way as the observed curve brought it back to the nominal ~5%. This is LIVE, not behind a flag.

Calibrating it caught a latent landmine before it could fire: the critic's old default FAP gate was 0.01, but the smallest FAP a null ensemble can report is its resolution floor 1/(nNull+1) = 0.012 at nNull=80. A gate BELOW the floor would reject EVERY candidate -- real planets included, since a 100%-certain signal still measures at the floor. We raised nNull to 120 (floor 0.008) and set the threshold from real data.

Safety certification (npm run p1:fap-calibrate, the mandatory measure-before-you-gate step): on 19 known planets that clear the production SNR>=7 cut, every calibrated FAP sits at the floor (0.0083) against the 0.05 threshold -- zero false rejections. (A couple of test planets like TOI-2128 score high FAP only because BLS finds nothing for them at all, SNR<0 -- they are already declined upstream and never reach the gate.) The threshold 0.05 keeps ~4x margin over the floor while still rejecting genuinely insignificant peaks, and it is stricter and better-founded than the old fixed SNR label because it adapts to each host's measured noise color. This is the operating principle the whole project runs on: don't assert reliability, MEASURE it; and never wire a rejection gate without first proving on known-good data that it doesn't kill real discoveries.

One subtler statistical question we probed rather than assumed: on the FULL-resolution real-photometry path the observed SNR is the maximum of the WIDE detection search (0.8-16 d) while the null ensemble is searched over a narrow window around the candidate period, and a wide-grid maximum is in principle inflated relative to a narrow-grid one (a look-elsewhere effect). npm run p2:fap-grid compares the two null choices on identical full-resolution red-noise curves. Measured, the two FAPs agree closely and the choice flips NO accept/reject decision: planet-free 3/8 vs 3/8 at FAP<=0.05 -- and every one of those carries SNR<7, so the production minSnr=7 floor rejects it anyway -- and planets 4/4 vs 4/4. Widening the null grid to the detection grid would cost ~9x the edge compute for zero decision benefit, so the narrow null stays and the probe ships as a permanent regression check. The load-bearing false-positive protection on the money path is not this FAP nuance but the minSnr floor plus the blind critic's direct-catalog-match requirement, which is why an empty field can never be staked.

Blind / discovery mode (independent re-detection of UNCONFIRMED candidates)

Everything above re-validates planets we already KNOW. To show the pipeline can stand on its own, src/data/toiArchive.ts (npm run p5:blind) sources candidate transits that are not yet confirmed -- the NASA Archive toi table (TESS Objects of Interest), minus any host that already appears in the confirmed catalog -- pulls the same token-free TESS-SPOC photometry, runs detectTransit BLIND (the whole period grid, with the TOI's reported ephemeris deliberately WITHHELD from detection), and only afterwards cross-matches the period we recovered against the TOI reference. Independent recovery is evidence the signal is real in the downlinked pixels, not a restatement of the discovery pipeline's own answer.

A real run (8 brightest unconfirmed candidates, SNR>=7) independently blind-recovered 5 of 8:

TOI-1773.01  TOI P=0.7365d -> ours 0.7362d  SNR 44.5   CANDIDATE (blind recovery)
TOI-1025.01  TOI P=9.6838d -> ours 9.7442d  SNR 140.6  CANDIDATE (blind recovery)
TOI-1978.01  TOI P=6.0810d -> ours 6.0866d  SNR 121.7  CANDIDATE (blind recovery)
TOI-2011.01  TOI P=11.5778d -> ours 11.390d SNR 15.1   CANDIDATE (blind recovery)
TOI-1469.02  TOI P=6.7650d -> ours 6.7772d  SNR 11.4   CANDIDATE (blind recovery)
-- the 3 misses are honest: TOI-1469.01 locked its brighter same-star sibling, and TOI-1487.01 /
   TOI-2011.02 (23-28 d periods) fall below single-sector completeness, exactly as measured above.

The safety boundary is deliberate and load-bearing: blind mode is a research probe, not an announcement machine. Nothing it finds is anchored, posted, spent on, or claimed as a discovery -- a hit is printed as CANDIDATE (blind recovery, review) for a human to follow up. Auto-anchoring an unconfirmed candidate would put an unverified judgement on the provenance money-path and over-claim, which is precisely what this project refuses to do. The rig reports what it can independently recover and stops short of calling it a planet. (A num(null)->0 coercion bug the offline schema fixture caught: TAP nulls must map to NaN or candidates with no period silently pass as valid.)

This blind posture is now the DEFAULT critic, not a special probe mode. reviewCandidate decides every verdict from evidence alone -- BLS SNR, the calibrated FAP, physical plausibility, transit coverage, alias checks, injection-recovery confidence and (on real pixels) the centroid shift -- and the known/injected truth period is consulted only AFTERWARDS, as a non-gating post-hoc note that can never move the verdict. An accepted blind verdict requires ALL of: no hard-fail, no low-confidence flag, at least 3 observed transits, a clean alias check, and a DIRECT catalog match (recovered harmonic === 1). A harmonic or odd/even alias, a candidate with no catalog row to match, or any alias ambiguity is capped at needs-more-data rather than accepted. Recovery gating -- scoring a candidate by whether it matched the known truth period -- still exists but is now an explicit opt-in (RECOVERY_GATING=1) reserved for benchmark self-tests, never the production path, so the money-path verdict can never be flattered by peeking at the answer key.

Survey-scale blind operating characteristics (npm run p6:survey)

To quote completeness/purity under the FULL blind critic -- not just raw period recovery -- p6:survey runs a deterministic, fully OFFLINE synthetic survey: it injects known planets into per-star red-noise light curves and, in parallel, generates planet-free red-noise curves, then pushes every target through the same scout -> simulate -> calibratedFap -> critique path the production seats use (recoveryGating:false, truth recorded post-hoc only). Injected periods are kept in the confirmable regime (>= 3 transits over the baseline). Reproduce with npm run p6:survey (default 40 planets + 40 noise curves, ~3 min). A representative run:

planets (N=40)   accepted 35   needs-more-data 5   rejected 0
                 raw period recovery 36/40 = 90.0%   blind completeness 35/40 = 87.5%
noise   (N=40)   accepted 0    needs-more-data 1   rejected 39
                 4 curves threw a raw SNR>=7 peak, 1 of those cleared FAP<=0.05 -- yet 0 accepted
                 false-accept rate 0/40 = 0.0%   purity 100.0%

Two honest readings. (1) The 5 planet misses are all DIAGNOSED, not silent: three recovered the half-period (harmonic x2) instead of the fundamental, one locked a wrong period with no catalog match, and one recovered the TRUE period yet was still capped at needs-more-data by the odd-even alias gate (x3). That last case is the entire gap between raw recovery (90.0%) and completeness (87.5%) -- the honesty gates would rather decline a correct-but-ambiguous period than accept it. (2) On the money path the survey proves the load-bearing invariant: four planet-free curves produced a tempting SNR>=7 peak and one even passed its calibrated FAP, yet ZERO were accepted, because a blind accept demands a DIRECT catalog match (harmonic 1) and an empty field has no catalog row to match -- so accepted-only auto-anchoring can never stake a planet-free star. Purity stays 100% by construction, not by tuning.

Real-sky blind recovery of UNCONFIRMED candidates (npm run p7:toi-survey)

p6:survey measures the blind critic on synthetic curves; p7:toi-survey is its real-sky complement -- a genuine independent re-detection test, not a verification. It scales the p5:blind protocol to tens of REAL unconfirmed TESS candidates (the Archive toi table minus confirmed hosts): pull the same token-free SPOC photometry production uses, run detectTransit BLIND (the whole period grid, the candidate's reported ephemeris NEVER shown), and only afterwards cross-match the period we recovered against the TOI reference. A direct fundamental match at SNR>=7 is evidence the signal is real in the downlinked pixels, independent of the discovery pipeline's own answer.

Measured on 70 candidates that had token-free data (5 skipped, no product):

independent blind recovery (fundamental, SNR>=7): 27/70 = 38.6%   (+7 harmonic-only aliases, not counted)
median |recovered period - TOI period| = 0.22%  (max 1.62%)  -- recovered blind, ephemeris withheld
by period bin:  <2d 8/14   2-4d 9/16   4-7d 6/12   7-13d 4/11   >13d 0/17

The period bins are the honest story: within the single-sector confirmable regime (<13 d) we independently recover 27/53 = 51%, and 0/17 beyond it -- a 20-70 d orbit yields too few transits in one ~27-day sector to pin down, a physical coverage limit (the same one the real-sky completeness study shows), not a pipeline failure. The recovered PERIOD is the reliable independent measurement (sub-percent); the BLS box DEPTH is a crude proxy (median 26% off the reported depth) and is reported as such, not dressed up -- we later fit the exact transit model to repair precisely that, below. SAFETY BOUNDARY: like p5, this is a research probe off the money path -- every hit is a CANDIDATE for human follow-up, nothing is anchored, posted, spent on, or claimed as a discovery.

Independent DEPTH, repaired: an exact limb-darkened fit beats the crude box (npm run p9:exact-depth)

p7 promised that the crude BLS box depth "needs the exact-transit model fit, a separate step." p9 IS that step, and it validates the repair against ground truth on REAL confirmed planets. For each one that has a token-free SPOC curve and a measured stellar radius, we fold the photometry at the catalog ephemeris, fix a/Rs from Kepler (catalog R*/M* + period) -- which collapses the problem to a well-posed 3-parameter fit -- and least-squares fit the exact quadratic limb-darkened occultation model (src/detect/transitFit.ts, the same physics as src/sim/exactTransit.ts) over radius ratio p, impact parameter b and a small timing offset. The catalog depth is used ONLY as a post-hoc score, never shown to the fit; BLS is run on a narrow grid at the SAME period so the two depth estimators are compared apples-to-apples on the same curve.

Measured on 48 confirmed planets (a replication at ~1.7x the earlier 28; every headline below holds):

median |fit depth - catalog depth|:   exact-model 13.8%   BLS box 32.7%
exact fit closer than the box:        40/48 = 83%
unphysical (<= 0 ppm) depths:         box 4, exact-model 0  (the forward model is bounded >= 0)
short period (< 13 d, 32 planets):    exact 12.1%  vs box 30.7%
long period (>= 13 d, 16 planets):    exact 18.4%  vs box 38.1%   (exact still wins; both degrade)
median statistical 1-sigma on depth:  5.8% of depth
catalog depth inside +/-1sigma:       16/48 = 33%  (a calibrated error would be ~68%)
catalog depth inside +/-2sigma:       22/48 = 46% (calibrated ~95%)
LD-sensitivity floor on depth:        12.1% (all) / 11.9% short-period  (refit across 4 limb-darkening laws)
independent planet RADIUS vs catalog:  6.5% median (all) / 5.5% short-period  (Rp = p x Rstar; pl_rade withheld)
combined stat+LD 1-sigma coverage:     42% (statistical-only 33%; still < ~68% -> an approximate bar)
empirical inflation to reach 68%:      k = 3.2x  (honest 1-sigma = 3.2 x combined bar; 33/48 = 69% inside)

The honest reading: on well-sampled short-period transits the exact fit recovers the catalog depth to about 12%, LESS than half the BLS box error, and -- unlike a box average that can dilute a short transit into nothing -- it never returns an unphysical negative depth (the box did on 4 of 48). The advantage narrows on long-period (>=13 d) and magnetically active hosts, where one or two transits per sector weakly constrain the p-b shape and flares/spots/blends corrupt the fold: there the exact fit still beats the box (18% vs 38% median) but both degrade, and individual outliers reach 40-80% -- the same single-sector coverage cliff p7 measured and the active-star regime p8 isolated, now visible in the depth estimator too, reported rather than hidden. The fitted impact parameter b is for shape context only: we did not fetch catalog b, so it is NOT scored here. Offline, tests/transitFit.test.ts (deterministic, no network) confirms the fitter recovers an injected depth to grid resolution noiseless, to ~12% under realistic scatter, and that its statistical error bar shrinks with the in-transit cadence count.

The error bars are optimistic, and we say so. The fit also reports a first-order statistical 1-sigma (from the point-to-point scatter and the number of in-transit cadences). Its median is only ~5.8% of depth, yet the catalog depth lands inside +/-1sigma just 33% of the time and inside +/-2sigma only 46% -- far below the ~68% / ~95% a well-calibrated error would give. That undercoverage is the honest signal that the residual is dominated by SYSTEMATICS -- the fixed limb-darkening law, an imperfect ephemeris, and stellar activity -- not photon noise. So we quote the depth as a point estimate with a statistical bar and treat that bar as a LOWER bound on the true uncertainty; over-stating precision would be exactly the kind of self-deception the rest of this section is built to avoid.

And we found WHERE that floor comes from. Re-fitting the SAME folded curve under four plausible limb-darkening laws (uniform / modest / solar / strong) moves the short-period depth by a median 11.9% -- almost exactly the 12.1% we are off the catalog, and double the 5.8% statistical sigma. In other words the unknown limb darkening of each star, NOT photon noise, sets the depth-accuracy floor; that is precisely why the +/-1sigma bar undercovers. We carry an honest single solar-like law rather than fitting u1,u2 per host (which would just trade one uncertainty for a worse degeneracy on one-sector cadences), and report the LD spread as the real systematic budget. SAFETY BOUNDARY: this validates a measurement METHOD on already-confirmed planets -- nothing is anchored, posted, or claimed as a discovery.

From depth to size. Multiplying the fitted radius ratio by the catalog STELLAR radius (never the planet's catalog radius) gives an INDEPENDENT planet radius. It agrees with the published value to a median 6.5% (5.5% on short-period planets) -- almost exactly HALF the depth error, as it must be, since radius scales as the square root of depth. So the same fit that repaired the crude box depth also yields the planet's physical SIZE to a few percent, straight from the shape of the light curve -- a quantity a non-specialist can grasp immediately (is it Earth-sized? Neptune-sized? Jupiter-sized?) and a genuine, self-consistent measurement chain: pixels -> shape -> depth -> radius.

Can we make the error bar honest? Adding the measured LD spread to the statistical sigma in quadrature lifts the 1-sigma coverage from 33% to 42% and the 2-sigma from 46% to 56% -- a real improvement, but still short of the ~68% a fully calibrated error would reach. Rather than stop at "approximate," we calibrate it empirically: scanning a single inflation factor k on +/-k*(combined bar), the coverage crosses 68% at k = 3.2 (33/48 = 69% inside). So an honest depth uncertainty is about 3.2x the combined bar -- several times the naive statistical sigma. That is the concrete, defensible form of "the residual is systematics- dominated": limb darkening is the largest term, but an imperfect ephemeris and stellar activity keep the total a few times larger than photon noise alone would suggest. Quoting the tight statistical sigma would have overstated our precision by a wide margin; we publish the measured multiplier instead of hiding the gap.

The same rule now governs the product surfaces, from a later measurement. P90 (reports/p90-depth-estimators.md) graded the two depth estimators the PRODUCTION path actually runs -- the BLS box and the per-sector shape fit, both at the detector's own ephemeris -- against the archive on 16 period-matched planets, and reported not only the median absolute error (33.0% for what the adoption guard publishes) but the radii that cover 68% and 95% of those rows (48.0% and 104.4%). A "+/-" is read as one standard deviation, so the bar a card prints is the 68% radius carried up to one significant figure, combined in quadrature with each record's own statistical term 1/|snr| -- never the median, which covers only half the rows. Depth and period now go through one rule on every surface (src/core/depthFormat.ts, src/core/periodFormat.ts, mirrored for the board by web/honest-precision.js, held in step by tests/depth-honesty.test.ts, which also re-derives the cited numbers from P90's own stored rows). The tail is heavy enough (worst row 188.9%) that no surface doubles this bar into a 95% interval.

Independent SIZE of blind-recovered, UNCONFIRMED candidates (npm run p11:candidate-size)

p7 re-detected the ORBIT of real unconfirmed candidates straight from the pixels, blind; the depth fit above shows the exact model measures a planet's SIZE from the SHAPE of one transit. p11 fuses them: for every candidate p7 can independently re-detect, we fit the exact model at the period AND EPOCH WE RECOVERED (the TOI ephemeris is never shown) and report an independent depth and planet radius, then compare -- only AFTER the fact -- to what the TOI discovery pipeline published. Two independent measurements agreeing is a CONSISTENCY check, not a confirmation.

Measured on 60 candidates that had token-free data:

blind-recovered (direct match, SNR>=7):  24/60 = 40%   (independently reproduces p7's 38.6%)
characterized (host radius available):   19
independent DEPTH  vs TOI:  median |err| = 20%
independent RADIUS vs TOI:  median |err| = 15%  (6/19 within 10%, 11/19 within 20%)

The candidate agreement (15% in radius) is LOOSER than on confirmed planets (6.5%) -- exactly as it must be, because a single short sector, a main-sequence mass approximation for a/Rs (the toi table carries no stellar mass), and the occasional internally inconsistent or blended TOI row all add error; a few candidates differ by 45-60%, precisely the outliers a real survey flags for follow-up rather than buries. What the study establishes is that the same shape-fitting machinery that repaired the depth on known worlds also produces a sensible, independent size on REAL worlds it has never been fitted to before -- straight from the pixels, blind. SAFETY BOUNDARY: every object is a CANDIDATE; none is confirmed, anchored, or claimed as a discovery here.

Attacking the long-period cliff: more sectors, but fit them PER SECTOR (npm run p12:multisector)

Everything above used ONE ~27-day TESS sector, which is why long-period planets (too few transits in a short baseline) are the known weak spot -- the p7 recovery cliff and the p9 long-period degradation both come from it. The obvious fix is to stitch every available sector into one long curve and fold it all at once. p12 tests that idea directly on 18 confirmed planets with P >= 10 d, comparing three depth estimators scored against the catalog value (post-hoc reference only, never shown to a fit):

median depth |err|   single best sector : 39.6%
median depth |err|   one long fold       : 34.7%   (all sectors stitched, folded at the catalog ephemeris)
median depth |err|   per-sector median   : 24.9%   (fit each sector alone, combine the depths by median)
per-sector beats single 13/18, beats long-fold 11/18; 17/18 targets had >=2 SPOC sectors (up to 6)

The result is the opposite of naive intuition, and we report it honestly. Multi-sector SPOC data turns out to be WIDELY available and the long fold gathers about twice the in-window points, yet folding 1000-1800 days at the catalog period is only marginally better than one clean sector and is FRAGILE: a tiny period error accumulates phase drift across the long span, individual transits stop landing at the same fold phase, and a single-epoch model then smears the dip (worst case here, TOI-2128 b: one sector mis-fit at 5952%, the long fold at 48%). Fitting each sector on its own sidesteps that drift -- inside one 27-day sector the ephemeris is locally coherent -- and taking the median across sectors rejects the occasional bad sector, so the ROBUST way to exploit extra sectors is per-sector characterization plus a combine, NOT one giant fold. This reframes the long-period limit as a PHASE-COHERENCE problem rather than a data-volume problem, and points the next iteration at per-transit-epoch refitting (already prototyped in oddEvenRefined) long before any of it would touch a gate. SAFETY BOUNDARY: research probe only -- the additive stitchLightCurves helper in mastS3.ts, the production detection path is untouched, nothing is gated, and no planet is claimed.

Testing, not asserting, the limb-darkening floor: fit u1 to the data (npm run p13:ld-fit)

p9 found the depth-accuracy floor is set by each star's UNKNOWN limb darkening, and chose to carry a single fixed solar-like law (u1=0.4, u2=0.1) rather than fit u1,u2 per host, CLAIMING the extra freedom would just be absorbed by a degeneracy on one-sector cadences. p13 puts that claim to the TEST instead of asserting it: on the 15 brightest, best-sampled confirmed planets (deep >= 3000 ppm, P < 13 d -- the high-SNR regime where limb darkening is MOST fittable), it profiles the exact-model fit over u1 (u2 tied to u1) and keeps the data's preferred value, then scores the resulting depth against the catalog.

median depth |err|, FIXED solar LD : 7.0%
median depth |err|, u1 FIT to data : 7.0%   (identical)
u1-fit lowers the fitted rms on 14/15 planets, yet beats the fixed law on only 9/15 (6 are worse)
fitted u1* : median 0.50, range 0.15-0.75 (solar assumed 0.4)

That pattern is the textbook signature of a degeneracy, and it is the honest verdict: letting one more parameter chase the scatter drops the rms almost everywhere (as any added freedom does) but leaves the physically-meaningful error -- the depth against the catalog -- COMPLETELY unchanged, with per-planet shifts scattered both ways. The extra limb-darkening freedom is absorbed by the radius-ratio / impact-parameter / LD covariance and buys nothing real. This CONFIRMS, with data rather than argument, the fixed-law choice p9 had only asserted: the LD floor is not removable by fitting the light curve alone -- it needs external, per-star limb-darkening knowledge (spectroscopy or TESS-band stellar models), which is out of scope for a pixel-only rig. (Note the bright-planet fixed error is 7.0%, tighter than the 12% full-sample floor -- brighter and better-sampled stars are simply better constrained.) SAFETY BOUNDARY: research probe only -- transitFit is untouched, the production path is unchanged, nothing is gated, and no planet is claimed.

A tempting fix that did not work: refine the ephemeris before the long fold (npm run p14:coherence)

p12 said the long-period depth limit is phase coherence, not data volume -- so the obvious repair is to stop trusting the catalog period: time each transit, refit period+epoch by an O-C line (src/detect/ephemeris.ts, offline-tested: it recovers an injected drift perfectly on synthetic data), then fold the whole baseline at the refined value. p14 tries exactly that on 18 real multi-sector long-period planets and reports an honest NEGATIVE result:

median depth |err|  naive long fold       : 33.3%
median depth |err|  per-sector median     : 28.3%   (p12's robust baseline -- still the winner)
median depth |err|  refined-ephemeris fold: 47.5%   (WORSE than not refining at all)
refined beats naive on 3/18, per-sector on 2/18; |refined - catalog| period shift median 0.025%

Why it fails, and what that teaches: the catalog periods were ALREADY precise (our re-fit moves them by a median 0.025%), so there is almost no drift to recover -- yet with only a handful of independently-timed transits per long-period target (2-11, median ~4), our own O-C period is noisier than the discovery team's, so folding at it smears the dip MORE, not less. The synthetic unit test passes; real cadences humble the idea -- the recurring lesson here that a method must be validated on real data before it is trusted. The takeaway is constructive: the long-period cliff is NOT removable by self re-timing from a few sectors; the robust route stays per-sector characterization, and beating it needs genuinely more and better-timed transits (a longer baseline, higher SNR), not a cleverer fold of the same sparse ones. SAFETY BOUNDARY: research probe only -- ephemeris.ts is additive and offline-tested, the production path is unchanged, nothing is gated, and no planet is claimed.

Independent review of an external candidate claim (npm run p15:review)

The same blind-re-detection + host-vetting stack doubles as an INDEPENDENT AUDIT of someone else's planet candidate. As a live test we reviewed P. Chopra's two Earth-sized TESS candidates (public repo + Zenodo, checked 2026-10-09): TIC 370612437 "Aakanksha" (P=3.5649 d, 210 ppm) and TIC 252721481 "Orbit" (P=4.9335 d, 279 ppm) -- FFI-cadence signals around nearby dwarfs that he asks the community to review. Our rig re-checks them independently, with his ephemeris withheld from our detector:

novelty   : neither TIC is in the Archive confirmed list (3243 scanned) or the TOI list (2103) -> "not on any NASA list" corroborated
host star : Gaia distances 158 ly / 134 ly match his; RUWE 0.89 / 1.03 (clean astrometry, no strong close-binary signal)
blend/EB  : dilution inside the 21" aperture only 1.8% / 0.4% (brightest neighbour 5.4 / 6.2 mag fainter) -> his EB worry is small, not zero
geometry  : his (P, depth, Rp/R*, b) implies T14 = 1.51 h / 1.70 h vs his measured 1.44 / 1.59 h -> internally consistent
blind BLS : our general box detector does NOT isolate either period (a ~1.5 h transit at 30-min cadence is ~3 points, 210-279 ppm shallow)

Honest verdict: every independent check we can run is CONSISTENT with two genuine, well-behaved small-planet candidates -- novel, correct host parameters and distance, clean astrometry, low dilution, self-consistent transit geometry. The one thing we could NOT reproduce is a from-scratch blind detection with our UNTUNED box BLS at 30-min cadence; that is a limit of our general detector (his tuned multi-season SDE ~10-11 is more sensitive), NOT evidence against him -- and it echoes our own p12/p14 finding that naive multi-season folding is fragile. So we neither confirm nor refute: we corroborate plausibility and echo his own "tentative, pending follow-up" status. The decisive test is his preregistered 2027 transits plus ground-based neighbour clearing / TRICERATOPS (which he notes is not yet done). This is a review of ANOTHER PARTY'S candidate, not our discovery; nothing is anchored or claimed.

The review, promoted to a reusable library + a detection-floor table (src/review/externalReview.ts, npm run p16:review)

"Independently auditing someone else's claim" is arguably the most valuable thing this rig does -- yet p15 was a throwaway script. We factored the durable parts into a dependency-injectable library so the audit is a reproducible primitive, not prose:

  • impliedGeometry (Kepler-III transit-duration self-consistency), periodPowerRank (where the claimed period sits in OUR blind full-grid power), blendRiskTier (Gaia dilution -> low/moderate/high/undetermined EB risk, always a diagnostic, never a reject), detectabilityFloor (an honest statement of when WE are blind), makeCatalogStamp (binds scanned counts + an as-of UTC stamp to every number, so a re-run can be checked for catalog drift), and reviewExternalCandidate(claim, deps) (the orchestrator).
  • All pure functions + the orchestrator are unit-tested OFFLINE via fake channels (tests/externalReview.test.ts); the worker does not import this module, so the production bundle and money path are untouched.

p16:review drives the library on LIVE public data and adds a piece p15 never made explicit -- the DETECTION FLOOR: for each claimed planet it prints whether our untuned box-BLS could even resolve that dip shape across sampling regimes. Measured (2026-10-12, the SPOC product for these targets arrived at ~3-min, not 30-min, cadence):

Aakanksha  210ppm / 1.44h  ->  2min:43pts  5min:17pts  10min:8.6pts/BLIND  20min:BLIND  30min:BLIND  60min:BLIND
Orbit      279ppm / 1.59h  ->  2min:48pts  5min:19pts  10min:9.5pts/ok  20min:BLIND  30min:BLIND  60min:BLIND
novelty/host/blend/geometry all reproduce p15 exactly (3243+2103 scanned; 158/134 ly; RUWE 0.89/1.03;
1.8%/0.4% contamination -> LOW; T14 1.51/1.70h consistent); our blind BLS still ranks his period #2455/#6596
of 20001 -> corroborated = NO on both.

The floor proxy is honest about its own limits: it captures SAMPLE-STARVATION (a dip shorter than ~2 samples is unresolvable regardless of depth) and DEPTH, but NOT the multiple-trials penalty of a ~20k-period blind search or per-star red noise -- which is exactly why, even at ~3-min cadence where the proxy says "not sample-starved", the blind grid still loses his ~200 ppm signal to noise+trials. "We did not detect it" is therefore always printed next to the quantified reason it is OUR limit, not a statement about the planet.

Related fix: the audit surfaced a real correctness bug now closed at the source -- Gaia returns source_id as a JSON NUMBER and many ids exceed 2^53, so await r.json() silently rounded 1981360227180249088 -> ...249000 (a false "no such star"). src/data/gaiaDilution.ts now asks ADQL for cast(source_id as varchar), verified against the live endpoint, with a regression test that pins both the faithful path and the precision-loss trap.

Per-sector robust merge, wired in behind a default-off gate (src/detect/perSector.ts, perSectorRun.ts, workflow.ts)

p12/p14 measured that long-period depth accuracy is limited by PHASE COHERENCE, not data volume: fitting each sector locally and taking the robust MEDIAN (24.9% median error) beats stitching all sectors and folding once over a ~1800-day baseline (34.7%), because a tiny period error smears the transits in the long fold. That statistic is now packaged as combineSectorFits (median + scaled-MAD over usable per-sector depths, dropping !ok/non-finite, NaN when nothing is usable) and wired into the production edge via robustPerSectorDepth (src/detect/perSectorRun.ts), which folds + exact-fits EACH sector and returns the robust median.

It sits behind env.PERSECTOR and is OFF by default: the scout step only recomputes the depth when PERSECTOR === "1" AND the target is real multi-sector SPOC photometry AND >=2 sectors are characterisable, else it falls back to the single-fit depth. With the flag unset the whole run path -- and the bundle hash -- is byte-for-byte unchanged, so this wiring is inert until an operator deliberately (a) deploys it and (b) flips the flag in a safe window. tests/perSectorRun.test.ts validates the ON logic OFFLINE (median shrugs off a biased sector, null fallback below 2 sectors, a maxSectors fetch cap) via an injected fetcher -- no network, in CI.

STATUS: enabled (wrangler.toml PERSECTOR="1") and deployed by operator decision on 2026-10-12, so live runs on real multi-sector SPOC targets use the robust per-sector depth; set PERSECTOR="0" + redeploy to revert.

Cadence-aware search, opt-in only (cadenceAwareOptions in src/detect/bls.ts)

A box narrower than the sampling cannot be resolved: bls defaults minDurationHours to 0.5, which at TESS 30-min FFI is a ~1-sample box. cadenceAwareOptions(cadenceSec) returns options keeping the smallest trial box at least k samples wide (default k=3 -> 1.5 h at 30-min). It is deliberately OPT-IN: bls() and detectTransit() keep their certified defaults, so the worker's real-photometry path is byte-for-byte unchanged (no bundle/money-path effect) and only a coarse-cadence caller opts in. tests/cadenceAware.test.ts pins both the numeric mapping and the "default unchanged" invariant, plus that an opted-in search can never return a box thinner than the floor it asked for.

BLS cross-validation against astropy (npm run p2:bls-astropy, dev-only)

Our BLS is a self-contained, deterministic implementation of the same box-least-squares statistic astropy ships as astropy.timeseries.BoxLeastSquares. p2:bls-astropy proves they agree rather than asserting it. The TS half ALWAYS runs: it builds three fixed-seed light curves (two white-noise, one AR(1) rho=0.5 red-noise active star), self-checks that OUR BLS recovers each injected period to within one grid step, and writes the curves plus our grid to out/bls-astropy-fixtures.json. The Python half (scripts/bls_astropy_check.py) then runs astropy's BoxLeastSquares on the SAME fixtures and compares best period (1% astropy-vs-ours, 2% vs truth -- one grid step) and transit depth (25%). It is dev-only and FAILS SAFE: if Python, numpy or astropy is missing it prints an honest SKIP and exits 0, so CI and a stock npm install are unaffected. On a machine that has astropy, npm run p2:bls-astropy runs both halves and prints an aligned per-case PASS/FAIL table.

Gaia dilution / blend diagnostics (src/data/gaiaDilution.ts, npm run p3:gaia)

The pixel centroid test rejects an off-target source. The complementary, also phase-INDEPENDENT question is DILUTION: an unresolved faint companion inside the aperture adds constant light, so the observed depth UNDERSTATES the host's true depth (and a diluted eclipsing binary on that companion could mimic a shallow planet). p3:gaia queries the anonymous Gaia DR3 archive (gea.esac.esa.int TAP; a read-only probe confirmed the endpoint, the COLUMNAR {metadata,data} JSON shape, and that distance is a reserved ADQL word) and sums neighbour flux (G band as a documented TESS proxy) within the aperture. On 10 confirmed-planet hosts the measured dilution factor was ~1.000 (min 0.999) and 0/10 had more than 5% contaminating light -- neighbours were +8 to +15 mag fainter, even in the 27-source crowded field of TOI-5789 -- so these planets' depths are trustworthy as-is.

Dilution ships as a correction/diagnostic, not a rejection gate, for two honest reasons: a real planet can legitimately orbit a diluted star (unlike an off-target centroid, low dilution is not proof of authenticity), and we decline to bolt a rate-limited third external service onto every production run for a quantity that cannot decide a verdict. gaiaDilution.ts's columnar parser and aperture sum are unit-tested offline against a captured Gaia JSON fixture (which also caught the same Number(null)===0 coercion pitfall -- null magnitudes/coordinates must become NaN).

Quickstart

Requires Node.js 22 or 24 (ESM, run via tsx). No database, no build step.

npm install

# 1) run the science pipeline (real NASA data by default; --offline uses the bundled catalog)
npm run orchestrator -- --targets 4 --delay 250
npm run orchestrator -- --offline            # deterministic, no network
npm run orchestrator -- --resume             # continue from the last snapshot

# 2) serve the live dashboard + HTTP API + SSE
npm run api                                  # http://127.0.0.1:8787/

# 3) anchor accepted bundles on Solana mainnet
npm run anchor -- --dry-run                  # preview memos, send nothing
npm run anchor                               # anchor all accepted findings
npm run anchor -- --verify                   # re-read the chain, confirm hashes

Artifacts are written under out/:

out/
  state/rig.json          the snapshot (checkpoint / restore point)
  findings/*.svg          plot gallery
  bundles/<findingId>/    manifest.json + HASH.txt + the three SVGs
  ledger.json             local mirror of on-chain anchoring state
  anchor-keypair.json     auto-generated throwaway mainnet fee-payer keypair

The live dashboard

npm run api then open http://127.0.0.1:8787/. The page is served by the same Hono server as the API (no separate frontend host) and updates over a single Server-Sent Events connection:

  • Stats strip - confirmed / watchlist / rejected / on-chain / targets done.
  • The team - a live topology of the five seats with per-seat status and last message, so you can see exactly which agent is working.
  • Run controls - start a run from the browser (targets, stage delay, recovery trials, offline, resume). Progress streams in real time.
  • Findings board - cards ranked by critic score, each with the verdict badge, the headline, the hard numbers (period, depth in ppm, SNR, duration, recovery confidence, known-planet match), the plain-English explainer, the bundle hash, and the on-chain tx. Buttons let you open the plots, verify the bundle, and anchor it on mainnet.
  • Display / projection layer (immutable bundles + honest relabeling). Bundles are sealed and append-only, so a card always renders the exact headline that was committed on-chain - we never rewrite history in place. Before the 2026-10 honesty pass the user-facing word for verdict=accepted was "Confirmed"; it is now "Recovered" (injection-recovery: the pipeline recovered a signal it was pointed at, not an independent confirmation), and every new headline is prefixed [real TESS photometry] or [stand-in synthetic curve]. Because older bundles still store the old wording, the render layer derives a relabel rather than mutating them: a card whose stored headline still says "confirm" AND whose createdAt is on or before the policy date (2026-10-12, inclusive - audit-4 caught that a strict < boundary excluded every legacy card, since all four live ones were created ON that date) gets a visible note - "Re-labeled under the 2026-10 display policy: injection-recovery, not an independent confirmation." Post-policy cards are untouched. Immutability must not mean the public history stays frozen at its most overclaiming version; the predicate lives in web/relabel.js (headlineRelabelNote), imported by web/app.js and shared with tests/live-data-regression.test.ts, so it is the single source of truth for the projection and the browser and the test run the SAME function.
  • Live feed - the raw seat event stream.
  • On-chain ledger - every anchoring attempt with its status and explorer link.

HTTP API

Base URL http://127.0.0.1:8787 (override with PORT / HOST).

Method Path Purpose
GET /health liveness/readiness probe (used by docker-compose + rig.yaml)
GET /api/state full dashboard payload: stats, seats, findings, bundles, events
GET /api/run/status current/last run state (Node)
GET /api/findings all findings (newest first); accepts the v0.2 query (verdict, origin, tic, q, periodMin/Max, depthMin/Max, snrMin, sort, dir, limit, offset) -- a valid dir=asc engages the default sort but still pages at limit (default 100, max 500), so ascending over the whole catalog asks limit=500; hasMore discloses the truncation, and criteria that were dropped or bounded come back as ignored/clamped
GET /api/findings/:id one finding
GET /api/bundles bundle records
GET /api/bundles/:id/manifest the reproducible bundle manifest JSON
GET /api/bundles/:id/verify recompute the hash + re-hash artifacts
GET /api/bundles/:id/artifact/:name serve folded.svg / periodogram.svg / timeseries.svg
GET /api/ledger anchoring ledger (bundle anchors)
GET /api/discussion grounded deliberation threads for all findings (fast, no LLM)
GET /api/discussion/:id on-demand two-model cross-examination; seals the receipt and returns deliberationHash
GET /api/discussion/:id/receipt the sealed deliberation receipt JSON
GET /api/discussion/:id/receipt/verify recompute the receipt hash and compare (tamper check)
GET /api/discussion/:id/anchor read back any on-chain deliberation anchor
GET /api/events live event stream (SSE on Node; WebSocket-first with SSE fallback on the edge)
POST /api/run start a run { targets, delay, confidence, tmag, offline, resume } (Orchestrator on Node; triggers the rig-run Workflow on the edge)
POST /api/anchor/:id anchor one finding's bundle hash on mainnet { dryRun } (memo sdr:1|...)
POST /api/anchor/:id/deliberation anchor the sealed deliberation receipt { dryRun } (memo sdr:delib:1|..., idempotent, separate from the bundle ledger)

POST /api/run returns immediately and streams progress over SSE; only one run at a time (a second call returns HTTP 409).


Reproducibility and on-chain provenance

Bundle hash. The reporter writes a manifest.json capturing the recovered science, the verdict and its individual checks, the recovery confidence, the predicted ephemeris, data provenance (catalog source + light-curve hash), and a SHA-256 of every artifact. The bundle hash is SHA-256(canonicalJson(manifest)), where canonicalJson sorts object keys recursively and strips insignificant whitespace - so logically-equal manifests hash identically across machines and runs.

Anchoring. npm run anchor writes each accepted bundle hash to Solana mainnet in an SPL Memo instruction (MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr) using the compact record sdr:1|<findingId>|<bundleHash>. No custom program or token is involved. The non-reproducible AI deliberation is anchored separately, on demand, under its own sdr:delib:1|<findingId>|<deliberationHash> memo -- a distinct schema so an auditor can never confuse AI commentary with the reproducible science bundle, and it never touches the bundle ledger or the bundle hash.

RPC failover. Endpoints are tried in order: the private RPC in SOLANA_RPC (a local, untracked .env, e.g. an Alchemy mainnet endpoint) first, then SOLANA_RPC_FALLBACKS, then the public mainnet RPC - the first provider that answers wins, so a rate-limited or down endpoint falls back transparently. Copy .env.example to .env to configure it.

Graceful by design. If every RPC is unreachable, or the fee-payer is not funded with real SOL, anchoring returns { ok:false, error }, the local ledger marks the entry failed/pending, and nothing crashes - the bundle hash remains fully valid and verifiable off-chain. Mainnet has no faucet: point --keypair <funded-id.json> at a wallet that already holds SOL, or fund the generated address:

npm run anchor -- --keypair out/anchor-keypair.json   # out/anchor-keypair.json must hold SOL

Independent verification. Anchoring is checkable without trusting this app:

npm run anchor -- --verify              # re-reads each tx and confirms the memo matches
curl http://127.0.0.1:8787/api/bundles/F-HD-189733-b/verify   # recomputes hash + artifacts

OpenRig integration

openrig/ is a self-contained OpenRig rig (RigSpec 0.2) that runs these five seats as a real agent team (Claude Code + Codex runtimes) with the deterministic code containerized as managed services. See openrig/README.md for launch, validation, snapshot/restore, and bundling instructions. OpenRig requires WSL2 on Windows (plus Node 22/24, tmux, and Docker for the services block).


Project layout

src/
  core/         types.ts (domain model), store.ts (RigStore), ports.ts (IO ports), state-types.ts
  data/         exoplanetArchive.ts (NASA TAP + fallback), synthetic.ts (red-noise stand-in builder),
                mast.ts + fits.ts (real TESS download + zero-dep FITS reader),
                mastCurated.ts (curated real-product registry), mastDiscover.ts (best-effort CAOM),
                mastS3.ts + mastS3Pixels.ts (token-free SPOC photometry + target-pixel cube),
                toiArchive.ts (blind unconfirmed-TOI source), gaiaDilution.ts (blend diagnostic)
  detect/       bls.ts (+ cadenceAwareOptions opt-in), detrend.ts, pipeline.ts, svgPlot.ts, fap.ts (phase-scramble null),
                significance.ts (per-candidate calibrated FAP), vetting.ts (odd/even + secondary),
                transitFit.ts (exact limb-darkened depth/shape least-squares fit; offline only),
                ephemeris.ts (per-transit O-C timing -> refined period/epoch; offline only),
                perSector.ts (robust per-sector median merge, pure) + perSectorRun.ts (wired into the
                edge scout step behind env.PERSECTOR, default OFF -> unchanged when off)
  sim/          transitModel.ts, exactTransit.ts (limb-darkened), injectionRecovery.ts, confidence.ts,
                aliasResolver.ts, redNoise.ts (AR(1) + estimateRho), roc.ts (completeness/purity),
                injectInto.ts (inject into real curves), uncertainty.ts (block bootstrap)
  critic/       review.ts (cross-match + verdict + score)
  review/       externalReview.ts (independent audit of a third-party claim as a reusable library +
                detection-floor model; DI-testable offline, NOT imported by the worker -> no bundle change)
  reporter/     narrate.ts (plain-English narration)
  bundle/       bundle.ts (canonical JSON + SHA-256 manifest + verify)
  ai/           llm.ts (multi-provider client), discussion.ts (two-round cross-examination),
                threads.ts (grounded threads), deliberation.ts (sealable receipt + hash)
  seats/        seats.ts (seat role wiring)
  orchestrator/ pipeline.ts (the 5-seat Orchestrator), run.ts (CLI driver)
  solana/       anchor-core.ts (runtime-agnostic memo + chain path), anchor.ts (Node adapter + ledger)
  node/         artifact-store.ts (fs ArtifactStore), code-version.ts
  cf/           worker.ts (edge Hono API), rig-state.do.ts (Durable Object), workflow.ts (rig-run
                Workflow), artifact-store.ts (R2), env.ts, run-types.ts
  api/          server.ts (Node Hono HTTP API + SSE + static dashboard)
scripts/        p0-*.ts, p1-{roc,rho,fap-calibrate,real-completeness,uncertainty}.ts,
                p2-{vet-calibrate,anchor}.ts, p2-bls-astropy.ts + bls_astropy_check.py (astropy
                cross-check), p3-{discover,gaia-dilution}.ts, p4-autonomous.ts, p5-blind-probe.ts,
                p6-blind-survey.ts, p7-blind-toi-survey.ts, p8-noise-acf.ts, p9-exact-depth.ts,
                p10-figure.ts (renders web/transit-example.svg from a real transit),
                p11-candidate-size.ts (blind-recovered candidates: independent depth + radius),
                p12-multisector.ts (long-period: single vs long-fold vs per-sector depth estimators),
                p13-ld-fit.ts (tests whether fitting limb darkening beats the fixed law -- it does not),
                p14-longperiod-coherence.ts (per-transit ephemeris refit vs the long fold -- honest negative),
                p15-external-review.ts (independent audit of an external planet-candidate claim),
                p16-review-library.ts (the audit as a reusable library + a detection-floor table),
                validate-curated.ts (npm run mast:validate)
web/            index.html, styles.css, app.js, live.js, transit-example.svg (the dashboard)
openrig/        rig.yaml, CULTURE.md, Dockerfile, docker-compose.yaml, agents/*

npm scripts: orchestrator, api/start, anchor, p0:benchmark, p0:archive, p0:detect, p0:mast, p1:roc, p1:rho, p1:fap-calibrate, p1:real-completeness, p1:uncertainty, p2:vet-calibrate, p2:bls-astropy, p3:discover, p3:gaia, p4:autonomous, p5:blind, p6:survey, p7:toi-survey, p8:noise-acf, p9:exact-depth, p10:figure, p11:candidate-size, p12:multisector, p13:ld-fit, p14:coherence, p15:review, p16:review, mast:validate, cf:dev, cf:deploy, test, typecheck, typecheck:worker, typecheck:all.


Design constraints

  • Solana mainnet by default. Accepted bundles are sealed with an SPL Memo transaction on mainnet. The fee-payer keypair is operator-supplied (a Worker secret in production, a local file under out/ for Node runs) and must hold real SOL -- mainnet has no faucet. Devnet endpoints are still supported; airdrops only fire when the RPC URL targets devnet.
  • No token contract here. The token lives on pump.fun; this repo only anchors provenance via the standard SPL Memo program.
  • Deterministic and offline-capable. With --offline the whole pipeline runs against a bundled real-value catalog and touches no network, which keeps it provable anywhere.
  • English-only code. All source, comments, docs, and config are ASCII English with no non-Latin characters, so they render identically in the terminal, the JSON bundle, the browser, and on-chain.

Discovery roadmap (in progress)

The rig currently re-verifies planets we already know about. The roadmap turns it into a production-grade high-confidence candidate engine: it blind-searches data nobody has searched, rejects false positives to a validated level, ranks what is worth following up, and packages reproducible candidates for the real confirmation ecosystem. Two honest boundaries are fixed: the engine emits candidates, never discovery claims, and turning a candidate into a confirmed planet requires independent follow-up (other instruments, ground-based time, human vetting) that this pipeline hands off to rather than performs.

The two hard pivots this requires:

  • Seed source: from the confirmed-planet catalog (which can only re-find the known) to stars with no known planet, with the novelty check turned inward to filter our own detections.
  • Data entry: from curated/TESS-SPOC light curves to full-frame-image (FFI) photometry that covers the un-targeted sky.

Stages: 0 foundation (candidate contract, FFI feasibility, regression baseline) -> 1 real blind search on planet-free stars via FFI photometry -> 2 validation-grade false-positive rejection with completeness/purity measured on real data -> 3 prioritization + follow-up observability cards -> 4 production scale + ops -> 5 independent corroboration with collaborators (still labelled candidate by the pipeline).

Stage 0 status: complete.

  • Candidate contract src/candidate/candidateRecord.ts: a conservative confidence tier (high/moderate/low/sub-floor/not-novel/not-planetary) that DOWNGRADES on any undetermined axis, plus an executable assertHonest() that makes a discovery claim or an unjustified "high" impossible.
  • FFI feasibility probe scripts/p17-ffi-feasibility.ts (npm run p17:ffi): measured FEASIBLE -- token-free FFI-derived light curves (TGLC, QLP) are anonymously listable and downloadable from the public stpubdata S3 bucket, sharded by Gaia DR3 source_id; no photometry pipeline needs to be built. The remaining gap is a TGLC column map (its TIME/psf_flux columns differ from SPOC PDCSAP), which is a Stage 1 task.
  • Offline regression baseline src/orchestrator/regressionBaseline.ts + scripts/p18-regression-baseline.ts (npm run p18:baseline) and a golden test: locks the current closed-loop verdicts/periods so the FFI work cannot silently regress validated behaviour.

Stage 1 status: complete.

  • TGLC column mapping src/data/tglc.ts (selects a flux column by finite-value count, not by name -- some psf_flux columns are entirely NaN; reads host TIC/RA/Dec/Tmag from the PRIMARY header) and the first end-to-end blind search scripts/p19-ffi-blind.ts (npm run p19:ffi-blind): real sector FFI light curves -> blind BLS -> a calibrated depth screen -> a NOVELTY gate against the live confirmed + TOI catalogs -> an honest candidate record via the Stage 0 contract. Novelty is asserted only when BOTH catalogs come from the live archive; on any fallback it refuses to emit (guards the silent-truncation over-claim).

Stage 2 status: complete (transit-depth false-positive screen).

  • Depth is one of the few 1D measures safe to classify on single-sector FFI, but only once CALIBRATED. scripts/p21-depth-calibrate.ts (npm run p21:depth-calibrate) measures the confirmed-planet depth distribution (n=4207: p99 ~30589 ppm, observed max ~166590 ppm), which REFUTES a naive ">3% = EB" cut -- ~1% of real confirmed planets are deeper than 3%. src/candidate/falsePositive.ts encodes that as an auditable regime: only a signal deeper than EVERY known planet (>200000 ppm) is rejected as not-planetary; the sparse 40000-200000 ppm band is deep-suspect (kept, but capped below "high" until a resolved blend / eclipsing-binary check corroborates it). p19 wires the screen into the candidate chain.
  • The odd/even depth and secondary-eclipse discriminators stay DIAGNOSTIC ONLY and are deliberately NOT gated: real-data calibration showed they mis-kill true planets on single-sector photometry.

Stage 3 status: complete (prioritization + observability).

  • src/candidate/prioritize.ts ranks surviving candidates by evidence (SNR, brightness, tier), imminence (when the next transit falls), and observability -- the target's altitude at mid-transit from a reference ground site, computed with standard hour-angle astronomy (verified against time-independent invariants). It never re-labels a Stage-2 tier or adds a discovery claim; undetermined inputs stay neutral and are named in the card. p19 now prints a ranked follow-up queue. This needs ABSOLUTE epochs, so src/data/tglc.ts converts the TGLC reduced time (measured as BJD_TDB - BJDREFI, header value applied) to absolute BJD.
  • Odd/even and secondary-eclipse stay diagnostic-only; lifting deep-suspect/moderate records to a genuinely corroborated candidate still needs a second epoch / multi-sector coverage or a resolved blend.

Stage 4 status: false-positive control MEASURED (npm run p26:corroboration / p27:shape-coherence); scale-out remains data-gated toward Stage 5.

  • Measured on real Sector 1 data, the single-sector blind search returns the SAME (period, epoch) on several unrelated host stars -- a sector-common systematic (the crude "deepest cadence" epoch estimator plus a ~sector-length window alias of the BLS periodogram), NOT independent planets. Scaling out before handling this would multiply one artifact into many fake candidates. So Stage 4 opens with a coincidence collapse: src/candidate/dedup.ts clusters candidates by ephemeris (period within 0.5%, period-folded epoch within 0.05 d) and flags any group spanning multiple hosts as a suspected common systematic, ranking ONE representative each; p19 applies it before emitting the follow-up queue.
  • scripts/p22-blind-multisector.ts (npm run p22:blind-multisector) scales the p19 chain across a list of sectors, then (a) collapses coincident ephemerides (multi-host = suspected systematic) and (b) runs corroborateHosts -- the cross-sector test that a real signal recurs on the SAME host across >=2 independent sectors while a single-sector systematic does not. Ranked surviving candidates are persisted to a local JSON research artifact under out/ (git-ignored, NOT the production store); the script never surfaces or anchors.
  • The candidate pipeline is surfaced on the console in a default-open "CANDIDATE BOARD" section (placed above the FINDINGS CATALOG) with an honest tally banner up front (web/index.html + web/app.js). It reads a static snapshot web/candidates.json published by p22 and validated by src/candidate/snapshot.ts, which enforces the honesty invariant that no row can carry a discovery claim. The board is a read-only static asset -- it touches no worker/DO code, no verdict, no bundle hash and no money path, and is kept visually separate from the verified, on-chain results. Every row is labelled CANDIDATE with its tier, depth regime, follow-up priority, next transit + observability, and any shared-ephemeris / cross-sector flags.

Candidate-chain validation (measured, and it is humbling). The blind-search candidate chain (BLS -> Stage-2 depth screen -> SNR>=7) was put through injection-recovery on REAL TGLC full-frame-image light curves (npm run p23:candidate-validation). Over 60 quiet-star curves from sectors 1-3: a false-alarm rate of ~25% (15/60 planet-free stars still yield a surviving candidate) and low completeness (0% at <=1500 ppm, ~3% at 4000 ppm), purity proxy ~29%; a 30-curve run gave ~37% / ~11%. FFI blind search is far harder than targeted SPOC photometry, which recovers 60-88% at these same depths. Conclusion: gated only on SNR, the candidate chain is false-positive-dominated and NOT validation-grade. We then TESTED the obvious fix -- adding the per-candidate empirical-FAP gate the production verdict path uses (SNR>=7 AND FAP<=0.05, nulls matched to each star's own noise color) -- and it barely helped: the false-alarm rate only fell ~25% -> ~23%, completeness was unchanged, and the surviving false positives were still the shared-ephemeris systematics. The reason is instructive: those false alarms are COHERENT instrumental features (sector-common dips and spikes), which a stochastic noise surrogate cannot represent, so they score as "significant". A significance gate is therefore NOT the lever here. The real controls are cross-sector / multi-epoch corroboration and a resolved-blend / centroid check -- exactly what the board flags, and why it states up front that most rows are expected to be spurious. p23:candidate-validation stays as the permanent regression that measures all this.

The corroboration lever, MEASURED (npm run p26:corroboration). p23 named cross-sector / multi-epoch corroboration as the right FP lever but did not quantify what it costs. We packaged the gates as a pure, offline-tested module (src/candidate/corroboration.ts applyLayerFilters, 6 cases in tests/corroboration.test.ts) and drove them on REAL quiet-star TGLC curves in a two-arm ROC. Four cumulative layers on the null arm (every surviving planet-free host is a false positive by construction), measured on 60 hosts across sectors 1-5: L1 SNR+depth 33.3% (20/60); L2 + empirical FAP<=0.05 31.7% (19/60) -- barely helps, reproducing p23; L3 + reject ephemerides shared by MULTIPLE hosts 11.7% (7/60) -- a ~63% cut of the FAP-surviving false alarms; L4 + require the SAME host to recur across >=2 sectors 0%. A 30-host replicate gave the same picture (~70% L3 cut), and a LARGER 300-host run (sectors 1-10, --null-only) firms it: L1 27.7% -> L2 25.7% -> L3 12.3% (a 52% cut of the FAP-surviving FP), with 0/300 hosts recurring in two sectors -- so the effect is robust across three independent samples and the same-host corroboration wall is a genuine coverage limit, not a small-sample artifact. The decisive honest result: the cheap structural gate (L3, already implemented as collapseCoincident) removes most of the coherent systematics that the significance gate cannot, and it costs NO true planet -- on the injected arm the completeness is flat across L1->L2->L3 (5.8% -> 5.8% -> 5.8%, 240 injections of 1500/4000 ppm at 2/4 d), because a single-host planet is never a multi-host artifact. The extra cut L4 would give (drive FP to 0) is gated by same-host multi-sector coverage, which a single-pass FFI sample essentially never provides (0/60 hosts recurred -- the same wall p22 hit at 0 corroborated). To still price that lever we split one real curve into two sub-baselines and require a coherent injected transit to survive BOTH: completeness collapses 5.8% -> 0.8%, i.e. demanding repeatability in two independent windows is brutal on shallow FFI. So the honest, data-grounded conclusion is: adopt L3 now as the high-value FP control; treat same-host cross-sector corroboration (L4) as a tier-cap / flag, not a hard reject, because turning it into a gate would gut completeness until a genuinely deeper multi-sector baseline exists (Stage 5 -- more sectors / collaborators). This is a research measurement: nothing is gated, anchored, spent, or claimed; the candidate board already flags shared-ephemeris rows exactly as L3 sees them. Reproduce with npm run p26:corroboration -- --sectors 1,2,3,4,5 --limit 12 --depths 1500,4000 --periods 2,4 --seed 13.

L3 is now APPLIED, not just measured. The blind-search aggregator (p22) now runs partitionBySystematic (src/candidate/corroboration.ts) by default: multi-host shared-ephemeris groups are collapsed as systematics and NOT emitted as candidate rows -- only counted in the snapshot summary (systematicCollapsed, l3Applied) -- while single-host independent candidates pass through unchanged. The opt-in console's Candidate Board reads this and its banner now states how many groups were collapsed rather than showing them as candidates; --keep-systematic restores the old flag-only view. Regenerating the sectors 1-3 snapshot dropped 7 independent groups to 5 emitted candidates (2 shared-ephemeris systematics collapsed), with every row still discoveryClaim=false under the snapshot validator. This is still the research candidate chain only -- the worker does not import any of it, so no verdict, bundle hash, or money path changes, and it is not deployed by this code change.

A tempting cheap shortcut that does NOT work: in-transit shape coherence (npm run p27:shape-coherence). If the FP that survive the significance gate are one-point instrumental spikes, an in-box MEDIAN-vs-MEAN depth ratio (src/detect/shapeCoherence.ts, 3 offline cases) should read ~0 for a spike and ~1 for a transit that fills its own box. We MEASURED it rather than assumed, on 60 real quiet-star TGLC curves: injected true positives read median ratio 1.05, but the null-arm false positives read 1.02 (and the single-host residuals that survive the multi-host collapse 1.08 -- if anything HIGHER). The threshold scan flags 0% of FP while keeping 100% of TP at every cut: no separation at all. The reason is the same mechanism p26 exposed -- these are not ragged single samples; they are coherent features that stack consistently under the fold, so their box median matches its mean just like a planet's does. This is a useful negative result: it RULES OUT a per-curve shape shortcut and re-establishes that REPEATABILITY across independent baselines is the only lever on this FP class -- which is exactly why validation-grade needs the deeper multi-sector coverage deferred to Stage 5. Measure-only, nothing gated. Reproduce with npm run p27:shape-coherence.

Can the pipeline actually corroborate a REAL planet across sectors? A positive control says not yet (npm run p28:corroboration-proof). Before pointing the search at novel targets we tested the strongest FP lever -- cross-sector corroboration -- on data where the answer is known: 10 confirmed planets, each pulled from up to 4 real TESS-SPOC sectors covering it, run through a BLIND per-sector BLS (the catalog period is never shown to the detector). We added a period-only corroborator (corroborateByPeriod, src/candidate/corroboration.ts, 3 offline cases) alongside the strict corroborateHosts (period AND folded epoch must coincide). Measured result: the STRICT test fired on 0/10 real planets -- a single 27-day sector resolves a period too coarsely for the extrapolated epoch to match across sectors, so the board's L4 flag is effectively inert for blind detections. The PERIOD-ONLY test fired on 3/10 but matched catalog truth on only 1/3 (the other two were aliases landing on one shared period). And only 3/10 known planets even produced >=2 significant blind hits -- most confirmed planets are too shallow to re-detect sector-by-sector at SNR>=7. Honest conclusion, stated as a limitation of OUR detector not of the sky: a blind per-sector pass over public FFI is currently UNDER-POWERED for a defensible new-planet claim -- it often cannot even re-find planets we already know. A genuine discovery needs a JOINT multi-sector coherent period search (fold the long baseline together, not independent per-sector hits) plus deeper/longer data, and any surviving signal must still clear novelty + calibrated structural vetting + independent follow-up before anyone may say "planet." Measure-only; nothing gated, anchored, or claimed. Reproduce with npm run p28:corroboration-proof -- --targets 10 --maxsectors 4.

The joint multi-sector search fixes the resolution problem and proves the corroboration engine works (npm run p29:joint-search). p28 said the blocker was single-sector period resolution, so we built the joint method it called for: stitch a host's sectors into one long baseline, fit ONE period with BLS, then require a positive, planet-plausible dip in EACH independent sector at that ephemeris (foldedBoxDepth, src/detect/shapeCoherence.ts, 3 offline cases). On the same 10 confirmed planets the joint fit nails the PERIOD when it locks the true one -- 0.01-0.05% error (e.g. HR 858 b recovered at 3.5857 d vs catalog 3.585 d) -- and >=2 sectors confirmed a matching dip for 7/10 planets, versus p28's per-sector method which barely produced two significant hits at all. So the corroboration LOGIC is now proven on ground truth. The next bottleneck is period SELECTION, not sensitivity: stitched BLS reports the system's strongest periodicity, which in a multi-planet system is a real but DIFFERENT planet (HD 63433's three planets collapse onto the dominant ~3.19 d signal) and can land on a half/double alias (pi Men -> 3.135 d = P/2; HD 25463 -> 14.1 d = 2P). That is why a naive "did it match the one planet we queried" scores only 1/10 even though the engine is corroborating real signals. The honest to-do before a novel-target hunt: add alias / harmonic disambiguation (the odd-even resolver in src/sim/) and score per-planet, not per-system. Measure-only; nothing gated, anchored, or claimed. Reproduce with npm run p29:joint-search -- --targets 10 --maxsectors 5.

p30 tested that to-do and it OVERTURNS the optimistic read of p29: the real bottleneck is detection purity, not corroboration or aliasing (npm run p30:joint-alias). We did exactly what p29 named -- run the production odd/even alias resolver (resolvePeriodAlias, now with 3 dedicated offline cases) on the stitched curve, and score PER HOST SYSTEM (credit recovering ANY planet orbit of that star, since a multi-planet star lighting on its strongest signal is a success). On 10 host systems the honest result is sobering: the resolver changed the period on 0/10 (the stitched BLS peaks are not clean odd/even-fixable P/2 aliases -- sector gaps leave too few usable epochs), and even scoring per-system the recovered period matched a known planet of that host on only 2/10, with system-corroboration (match AND >=2 sectors confirming) at 1/10. Most raw periods were 12-100% off. Since p12 already showed these planets ARE in the data (folding at the KNOWN catalog period yields real depths), the failure is that a single blind BLS on a stitched multi-sector baseline frequently locks the WRONG period. Convergent conclusion across p26/p27/p28/p29/p30: this public-data blind pipeline is NOT yet discovery-grade -- it cannot reliably re-detect planets we already know -- so a genuine, defensible new-planet claim needs a stronger detector (iterative multi-signature extraction, TLS, targeted bright/shallow-first selection), and NOTHING short of that earns relaxing the honesty guardrails. Measure-only; nothing gated, anchored, or claimed. Reproduce with npm run p30:joint-alias -- --hosts 10 --maxsectors 5.

The source-level false-positive lever, MEASURED (npm run p31:common-mode-fp). p23/p26 established that the surviving false alarms are COHERENT instrumental systematics -- a dip or spike shared by many unrelated stars at the same cadence -- which a per-star empirical-FAP null cannot touch (it only cut 25% -> 23%) and which L3 only clusters AFTER detection. p31 attacks them at their SOURCE, in the photometry: within each sector we build a common-mode template (the robust median fractional deviation across the whole stellar ensemble at every cadence) and subtract it from each star BEFORE BLS (src/detect/commonMode.ts, research-only, worker not imported). A shared artifact is removed; a lone transit is a minority signal the median leaves intact (proven offline in tests/commonMode.test.ts). Measured on real TGLC sector-1 FFI quiet stars plus injected planets (seed 7), the false-alarm cut is robust at every ensemble size N -- 42.5% -> 27.5% at N=40, 51.7% -> 30.0% at N=120, 47.5% -> 28.2% at N=240 (-35 to -42%) -- the first lever here to move the false-alarm number materially. Completeness depends on N exactly as template-estimation noise (SE ~ sigma/sqrt(N)) predicts: it DROPS at N=40 (8.3% -> 6.7%, a small ensemble stamps its own noise on every star) but RISES at N>=120 (8.5% -> 10.1% at 120, 7.7% -> 9.7% at 240), lifting purity from ~66% to ~80% and nearly doubling 10000 ppm recovery (5.7% -> 9.8% at N=240). At N>=120 the full subtraction dominates a spike-gated variant on all three axes, so the default runs a

=120-star per-sector ensemble. This is a real gain in candidate purity and shallow-signal completeness; it does NOT overturn the p26-p30 "not discovery-grade" verdict, which is a period-SELECTION / single-baseline limit orthogonal to systematics removal. Measure-only; nothing gated, anchored, claimed, or deployed. Reproduce with npm run p31:common-mode-fp -- --sectors 1 --limit 240 --seed 7.

Iterative multi-signal extraction, MEASURED (npm run p32:iterative). p30 ended by naming the detector upgrade a real survey needs: a single blind BLS pass on a stitched baseline reports ONE period, so in a multi-planet system it can recover at most one co-orbiting planet (p30 per-system recovery 2/10). src/detect/iterative.ts (research-only, worker not imported) implements the standard fix -- find the strongest transit, resolve its half/third-period alias, refine the period on a dense local grid, MASK its in-transit cadences, re-run BLS on the residual, repeat -- and p32 scores it per host system against the single-pass baseline on REAL photometry. Phase 1 (catalog positive control, 12 multi-planet hosts, 29 known in-range planets, stitched public SPOC sectors, production gate SNR>=7 & depth>=200 ppm): single-pass 1/29, iterative 1/29, delta +0, 0/12 hosts improved -- on underpowered public few-sector curves the second, shallower/longer-period planet falls below the gate, so the limit is DATA POWER, not the algorithm. Phase 2 (real-noise injection positive control on the same curves, two floor-clearing transits injected multiplicatively at 3.3 d@2500 ppm + 8.1 d@1800 ppm): iterative recovers BOTH on 5/6 curves and 10/12 injected transits overall, while single-pass is capped at one period per star (5/12) -- the mask-and-refind algorithm is sound on REAL noise, confirming Phase 1's null is a data-power limit. Consistent with p26-p30; motivates deeper baselines / more compute. Measure-only; nothing gated, anchored, claimed, or deployed. Reproduce with npm run p32:iterative -- --hosts 12 --maxsectors 2.

Large-scale reproducible completeness / purity ROC, MEASURED (npm run p33:scale-roc). Problems 1 and 3 of the roadmap ask for the blind false-positive rate and the completeness curve to be measured at SCALE on real data and published as a reproducible artifact, not as an anecdote on a handful of stars. p33 runs the iterative detector under the production gate (iterativeBls, maxSignals=3, gate SNR>=7 & depth>=200 ppm & depth-regime screen) over a seeded sample of real host stars in two arms. NULL arm (real curve as-is): every gated signal is classified against the CONFIRMED catalog -- over 33 usable public TESS-SPOC curves (7 coverage misses of 40 sampled, counted honestly) it fires 29 gated detections, 12 true-positive / 17 false-positive, a blind false-positive rate of 51.5% and a purity of 41.4%. Because the null arm only matches CONFIRMED planets, a genuine but still-unconfirmed planet/TOI counts as a false positive, so 41.4% is a LOWER bound on true purity -- but it is the first scale measurement of the blind FP rate on real curves (Problem 1 quantified, not asserted). INJECT arm (one transit at P=3 d injected multiplicatively across a depth grid on the SAME real noise): completeness rises 42.4% @300 ppm -> 54.5% @500 -> 69.7% @1000 -> 78.8% @2000 -> 81.8% @4000, then PLATEAUS at ~82% (an honest ~18% of real curves never recover even a deep injection, where the host's own signal or the sector-gap window function dominates). Per-curve wall-clock is metered so the cost of going bigger is MEASURED: mean 27.86 s per curve (null + 6 injections) => ~3.98 s per single-star detection => ~1.11 CPU-h per 1000 stars (detection only, excl. download). Public reproducible artifacts: reports/p33-scale-roc.md (metrics) and reports/p33-catalog.json (per-star recovery rows). Measure-only; nothing gated, anchored, claimed, or deployed. Reproduce with npm run p33:scale-roc -- --stars 40 --maxsectors 2 --period 3 --seed 7.

Compute / cost model for scaling, MEASURED (npm run p34:cost-model). p33 meters per-curve detector wall-clock; p34 turns those 33 real measurements into a reproducible scaling model (re-fit from reports/p33-catalog.json each run, nothing hardcoded). Detector cost is a power law in the point count, perCurveMs ~ k * nPoints^p with p = 2.94 (near-cubic; R^2 = 0.84 on N = 33) -- BLS over a fine period grid is superlinear and the iterative mask-and-refind re-runs it per signal. The consequence for Track C is exact: the lever that raises completeness (deeper baselines => more sectors => more points) is priced ~nPoints^2.94, so doubling the baseline costs ~7.7x. Detection-only CPU-hours (download/parse excluded and flagged separately): a 20000-star, 2-sector public catalog is ~22 CPU-h, and ~169 CPU-h at 4 sectors; detection is embarrassingly parallel across stars, so on a 64-way CI matrix 20000x2 is ~0.3 h and 20000x4 is ~2.6 h wall-clock. Dollar cost maps directly from any provider's vCPU-h price (the report quotes CPU-h and invents no price). Artifacts: reports/p34-cost-model.md + reports/p34-cost-model.json. Measure-only; nothing gated, anchored, claimed, or deployed. Reproduce with npm run p34:cost-model -- --sectors 1,2,4 --stars 1000,5000,20000.

Independent algorithmic cross-check, MEASURED (npm run p35:pdm-crosscheck). Problem 3 asks for independent validation; the cheapest real independence is a DIFFERENT ALGORITHM on the SAME data. src/detect/pdm.ts implements Phase Dispersion Minimization (Stellingwerf 1978): a non-parametric period finder that shares no machinery with the box-fit family (it never fits a box -- it minimizes the within-bin variance theta of the phase fold). p35 runs both estimators over the same seeded sample as p33. NULL cross-check: of 33 real curves, 18 had BOTH a gated BLS period and a finite PDM minimum; PDM's global theta minimum lands on the same period (or a low-order harmonic, tol 2%) for 9/18 (50.0%) -- corroboration where they agree, and where they differ PDM usually locks the dominant stellar rotation/activity period instead, exactly as expected of a general period finder on a real variable star. INDEPENDENT completeness (PDM alone recovers a P=3 d injection on the same real noise, same depth grid): 24.2% @300 ppm -> 36.4% @500 -> 51.5% @1000 -> 60.6% @2000 -> 75.8% @4000 -> 78.8% @8000, i.e. uniformly BELOW the detrended box-fit detector's 42.4/54.5/69.7/78.8/81.8/81.8% on the same 33 stars. That gap is the honest finding, not a defect: it quantifies why transit pipelines use a detrended box-fit search rather than raw PDM, and agreement cases like KELT-9 (PDM 1.48 d, theta 0.12 vs gated BLS 1.48 d) show the two independent estimators converge on strong real signals. Artifacts: reports/p35-pdm-crosscheck.md + reports/p35-pdm-crosscheck.json. Measure-only; nothing gated, anchored, claimed, or deployed. Reproduce with npm run p35:pdm-crosscheck -- --stars 40 --maxsectors 2 --seed 7.

Cross-pipeline validation harness, INDEPENDENT PYTHON STACK (npm run p36:export-fixtures + .github/workflows/crosscheck-python.yml). p35 added independence at the algorithm level; this adds it at the IMPLEMENTATION level: the same real photometry re-analyzed by a completely different software stack that shares no code with this repository -- batman transit injection (Kreidberg 2015), wotan biweight detrending (Hippke et al. 2019), astropy.timeseries.BoxLeastSquares (the Kovacs et al. 2002 reference implementation), and optional transitleastsquares (Hippke & Heller 2019). Because the dev environment has no Python interpreter, the harness is split deliberately: p36:export-fixtures downloads real public TESS-SPOC curves for the same seeded host sample as p33/p35 (first 16 catalog hosts, seed 7 -> 12 usable curves, 4 coverage misses), deterministically subsamples to <=2500 points (uniform stride, recorded in the manifest) and commits them as byte-identical CSV fixtures under tests/fixtures/spoc-curves/ (host, TIC, sectors, known CONFIRMED planet periods). The CI workflow crosscheck-python.yml (pinned Python 3.11 + pinned package versions; triggered by workflow_dispatch and by pushes touching the harness/fixtures) then runs scripts/cross_pipeline.py: NULL arm (wotan + astropy BLS on each real curve, alias-tolerant TP/FP against the manifest's known planets, same lower-bound purity semantics as p33) and INJECT arm (batman P=3 d transit across the depth grid [300..8000] ppm -> wotan -> astropy BLS recovery), uploading reports/crosscheck/ as a workflow artifact and publishing it back into the repository at the same path (artifact/log download requires GitHub auth; committed files are publicly readable -- the publish commit cannot re-trigger the workflow because its paths filter excludes reports/, so there is no loop). Every method is independently guarded (a missing/failed package degrades that cell to MISSING/error, the rest still runs). Honest boundary: the cross-pipeline numbers are produced in CI (measure-before-gate: they are read from the published report, never asserted here); fixtures are subsampled, so completeness is not directly comparable to p33's full-resolution numbers and differences in detrending (wotan here vs raw stitched curve in the p33 inject arm) are flagged in the report itself. Research tooling; nothing gated, anchored, claimed, or deployed. Reproduce: npm run p36:export-fixtures -- --stars 16 --maxsectors 2 --cap 2500, commit, then dispatch the cross-pipeline-python workflow.

Current rankings

  • 16

    Underappreciated Gems

    100 stars · 6 fork · Total stars (ascending)

    Underrated →

Momentum, past 30 days

Cumulative stars

Rank movement

← Back to boards