#!/usr/bin/env python3
"""Generate the claim tables on docs/optimized-debate/index.html from one data set.

The book's "An optimized approach to the shape debate" (pp. 208-231; also the standalone
PDF at shapedebate.com/social-proof) sets three Team A claims against 103 numbered Team B
claims. This script holds every item once -- number, a short paraphrase, the group it is
sorted into here, the page on this site that answers it (if one does), the verdict that
page reached, one line on what a reader can check, and a reference with enough detail to
act on -- and writes the tables between the <!--MAP:...--> markers on the page. Edit the
data here, re-run, and the page and the tests (tests/test_optimized_map.py) follow.

    python3 build_optimized_map.py           -> rewrites the page tables in place
    python3 build_optimized_map.py --check   -> prints the group counts and exits
"""
import os
import re
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PAGE = os.path.join(ROOT, "docs", "optimized-debate", "index.html")
SITE = "https://funwithscience.net/globe-deconstruction/"

# Groups, in page order. key -> (heading, one-line description)
GROUPS = {
    "earth": ("Earth: shape, curvature and gravity",
              "The items that are about the thing the debate is named for."),
    "helio": ("Heliocentrism: the Sun, the Moon, the orbits and the spin",
              "Items about the arrangement of the solar system. Most are checkable with a clock, a small telescope or a tide table."),
    "bodies": ("Other bodies: planets, asteroids and Pluto",
               "Planetary science. None of it bears on the shape of the Earth, and most of it needs a spacecraft or a large telescope to check."),
    "cosmos": ("The galaxy and the cosmos",
               "Galactic dynamics, dark matter, dark energy, black holes. Real science, all of it outside anything a shape debate can settle, and outside anything this review assesses."),
    "space": ("The space programme: Apollo, the ISS, the probes",
              "The class the landing page declares untouched: whether space-agency material is authentic turns on the good faith of named people, not on a measurement. Three items are the exception, because the station itself can be watched from a garden."),
}

# Chip classes match the landing page: refuted / standard / mislead / notdemo / unassessed
PAGES = {
    "bottom-up-observations": ("Bottom Up Observations", "standard", "Standard physics"),
    "flat-surface-test": ("Flat-Surface Test", "standard", "Standard physics"),
    "long-path-cases": ("Chicago &amp; Pontchartrain", "refuted", "Refuted"),
    "rampion": ("The Black Swan (Rampion)", "refuted", "Refuted"),
    "balloon-curve": ("The Curve at 118,000 Feet", "refuted", "Refuted"),
    "rockets-in-vacuum": ("Rockets Don't Push Against Air", "refuted", "Refuted"),
    "thrust-in-flight": ("Thrust, Measured in Flight", "refuted", "Refuted"),
    "action-lab-footage": ("Action Lab Footage", "refuted", "Refuted"),
    "full-moon-lighting": ("Full-Moon Lighting", "standard", "Standard physics"),
    "moon-tilt": ("The Moon-Tilt Illusion", "refuted", "Refuted"),
    "jupiter-shadows": ("Jupiter's Shadows", "refuted", "Refuted"),
    "eclipse-trajectory": ("The Eclipse Trajectory", "refuted", "Refuted"),
    "selenelion": ("The Selenelion", "standard", "Standard physics"),
    "luminaries": ("Where's the Moon's Silhouette?", "standard", "Standard physics"),
    "celestial-globes": ("Celestial Globes Exposed", "refuted", "Refuted"),
    "sunlight-and-shadows": ("Sunlight &amp; Shadows", "refuted", "Refuted"),
    "sun-does-not-shrink": ("The Sun Does Not Shrink", "refuted", "Refuted"),
    "planetary-imaging": ("Stacked &amp; Sharpened", "notdemo", "Not demonstrated"),
    "moon-sphericity": ("Is the Moon a Ball?", None, None),
    "eight-planets": ("The Eight-Planet Photograph", "notdemo", "Not demonstrated"),
    "hydrostatics": ("Hydrostatics", "refuted", "Refuted"),
    "sky-rate-and-shadows": ("The Sky Turns at One Rate", "standard", "Standard physics"),
    "earth-rotation": ("Does the Earth Spin?", "refuted", "Refuted"),
    "coriolis-drifters": ("Coriolis Drifters", None, None),
    "extraordinary-evidence": ("Extraordinary Evidence", "none", "Draft"),
    "self-test-protocol": ("Self-Test Protocol", None, None),
}

# Team A. (label, claim paraphrase, pages, verdict chip text, verdict class, note)
TEAM_A = [
    ("Claim #1", "Objects can disappear completely across a flat surface with nothing in the way; long-distance visual limits are under-studied.",
     ["bottom-up-observations", "flat-surface-test", "long-path-cases", "rampion"], "Standard physics", "standard",
     "Granted as stated: over 540 ft a globe hides nothing and something did vanish, by perspective and resolution. The book&rsquo;s own long-path photographs are the cases where those limits are exceeded, and each is measured on its page."),
    ("Claim #2", "Gas propulsion needs something to push off; in a vacuum there is nothing, so mainstream physics is wrong about rockets.",
     ["rockets-in-vacuum", "thrust-in-flight", "action-lab-footage", "balloon-curve"], "Refuted", "refuted",
     "Engines have been measured firing in vacuum several unrelated ways; the Action Lab footage he cites is read frame by frame and does not show what the chapter says it shows; and the sounding-rocket video he links at p.&nbsp;237 burns its second stage from 4&nbsp;km to 43&nbsp;km, through air thinning to a five-hundredth of sea level, to the speed its 253-km arc needs."),
    ("Claim #3", "Four celestial red flags: uniform full-moon lighting, the moon-tilt illusion, Jupiter&rsquo;s moon shadows, the eclipse trajectory. Together they say the Sun is not the illuminator.",
     ["full-moon-lighting", "moon-tilt", "jupiter-shadows", "eclipse-trajectory", "selenelion", "luminaries", "self-test-protocol"], "Refuted", "refuted",
     "Run as a cumulative case, and all four limbs are answered: two are real observations the standard model already predicts, two are measured and found not to be there."),
    ("The Rampion observation", "The Black Swan video: turbine bases and ships visible at 8&ndash;21 miles from Worthing Beach, presented as impossible on a globe.",
     ["rampion"], "Refuted", "refuted",
     "The video&rsquo;s own six-turbine slide, measured, records <em>k</em>&nbsp;&asymp;&nbsp;0.7 with about 2&nbsp;m of the near base and 5&nbsp;m of the far one below the sea; the bases do not meet the water, and a flat plane would need air past its overturning limit."),
    ("The debate card, p. 232", "Is extraordinary evidence provided that objects cannot disappear bottom-up, that gas-propelled vehicles work in a vacuum, and that Jupiter&rsquo;s black circles are shadows?",
     ["bottom-up-observations", "flat-surface-test", "rockets-in-vacuum", "thrust-in-flight", "jupiter-shadows"], "Answered", "standard",
     "Three questions, three pages. The first is granted as standard physics &mdash; objects can vanish bottom-up over flat water, and his own pond run shows it; the second and third are measured: thrust in flight where the pressure is zero, and shadow-transit timings met from back gardens."),
    ("The Antarctica 360&deg; Sun", "If the midnight-sun footage from the December 2024 expedition is shown fake, &ldquo;the game will be over.&rdquo;",
     [], "Not assessed", "none",
     "Whether that footage is authentic is one of the two things the landing page declares this review does not touch: it turns on the good faith of named people, not on a measurement a reader can repeat."),
]

# Team B. (n, paraphrase, group, pages, check, ref)
# check: one line on what evidence exists and whether a reader can test it. ref: a source with detail, or None.
B = [
    (1, "Team A is wrong about objects disappearing on flat surfaces and about gas propulsion in a vacuum; &ldquo;we are still waiting for Team B to conduct these experiments properly.&rdquo;", "earth",
     ["flat-surface-test", "long-path-cases", "rockets-in-vacuum", "thrust-in-flight"],
     "The two experiments have been done, on this site and elsewhere: his own 540-ft pond run is granted and rescaled, the long-path cases are measured with the day&rsquo;s weather attached, and vacuum thrust is measured in flight.",
     "<a href='https://funwithscience.net/globe-deconstruction/self-test-protocol/'>Self-Test Protocol</a> on this site gives the rescaled pond run and a vacuum-thrust run a reader can perform."),
    (3, "Earth&rsquo;s curvature hides 32.7 ft at 10 miles and 1,473 ft at 50 miles for a 6-ft observer.", "earth",
     ["long-path-cases", "bottom-up-observations"],
     "Both figures are correct for an airless globe (re-derived here: 32.7 and 1,473 ft). They are the starting point, not the prediction; the prediction is geometry plus the day&rsquo;s refraction, which the Reverse Refraction Solver prices on both surfaces.",
     "<a href='https://msi.nga.mil/Publications/APN'>Bowditch, <em>The American Practical Navigator</em></a> (NGA), Table 12 &ldquo;Distance of the Horizon&rdquo; and Table 13 &ldquo;Geographic Range&rdquo;, which fold standard refraction into exactly these numbers; and the <a href='https://funwithscience.net/refraction-solver/'>Reverse Refraction Solver</a>."),
    (12, "Planets are spheres with specific radii.", "earth", ["planetary-imaging", "moon-sphericity"],
     "Checkable with a small telescope: Jupiter&rsquo;s disc is visibly flattened (about 1 part in 15), Saturn&rsquo;s more so, and the phases of Venus run through a full crescent-to-gibbous cycle that only a sphere lit from one side produces.",
     "Jupiter&rsquo;s equatorial and polar radii 71,492 and 66,854 km, <a href='https://nssdc.gsfc.nasa.gov/planetary/factsheet/jupiterfact.html'>NASA planetary fact sheet</a>; Venus phase cycle in any planetarium program, and Galileo, <em>Letters on Sunspots</em> (1613) for the original observation."),
    (13, "The Sun is a sphere and its rays arrive parallel.", "earth", ["sunlight-and-shadows", "sun-does-not-shrink"],
     "Measured on this site: the divergence of shadows across a long baseline is 7.5 arcseconds against a signal of 25,596 for a nearby Sun, and the Sun&rsquo;s angular size does not change across the day.",
     "<a href='https://funwithscience.net/globe-deconstruction/sunlight-and-shadows/'>Sunlight and Shadows</a> on this site, with the shadow measurement and its working."),
    (14, "The Moon&rsquo;s light is the Sun&rsquo;s, so a full Moon should not be uniformly lit.", "earth", ["full-moon-lighting", "moon-sphericity"],
     "The uniform full Moon is the opposition surge of a rough, retro-reflecting surface, and it is reproducible at a desk with a lamp and a ball of the right roughness; the page gives the setup.",
     "<a href='https://doi.org/10.1126/science.260.5107.509'>Hapke, Nelson &amp; Smythe, &ldquo;The opposition effect of the Moon: the contribution of coherent backscatter,&rdquo; <em>Science</em> 260 (1993) 509&ndash;511</a>; the desk demonstration in <a href='https://funwithscience.net/globe-deconstruction/self-test-protocol/'>Self-Test Protocol</a>."),
    (45, "Gravity is what keeps the atmosphere from flying off into space.", "earth", ["hydrostatics"],
     "The book&rsquo;s own Q2 asks this; the page answers it with the barometric formula against measured pressure, and a reader can check the 12&nbsp;hPa fall per 100&nbsp;m with a phone barometer and a hill.",
     "<a href='https://ntrs.nasa.gov/citations/19770009539'>U.S. Standard Atmosphere 1976</a> (NOAA/NASA/USAF), Table 1, pressure against geometric altitude."),
    (46, "The atmosphere is in hydrostatic equilibrium: pressure gradient against gravity.", "earth", ["hydrostatics"],
     "Same page. The equilibrium is not an assumption but a measurement: any radiosonde profile shows dp/dz&nbsp;=&nbsp;&minus;&rho;g to within instrument error all the way up.",
     "<a href='https://weather.uwyo.edu/upperair/sounding.shtml'>University of Wyoming upper-air archive</a>: pick any station and date; the pressure&ndash;height columns are the test."),
    (48, "Gravity maintains the tropopause as a lid that keeps water vapour low.", "earth", ["hydrostatics"],
     "Partly right as stated: the tropopause is a temperature structure (a lapse-rate minimum) that gravity and radiation set together, not a lid gravity holds down. Checkable from the same soundings, where the temperature turns over near 10&ndash;16&nbsp;km.",
     "<a href='https://glossary.ametsoc.org/wiki/Tropopause'>WMO definition of the tropopause</a> (lapse rate falling below 2&nbsp;K/km); any <a href='https://weather.uwyo.edu/upperair/sounding.shtml'>Wyoming sounding</a> shows it."),
    (2, "Planet rotation speeds at the equator and distances from the Sun, tabulated.", "helio", ["eight-planets", "sky-rate-and-shadows"],
     "The rotations are checkable: Jupiter&rsquo;s Great Red Spot transits every 9&nbsp;h&nbsp;56&nbsp;min in a 100-mm telescope; the Sun&rsquo;s spots cross the disc in about two weeks. The distances follow from Kepler&rsquo;s third law once one baseline is measured (radar to Venus, since 1961).",
     "<a href='https://alpo-astronomy.org/'>ALPO</a> Jupiter Section transit timings; Kepler&rsquo;s third law with the astronomical unit from Venus radar ranging, <a href='https://doi.org/10.1086/108900'>Muhleman, &ldquo;Radar results as constraints on the models of Venus,&rdquo; <em>Astronomical Journal</em> 67 (1962) 277</a>."),
    (10, "The Sun holds 99.86 % of the solar system&rsquo;s mass and its gravity binds the planets.", "helio", [],
     "The mass ratio is a consequence of the orbital periods and distances (Kepler&rsquo;s third law gives the Sun&rsquo;s mass from any planet&rsquo;s orbit); Jupiter&rsquo;s moons give Jupiter&rsquo;s mass the same way, in a backyard telescope, over a fortnight.",
     "Timing Io, Europa, Ganymede and Callisto with a small telescope reproduces GM for Jupiter to a few per cent: see the &ldquo;Jupiter&rsquo;s moons&rdquo; lab in Hoff, Kelsey &amp; Neff, <em>Activities in Astronomy</em>, or the <a href='https://public.gettysburg.edu/~marschal/clea/CLEAhome.html'>CLEA &ldquo;Revolution of the Moons of Jupiter&rdquo; exercise</a>."),
    (11, "Solar eclipses are the Moon&rsquo;s shadow; lunar eclipses are the Earth&rsquo;s.", "helio", ["selenelion", "eclipse-trajectory", "luminaries"],
     "Checkable at the next lunar eclipse: the edge of the shadow on the Moon is always an arc of a circle about 2.7 times the Moon&rsquo;s diameter, whatever the Moon&rsquo;s altitude, which only a sphere casts. The eclipse pages on this site measure the geometry of two recent events.",
     "<a href='https://eclipse.gsfc.nasa.gov/LEcat5/LEcatalog.html'>Espenak &amp; Meeus, <em>Five Millennium Canon of Lunar Eclipses</em></a> (NASA TP-2009-214172), umbral radius and the geometry behind it."),
    (15, "Stars are made of the same stuff as the Sun, just far away.", "helio", [],
     "Checkable with a diffraction grating on a small telescope: the hydrogen Balmer lines and the sodium and calcium lines that Fraunhofer mapped in sunlight appear in the spectra of bright stars at the same wavelengths.",
     "<a href='https://www.patonhawksley.com/'>Star Analyser 100 grating</a> (Paton Hawksley) on any DSLR or small scope; Kaler, <em>Stars and Their Spectra</em> (CUP, 2nd ed. 2011), ch. 3&ndash;4 for the lines to look for."),
    (16, "Jupiter shields the inner planets from comets and asteroids.", "helio", [],
     "Contested in the literature itself: the simulations show Jupiter removes some impactors and delivers others, and the net effect depends on the population assumed. Not checkable from the ground, and not a shape claim.",
     "<a href='https://doi.org/10.1017/S1473550408004187'>Horner &amp; Jones, &ldquo;Jupiter &ndash; friend or foe? I,&rdquo; <em>International Journal of Astrobiology</em> 7 (2008) 251</a>, and parts II&ndash;IV through 2012."),
    (18, "Gravity weakens with distance; the difference across a body makes tides. Io is volcanic for that reason.", "helio", [],
     "The Earth half is checkable with a tide table and a calendar: spring tides fall at new and full Moon, neap tides at the quarters, month after month. Io&rsquo;s volcanism is spacecraft evidence (Voyager 1, 1979).",
     "<a href='https://tidesandcurrents.noaa.gov/'>NOAA Tides &amp; Currents</a> or <a href='https://easytide.admiralty.co.uk/'>UKHO EasyTide</a> for any port, read against the lunar phase; <a href='https://doi.org/10.1126/science.204.4396.972'>Morabito et al., &ldquo;Discovery of currently active extraterrestrial volcanism,&rdquo; <em>Science</em> 204 (1979) 972</a>."),
    (19, "Gravity propagates at the speed of light.", "helio", [],
     "Instrument-only evidence, and strong: the gravitational wave GW170817 and the gamma-ray burst from the same merger arrived 1.7&nbsp;s apart after 130 million years in transit.",
     "<a href='https://doi.org/10.3847/2041-8213/aa920c'>Abbott et al., &ldquo;Gravitational waves and gamma-rays from a binary neutron star merger,&rdquo; <em>ApJ Letters</em> 848 (2017) L13</a>."),
    (20, "Lagrange points exist and are used to park the James Webb telescope.", "helio", [],
     "Not a cancellation: the two bodies&rsquo; gravity together supplies exactly the centripetal acceleration a small object needs to orbit with their period, so it keeps station with them; &ldquo;balances the centrifugal force&rdquo; is the same fact stated in the rotating frame. JWST at L2 has been imaged by amateurs at about magnitude 17 at the position JPL Horizons predicts.",
     "<a href='https://ssd.jpl.nasa.gov/horizons/'>JPL Horizons</a> ephemeris for &ldquo;JWST&rdquo; (body id &minus;170); Szebehely, <em>Theory of Orbits</em> (1967) for the points themselves."),
    (30, "The Moon is tidally locked: one rotation per orbit, so we see one face.", "helio", ["moon-sphericity"],
     "Checkable by eye over a month: the same maria face us throughout, and the slow rocking at the limb (libration) exposes 59 % of the surface over time, which a locked sphere on an eccentric, inclined orbit predicts to the degree.",
     "Meeus, <em>Astronomical Algorithms</em> (2nd ed. 1998), ch. 53, optical libration, or the daily libration figures on <a href='https://svs.gsfc.nasa.gov/Gallery/moonphase.html'>NASA SVS Moon Phase and Libration</a>; compare the predicted libration in longitude and latitude with the position of Mare Crisium near the limb on successive nights."),
    (31, "Both sides of the Moon get sunlight; the far side has day and night too.", "helio", ["moon-sphericity", "full-moon-lighting"],
     "The near side is seen to run through a full day&ndash;night cycle every lunation; the far side&rsquo;s cycle is inferred from the same geometry and confirmed only by spacecraft imagery.",
     "LRO Wide Angle Camera global mosaic, <a href='https://www.lpi.usra.edu/meetings/lpsc2011/pdf/2387.pdf'>Speyerer et al., &ldquo;Lunar Reconnaissance Orbiter Camera global morphological map of the Moon,&rdquo; LPSC 42 (2011) 2387</a>."),
    (32, "The Moon has moonquakes from tidal stress and cooling.", "helio", [],
     "Instrument-only: the Apollo seismometers ran from 1969 to 1977. Not a shape claim.",
     "<a href='https://doi.org/10.1029/JB087iS01p0A117'>Nakamura, Latham &amp; Dorman, &ldquo;Apollo lunar seismic experiment &ndash; final summary,&rdquo; <em>JGR</em> 87 (1982) A117</a>."),
    (33, "Lunar surface temperatures swing from 127&nbsp;&deg;C to &minus;173&nbsp;&deg;C.", "helio", [],
     "Instrument-only: the numbers are the Diviner radiometer&rsquo;s. Not a shape claim.",
     "<a href='https://doi.org/10.1016/j.icarus.2016.08.012'>Williams et al., &ldquo;The global surface temperatures of the Moon as measured by the Diviner Lunar Radiometer Experiment,&rdquo; <em>Icarus</em> 283 (2017) 300</a>."),
    (34, "The Moon&rsquo;s gravity raises ocean tides and flexes the crust by centimetres.", "helio", [],
     "The ocean half is the same tide-table check as #18. The solid-Earth tide is measured by GNSS and gravimeters, not by eye.",
     "<a href='https://doi.org/10.1016/B978-0-444-53802-4.00058-0'>Agnew, &ldquo;Earth tides,&rdquo; in <em>Treatise on Geophysics</em> vol. 3 (Elsevier, 2nd ed. 2015)</a>."),
    (35, "The Moon stabilises Earth&rsquo;s axial tilt at about 23.5&deg;.", "helio", [],
     "A model result, not an observation; the chaotic-obliquity paper it comes from is the reference. Not checkable, not a shape claim.",
     "<a href='https://doi.org/10.1038/361615a0'>Laskar, Joutel &amp; Robutel, &ldquo;Stabilization of the Earth&rsquo;s obliquity by the Moon,&rdquo; <em>Nature</em> 361 (1993) 615</a>."),
    (37, "The Sun is an average G-type main-sequence star, about 4.6 billion years old.", "helio", [],
     "The classification is spectroscopy (see #15); the age comes from meteorite radiometric dating. Not a shape claim.",
     "<a href='https://doi.org/10.1038/ngeo941'>Bouvier &amp; Wadhwa, &ldquo;The age of the Solar System redefined by the oldest Pb&ndash;Pb age of a meteoritic inclusion,&rdquo; <em>Nature Geoscience</em> 3 (2010) 637</a>."),
    (38, "The corona is far hotter than the photosphere.", "helio", [],
     "Instrument evidence since 1939 (coronal emission lines identified as highly ionised iron); a total eclipse shows the corona to the eye but not its temperature. Not a shape claim.",
     "<a href='https://ui.adsabs.harvard.edu/abs/1943ZA.....22...30E'>Edl&eacute;n, &ldquo;Die Deutung der Emissionslinien im Spektrum der Sonnenkorona,&rdquo; <em>Zeitschrift f&uuml;r Astrophysik</em> 22 (1943) 30</a>."),
    (39, "The Sun is powered by fusion in its core.", "helio", [],
     "Instrument evidence: solar neutrinos detected at the rate fusion predicts, once neutrino oscillation is included. Not a shape claim.",
     "<a href='https://doi.org/10.1103/PhysRevLett.89.011301'>SNO Collaboration, &ldquo;Direct evidence for neutrino flavor transformation from neutral-current interactions,&rdquo; <em>PRL</em> 89 (2002) 011301</a>."),
    (41, "Light takes 8 min 20 s from the Sun.", "helio", [],
     "Checkable over a season with a small telescope: time the eclipses of Io by Jupiter for several months and the timings drift by up to about 16&frac12; minutes between the near and far sides of Earth&rsquo;s orbit, which is the light-time across two astronomical units. Half of it is the 499&nbsp;s claimed.",
     "R&oslash;mer&rsquo;s 1676 method; predicted timings for any date are in <a href='https://skyandtelescope.org/observing/interactive-sky-watching-tools/jupiters-moons-javascript-utility/'>Sky &amp; Telescope&rsquo;s Jupiter&rsquo;s Moons tool</a> and the <em>Astronomical Almanac</em> section on Jupiter&rsquo;s satellites."),
    (42, "Photons take thousands of years to escape the Sun&rsquo;s interior.", "helio", [],
     "A model result from radiative transfer; the commonly quoted range is 10,000 to 170,000 years depending on the assumptions. Not checkable, not a shape claim.",
     "<a href='https://doi.org/10.1086/172103'>Mitalas &amp; Sills, &ldquo;On the photon diffusion time scale for the Sun,&rdquo; <em>ApJ</em> 401 (1992) 759</a>."),
    (43, "The Sun rotates differentially: about 25 days at the equator, 36 at the poles.", "helio", [],
     "Checkable by projection: sketch sunspots daily for two weeks; spots near the equator cross in about 13 days, spots at high latitude take longer. This was Carrington&rsquo;s measurement.",
     "Carrington, <em>Observations of the Spots on the Sun</em> (1863); the <a href='https://solar-center.stanford.edu/'>Stanford Solar Center</a> &ldquo;Sunspot rotation&rdquo; activity gives a worked modern version with SOHO/SDO images."),
    (44, "The solar wind and the heliosphere.", "helio", [],
     "Instrument evidence (Mariner 2 onward); the aurora is the ground-visible consequence. Not a shape claim.",
     "<a href='https://doi.org/10.1126/science.138.3545.1095-a'>Neugebauer &amp; Snyder, &ldquo;Solar plasma experiment,&rdquo; <em>Science</em> 138 (1962) 1095</a>."),
    (17, "Pluto&rsquo;s Sputnik Planitia is convecting nitrogen ice.", "bodies", [], "New Horizons imagery and modelling. Not checkable from the ground, not a shape claim.",
     "<a href='https://doi.org/10.1038/nature18289'>McKinnon et al., &ldquo;Convection in a volatile nitrogen-ice-rich layer drives Pluto&rsquo;s geological vigour,&rdquo; <em>Nature</em> 534 (2016) 82</a>."),
    (49, "Asteroids are leftovers from planet formation.", "bodies", [], "Meteorite chemistry and sample return. Not a shape claim.", None),
    (50, "The asteroid belt is mostly empty space.", "bodies", [], "Spacecraft have crossed it without incident since Pioneer 10 (1972). Not a shape claim.", None),
    (51, "A 710-m asteroid spinning every 1.88 minutes must be solid rock.", "bodies", [], "A single-object result reported in 2026; the spin-barrier argument is standard. Not checkable from the ground, not a shape claim.", None),
    (52, "Bennu is a loosely packed rubble pile.", "bodies", [], "OSIRIS-REx touchdown data. Not a shape claim.",
     "<a href='https://doi.org/10.1126/science.abm1018'>Lauretta et al., &ldquo;Spacecraft sample collection and subsurface excavation of asteroid (101955) Bennu,&rdquo; <em>Science</em> 377 (2022) 285</a>."),
    (53, "Some asteroids have moons and rings.", "bodies", [], "Ida&ndash;Dactyl from Galileo (1993); Chariklo&rsquo;s rings from a stellar occultation, which amateurs contributed timings to. Not a shape claim.",
     "<a href='https://doi.org/10.1038/nature13155'>Braga-Ribas et al., &ldquo;A ring system detected around the Centaur (10199) Chariklo,&rdquo; <em>Nature</em> 508 (2014) 72</a>."),
    (54, "Ceres is a dwarf planet and a third of the belt&rsquo;s mass.", "bodies", [], "Dawn mission. Ceres itself is a binocular object at opposition. Not a shape claim.", None),
    (55, "Asteroids are rich in metals; Psyche will investigate.", "bodies", [], "Spectroscopic inference plus a mission in flight. Not a shape claim.", None),
    (56, "Carbon-rich asteroids delivered much of Earth&rsquo;s water.", "bodies", [], "Isotope ratios from Ryugu and Bennu samples. An active research question, not a settled fact. Not a shape claim.", None),
    (57, "DART moved an asteroid; Hera will inspect the result in 2026.", "bodies", [], "The orbital-period change of Dimorphos (33 minutes) was measured from the ground by timing mutual eclipses, including by amateurs. Not a shape claim.",
     "<a href='https://doi.org/10.1038/s41586-023-05805-2'>Thomas et al., &ldquo;Orbital period change of Dimorphos due to the DART kinetic impact,&rdquo; <em>Nature</em> 616 (2023) 448</a>."),
    (75, "Olympus Mons is the largest volcano in the solar system.", "bodies", [], "Orbital imagery and altimetry. Not a shape claim.", None),
    (76, "Valles Marineris is a 2,500-mile canyon.", "bodies", [], "Orbital imagery. Not a shape claim.", None),
    (78, "Martian soil is rich in perchlorates.", "bodies", [], "Phoenix lander wet chemistry (2008). Not a shape claim.", None),
    (79, "Mars has dust devils and global dust storms.", "bodies", [], "Global storms are visible in amateur telescopes: the 2018 storm hid the surface markings for weeks. Not a shape claim.", None),
    (80, "Mars has 38 % of Earth&rsquo;s surface gravity.", "bodies", [], "Follows from its mass (from its moons&rsquo; orbits) and radius. Not a shape claim.", None),
    (4, "The solar system orbits the galactic centre at about 515,000 mph.", "cosmos", [], "Radio astrometry of the galactic centre. Not a shape claim.",
     "<a href='https://doi.org/10.1086/424960'>Reid &amp; Brunthaler, &ldquo;The proper motion of Sagittarius A*. II,&rdquo; <em>ApJ</em> 616 (2004) 872</a>."),
    (5, "A galactic year is 225&ndash;250 million years.", "cosmos", [], "Follows from #4 and the distance to the centre (#22). Not a shape claim.", None),
    (6, "The Sun bobs through the galactic plane.", "cosmos", [], "Stellar dynamics. Not a shape claim.", None),
    (7, "The ecliptic is tilted about 60&deg; to the galactic plane.", "cosmos", [], "Checkable by eye on a dark night: the Milky Way crosses the zodiac at a steep angle. Not a shape claim.", None),
    (8, "The Sun orbits the solar system&rsquo;s barycentre.", "cosmos", [], "A consequence of Newtonian gravity with Jupiter&rsquo;s mass; seen directly in pulsar timing. Not a shape claim.", None),
    (9, "The Milky Way moves toward the Great Attractor and will merge with Andromeda.", "cosmos", [], "Proper-motion measurements of M31 with Hubble. Not a shape claim.",
     "<a href='https://doi.org/10.1088/0004-637X/753/1/9'>van der Marel et al., &ldquo;The M31 velocity vector. III. Future Milky Way&ndash;M31&ndash;M33 orbital evolution, merging, and fate of the Sun,&rdquo; <em>ApJ</em> 753 (2012) 9</a>."),
    (21, "The Milky Way is a barred spiral.", "cosmos", [], "Infrared star counts. Not a shape claim.", None),
    (22, "We are 26,000 light-years from the centre, in the Orion Spur.", "cosmos", [], "Radio parallax of masers. Not a shape claim.",
     "<a href='https://doi.org/10.3847/1538-4357/ab4a11'>Reid et al., &ldquo;Trigonometric parallaxes of high-mass star-forming regions,&rdquo; <em>ApJ</em> 885 (2019) 131</a>."),
    (23, "Sagittarius A* is a four-million-solar-mass black hole.", "cosmos", [], "Stellar orbits at the galactic centre, 2020 Nobel Prize. Not a shape claim.",
     "<a href='https://doi.org/10.1051/0004-6361/202037813'>GRAVITY Collaboration, &ldquo;Detection of the Schwarzschild precession in the orbit of the star S2,&rdquo; <em>A&amp;A</em> 636 (2020) L5</a>."),
    (24, "The universe is 13.8 billion years old, the Milky Way 13.6.", "cosmos", [], "Cosmic microwave background fits and stellar ages. Not a shape claim.", None),
    (25, "The Milky Way grew by consuming dwarf galaxies.", "cosmos", [], "Gaia stellar streams. Not a shape claim.", None),
    (26, "Ninety per cent of the galaxy&rsquo;s mass is dark matter.", "cosmos", [], "Rotation curves. An inference about missing mass, and the subject of live debate about its nature. Not a shape claim.", None),
    (27, "The galactic disc is warped.", "cosmos", [], "Gaia and HI surveys. Not a shape claim.", None),
    (28, "100&ndash;400 billion stars, most with planets.", "cosmos", [], "Star counts and Kepler statistics. Not a shape claim.", None),
    (29, "The Milky Way moves at 1.3 million mph relative to the microwave background.", "cosmos", [], "The CMB dipole, measured by COBE, WMAP and Planck. Not a shape claim.",
     "<a href='https://doi.org/10.1051/0004-6361/201833880'>Planck Collaboration, &ldquo;Planck 2018 results. I,&rdquo; <em>A&amp;A</em> 641 (2020) A1</a>, &sect;3 on the dipole."),
    (47, "Dark energy&rsquo;s fate for the universe: heat death, Big Rip or Big Crunch.", "cosmos", [], "Explicitly conditional in the claim itself. Not a shape claim.", None),
    (60, "Dark matter is 27 % of the universe, ordinary matter 5 %.", "cosmos", [], "Planck cosmological parameters. Not a shape claim.", None),
    (62, "Dark matter outweighs visible matter six to one.", "cosmos", [], "Same source as #60. Not a shape claim.", None),
    (77, "Black holes: event horizons and time dilation.", "cosmos", [], "Event Horizon Telescope imagery and gravitational-wave ringdowns. Not a shape claim.", None),
    (95, "Supermassive black holes power quasars and jets.", "cosmos", [], "Radio and X-ray astronomy. Not a shape claim.", None),
    (99, "Dark energy is 68&ndash;70 % of the universe&rsquo;s energy.", "cosmos", [], "Supernova distances and the CMB. Not a shape claim.", None),
    (40, "Parker Solar Probe is the fastest human-made object, at 430,000 mph.", "space", [], "Mission telemetry. Not assessed here.", None),
    (58, "NEOWISE catalogued 44,000 objects and 3,000 near-Earth asteroids.", "space", [], "Mission data; the catalogue itself is public through the Minor Planet Center. Not assessed here.", None),
    (59, "Voyager 1 reaches one light-day from Earth in 2026.", "space", [], "One light-day is 25.9 billion km (re-derived); the milestone is a matter of JPL&rsquo;s tracking. Not assessed here.", None),
    (61, "Deep-space probes use gravity assists.", "space", [], "Orbital mechanics; the manoeuvre is standard. Not assessed here.", None),
    (63, "Aldrin used a pen to close a broken circuit-breaker.", "space", [], "Apollo 11 mission transcript and crew accounts. Not assessed here.", None),
    (64, "Lunar dust smelled of gunpowder.", "space", [], "Crew accounts. Not assessed here.", None),
    (65, "Ninety-six bags of waste were left on the Moon.", "space", [], "Mission records. Not assessed here.", None),
    (66, "The Moon&rsquo;s sphere of influence.", "space", [], "Orbital mechanics; a definition more than a claim. Not assessed here.", None),
    (67, "The lunar module landed on a throttleable engine.", "space", [], "Engineering record. Not assessed here.", None),
    (68, "Lunar gravity meant only about 3,000 lb of thrust to hover.", "space", [], "Engineering record; the figure as stated is low for the landed mass. Not assessed here.", None),
    (69, "South Pole&ndash;Aitken is the largest lunar basin.", "space", [], "Orbital altimetry. Not assessed here.", None),
    (70, "Mons Huygens is the tallest lunar mountain.", "space", [], "Orbital altimetry; the Apennines are a fine sight near first quarter in any telescope. Not assessed here.", None),
    (71, "Permanently shadowed craters reach &minus;246&nbsp;&deg;C.", "space", [], "Diviner radiometer. Not assessed here.", None),
    (72, "Polar craters hold water ice.", "space", [], "LCROSS impact plume and neutron spectroscopy. Not assessed here.", None),
    (73, "Impact gardening turns over the regolith.", "space", [], "Sample analysis. Not assessed here.", None),
    (74, "The Moon has a tenuous exosphere.", "space", [], "LADEE and ground-based sodium-tail imaging. Not assessed here.", None),
    (36, "Lunar regolith is jagged, abrasive glass and rock.", "space", [], "Returned-sample analysis. Not assessed here.", None),
    (81, "Over 200 spacewalks built the ISS.", "space", [], "Programme records. Not assessed here.", None),
    (82, "Canadarm2 walks along the station.", "space", [], "Programme records. Not assessed here.", None),
    (83, "The truss is 357 ft and the arrays add drag.", "space", [], "Programme records. Not assessed here.", None),
    (84, "Modules were added through 2021.", "space", [], "Programme records. Not assessed here.", None),
    (85, "ECLSS recycles 93 % of water.", "space", [], "Programme records. Not assessed here.", None),
    (86, "The ISS has a distinct smell.", "space", [], "Crew accounts. Not assessed here.", None),
    (87, "Fluid shift and dulled taste in microgravity.", "space", [], "Crew medicine. Not assessed here.", None),
    (89, "The ISS is a microbial living lab.", "space", [], "Programme science. Not assessed here.", None),
    (90, "Astronauts see cosmic-ray flashes.", "space", [], "Crew accounts since Apollo. Not assessed here.", None),
    (91, "Skin flakes float in microgravity.", "space", [], "Crew accounts. Not assessed here.", None),
    (92, "No laundry in space.", "space", [], "Programme records. Not assessed here.", None),
    (93, "The space toilet is a vacuum system.", "space", [], "Programme records. Not assessed here.", None),
    (94, "Astronauts exercise 2.5 hours a day.", "space", [], "Crew medicine. Not assessed here.", None),
    (96, "The ISS orbits at about 17,500 mph, once every 90 minutes, giving 16 sunrises a day.", "space", [],
     "Checkable from a garden: a visible pass crosses the sky in about six minutes, and near the zenith the station moves about one degree per second, which at 400&nbsp;km altitude is 7.7&nbsp;km/s (re-derived: 7.67&nbsp;km/s, 92-minute period). Successive passes come about 90&ndash;95 minutes apart.",
     "<a href='https://spotthestation.nasa.gov/'>NASA &ldquo;Spot the Station&rdquo;</a> or <a href='https://www.heavens-above.com/'>heavens-above.com</a> for pass predictions to the minute; time a pass against a stopwatch and compare."),
    (97, "Gravity at ISS altitude is 90 % of the surface value; the station is in free fall.", "space", ["balloon-curve"],
     "The 90 % follows from the inverse-square law at 6,771&nbsp;km radius (re-derived: 88.5 %). Free fall is what the timed pass in #96 measures: the observed speed is the circular-orbit speed for that gravity. The book&rsquo;s own QR&nbsp;72 shows six minutes of it on a sounding rocket, and its coast time fits the inverse square.",
     "Same pass timing as <a href='#b96'>#96</a>; the circular-orbit speed &radic;(GM/r) with GM = 3.986&nbsp;&times;&nbsp;10<sup>14</sup>&nbsp;m&sup3;/s&sup2; from <a href='https://ssd.jpl.nasa.gov/astro_par.html'>JPL&rsquo;s astrodynamic parameters</a>."),
    (98, "Atmospheric drag slows the ISS; it needs periodic reboosts.", "space", [],
     "Visible in the public orbital elements: the station&rsquo;s altitude decays by tens of metres a day between reboosts, and the reboosts show as steps. Anyone can plot it.",
     "Two-line elements from <a href='https://celestrak.org/NORAD/elements/gp.php?CATNR=25544'>CelesTrak (NORAD 25544)</a>; the mean-motion history over a year shows the decay and the steps."),
    (100, "Tissue chips are tested on the ISS.", "space", [], "Programme science. Not assessed here.", None),
    (101, "MISSE exposes materials outside the station.", "space", [], "Programme science. Not assessed here.", None),
    (102, "The Cold Atom Lab makes Bose&ndash;Einstein condensates in orbit.", "space", [], "Programme science. Not assessed here.", None),
    (103, "The AMS has recorded 235 billion cosmic-ray events.", "space", [], "Programme science. Not assessed here.", None),
]


# Structural items bear on a model -- the shape of the Earth, the heliocentric arrangement, the
# standard cosmological picture -- so their evidence is evidence about the model. Every other item
# is particular: a fact about one object (this asteroid has a moon, that crater is cold) which no
# model stands or falls on, however well attested.
STRUCTURAL = {
    1, 3, 12, 13, 14, 45, 46, 48,                       # earth
    2, 10, 11, 15, 18, 19, 20, 30, 31, 34, 35, 39, 41,  # helio
    4, 5, 6, 7, 8, 9, 21, 22, 23, 24, 25, 26, 27, 29, 47, 60, 62, 77, 95, 99,   # cosmos
    61, 66, 96, 97, 98,                                 # space: orbital mechanics, observable
}


def kind(n):
    return "structural" if n in STRUCTURAL else "particular"


def validate():
    ns = sorted(n for n, *_ in B)
    expected = [n for n in range(1, 104) if n != 88]
    assert ns == expected, f"numbering: missing {sorted(set(expected) - set(ns))}, extra {sorted(set(ns) - set(expected))}"
    assert STRUCTURAL <= set(ns), sorted(STRUCTURAL - set(ns))
    for n, _, g, pages, check, ref in B:
        assert g in GROUPS, (n, g)
        for p in pages:
            assert p in PAGES and os.path.isdir(os.path.join(ROOT, "docs", p)), (n, p)
    for _, _, pages, _, _, _ in TEAM_A:
        for p in pages:
            assert p in PAGES and os.path.isdir(os.path.join(ROOT, "docs", p)), p


def counts():
    return {g: sum(1 for _, _, gg, *_ in B if gg == g) for g in GROUPS}


def chip(cls, text):
    return f'<span class="vd {cls}">{text}</span>'


def page_links(pages, chips=False):
    if not chips:
        return ", ".join(f'<a href="{SITE}{p}/">{PAGES[p][0]}</a>' for p in pages)
    return "<br>".join((chip(PAGES[p][1], PAGES[p][2]) if PAGES[p][1] else "") + f'<a href="{SITE}{p}/">{PAGES[p][0]}</a>' for p in pages)


def team_a_html():
    o = ['<table class="cmp map"><thead><tr><th>His claim</th><th>What it says</th><th>Answered on</th><th>Verdict</th></tr></thead><tbody>']
    for lab, text, pages, vtext, vcls, note in TEAM_A:
        o.append(f'<tr><td class="metric">{lab}</td><td>{text}<br><span class="how">{note}</span></td>'
                 f'<td>{page_links(pages) or "&mdash;"}</td><td>{chip(vcls, vtext)}</td></tr>')
    o.append("</tbody></table>")
    return "\n".join(o)


def team_b_html():
    o = []
    for g, (head, desc) in GROUPS.items():
        rows = [r for r in B if r[2] == g]
        o.append(f'<h3 id="g-{g}">{head} <span class="cnt">{len(rows)} of {len(B)}</span></h3>')
        o.append(f'<p class="gdesc">{desc}</p>')
        o.append('<table class="cmp map"><thead><tr><th>#</th><th>His claim, in short</th><th>Type</th><th>Where this site answers it</th><th>What a reader can check, and where the evidence is</th></tr></thead><tbody>')
        for n, text, _, pages, check, ref in rows:
            where = page_links(pages, chips=True) if pages else '<span class="none">no page: not a shape claim</span>'
            cell = check + (f'<br><span class="ref">{ref}</span>' if ref else "")
            o.append(f'<tr id="b{n}"><td class="metric">#{n}</td><td>{text}</td><td><span class="ty {kind(n)}">{kind(n)}</span></td><td>{where}</td><td>{cell}</td></tr>')
        o.append("</tbody></table>")
    return "\n".join(o)


def summary_html():
    c = counts()
    with_page = sum(1 for r in B if r[3])
    checkable = sum(1 for r in B if r[5])
    o = ['<table class="cmp"><thead><tr><th>Group</th><th>Items</th><th>Structural</th><th>Answered on a page here</th></tr></thead><tbody>']
    for g, (head, _) in GROUPS.items():
        rows = [r for r in B if r[2] == g]
        o.append(f'<tr><td><a href="#g-{g}">{head}</a></td><td class="metric">{len(rows)}</td><td class="metric">{sum(1 for r in rows if r[0] in STRUCTURAL)}</td><td class="metric">{sum(1 for r in rows if r[3])}</td></tr>')
    o.append(f'<tr><td><strong>All</strong></td><td class="metric"><strong>{len(B)}</strong></td><td class="metric"><strong>{len(STRUCTURAL)}</strong></td><td class="metric"><strong>{with_page}</strong></td></tr>')
    o.append("</tbody></table>")
    return "\n".join(o)


def inject(html, key, body):
    pat = re.compile(rf"(<!--MAP:{key}-->).*?(<!--/MAP:{key}-->)", re.S)
    assert pat.search(html), key
    return pat.sub(lambda m: m.group(1) + "\n" + body + "\n" + m.group(2), html)


def main():
    validate()
    if "--check" in sys.argv:
        for g, n in counts().items():
            print(f"{g:8s} {n}")
        print("total", len(B), "structural", len(STRUCTURAL), "with page", sum(1 for r in B if r[3]), "with reference", sum(1 for r in B if r[5]))
        for g in GROUPS:
            print("  ", g, "structural", sum(1 for r in B if r[2] == g and r[0] in STRUCTURAL))
        return
    html = open(PAGE, encoding="utf-8").read()
    html = inject(html, "A", team_a_html())
    html = inject(html, "SUMMARY", summary_html())
    html = inject(html, "B", team_b_html())
    open(PAGE, "w", encoding="utf-8").write(html)
    print("wrote", os.path.relpath(PAGE, ROOT), counts())


if __name__ == "__main__":
    main()
