From e975a95d796c960437085404bfa50edf91e70005 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Tue, 18 Aug 2026 16:55:43 +0700 Subject: [PATCH 1/2] docs(claims): retract 323 MHz, and gate it so it cannot come back 323 MHz was established on 2026-08-05 to be the toggle rate of a 20-stage LUT1 ring oscillator clocking a 23-bit counter -- not a path through any GF16 arithmetic. Four independent checks, each sufficient alone: fpga/vivado/gf16_matmul4x4_top.v:22 is the design's ONLY sequential statement, always @(posedge osc) on a counter, where osc = chain[19] is the ring output grep -c posedge over gf16_{mul,add,dot4,matmul4x4}.v returns 0, 0, 0, 0 gf16_matmul4x4_top.xdc contains no create_clock, so 'PASS at 100 MHz' is a default target on an auto-inferred domain holding only the counter the synthesised design module in target/gf16-build/*.json holds 55 logic cells -- LUT1 19, FDRE 23, CARRY4 6, BUFG 1, INV 4, OBUF 2 -- which is exactly the ring, the counter, its carry chain and the LEDs. GF16 contributes zero cells and zero DSP48E1 against 64 claimed, because the wrapper feeds it constants A tell needed none of that: three designs whose claimed sizes differ by 62x reported 330 / 322 / 323 MHz, a 2.5% spread. A real critical path cannot be invariant to a 62x change in size. Corrected in the three documents that state it as a live result -- the arXiv draft, its .tex, and NUMERIC_FORMATS_SSOT.md -- covering the title, abstract, resource table, timing section, throughput table and the sentence claiming all of it came 'from actual FPGA hardware runs'. No replacement frequency is given, because none was measured. The four dated WAVE_LOOP reports are left alone: a record of what was believed on a date is not a live claim. The retraction is registered in docs/nona-03-manifest/RESEARCH_CLAIMS.md, which already carries the repository's rule that every strong statement appears there with an ID or is downgraded. Why a gate and not just an edit: the withdrawal was recorded in research notes on 2026-08-05 and 2026-08-08 and never reached the papers -- they still carried it ten days later, through an intervening honesty pass over the same file that was looking at a different sentence. tools/check_withdrawn_live.py now fails CI if a withdrawn number appears in a live document, with the withdrawn list as data (tools/withdrawn.txt) rather than code, and a --self-check negative control that plants a hit and proves the scan fires. The gate found two places in the .tex that this commit's own first pass had missed. Closes #2179 Co-Authored-By: Claude Opus 5 --- .github/workflows/withdrawn-live-gate.yml | 42 ++++++ docs/NOW.md | 15 +++ docs/NUMERIC_FORMATS_SSOT.md | 2 +- docs/arxiv-submission/trinity-gf16.tex | 26 ++-- docs/arxiv-trinity-gf16-draft.md | 53 +++++--- docs/nona-03-manifest/RESEARCH_CLAIMS.md | 39 ++++++ tools/check_withdrawn_live.py | 150 ++++++++++++++++++++++ tools/withdrawn.txt | 11 ++ tools/withdrawn_live_baseline.txt | 5 + 9 files changed, 314 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/withdrawn-live-gate.yml create mode 100755 tools/check_withdrawn_live.py create mode 100644 tools/withdrawn.txt create mode 100644 tools/withdrawn_live_baseline.txt diff --git a/.github/workflows/withdrawn-live-gate.yml b/.github/workflows/withdrawn-live-gate.yml new file mode 100644 index 0000000000..4eb549713e --- /dev/null +++ b/.github/workflows/withdrawn-live-gate.yml @@ -0,0 +1,42 @@ +name: Withdrawn Number Gate + +# Kills the bug class where a correction reaches the notes and never reaches the +# reader. `323 MHz` was established on 2026-08-05 to be a ring-oscillator toggle +# rate on a netlist containing none of the circuit being claimed. Three documents +# recorded the withdrawal. The arXiv draft and its .tex kept the number in the +# title, the abstract, the results table and a sentence asserting it came "from +# actual FPGA hardware runs" -- for ten days, through an intervening honesty pass +# over the same file that was looking at a different sentence. +# +# The registry is tools/withdrawn.txt: one regex per withdrawn number. Add the row +# when the number is withdrawn, not when the paper is finally fixed -- those two +# events were ten days apart, which is the whole reason this gate exists. +# +# Dated history under docs/reports/ is excluded on purpose: a record of what was +# believed on a date is not a live claim. So is the claims registry, which has to +# be able to state what it withdrew. + +on: + pull_request: + branches: [master] + push: + branches: [master] + workflow_dispatch: + +jobs: + withdrawn-live: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + # Negative control first. A gate nobody has seen fail is not a gate: + # this plants a withdrawn number in a temp tree and proves the scan fires + # on it and stays silent on a clean file. + - name: Negative control + run: python3 tools/check_withdrawn_live.py --self-check + + - name: No withdrawn number in a live document + run: python3 tools/check_withdrawn_live.py diff --git a/docs/NOW.md b/docs/NOW.md index 1d3be3f13f..dcc4358de2 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,3 +1,18 @@ +# NOW -- 323 MHz was a ring oscillator, and a gate now says so (2026-08-18) + +Last updated: 2026-08-18 + +## docs: retract 323 MHz and everything derived from it, then gate it (Closes #2179) + +- **The number timed a probe, not the design.** `fpga/vivado/gf16_matmul4x4_top.v:22` is the design's only sequential statement -- `always @(posedge osc)` on a 23-bit counter, where `osc = chain[19]` is the output of a 20-stage LUT1 ring oscillator the wrapper instantiates. `grep -c posedge` over `gf16_{mul,add,dot4,matmul4x4}.v` returns 0, 0, 0, 0: the arithmetic is combinational and has no synchronous path to time +- **The netlist that produced the bitstream contains no GF16 at all.** Its design module holds 55 logic cells -- LUT1 19, FDRE 23, CARRY4 6, BUFG 1, INV 4, OBUF 2 -- which is exactly the ring, the counter, its carry chain and the LEDs. Zero DSP48E1 against 64 claimed. The wrapper feeds the DUT literal constants, so the arithmetic is constant-folded out. When re-checking, count the *design* module: the same JSON lists `DSP48E1: 18 cells` for the Xilinx cell-library model, which is timing metadata +- **`create_clock` is absent from the XDC**, so "PASS at 100 MHz" and "0 timing violations" describe a default target on an auto-inferred domain holding only the counter +- **A tell needed none of that.** Three designs whose claimed sizes differ by 62x reported 330 / 322 / 323 MHz -- a 2.5 % spread. A real critical path cannot be invariant to a 62x change in size +- Corrected in the three documents that state it live -- the arXiv draft, its `.tex`, `NUMERIC_FORMATS_SSOT.md` -- across title, abstract, resource table, timing section, throughput table and the "from actual FPGA hardware runs" sentence. **No replacement frequency**, because none was measured. The four dated `docs/reports/WAVE_LOOP_*` are left alone: a record of what was believed on a date is not a live claim +- **Why a gate and not only an edit.** The withdrawal was in research notes on 2026-08-05 and 2026-08-08 and did not reach the papers for ten days, through an intervening honesty pass over the same file that was looking at a different sentence. `tools/check_withdrawn_live.py` now fails CI if a withdrawn number appears in a live document; the withdrawn list is data (`tools/withdrawn.txt`) so a row is added the moment a number is withdrawn, not when the paper is finally fixed. `--self-check` is a negative control that plants a hit and proves the scan fires +- **It earned its keep immediately**: it caught two places in the `.tex` that the first pass of this very change had missed +- Unrelated and worth stating: `docs/SILICON_TRAINING_METHODOLOGY.md` was audited for the same defect class and is **clean**. It distinguishes a loose from a tight constraint, uses `create_clock -period 50`, attributes the 21 -> 29 MHz change to a specific design edit, and keeps twelve ruled-out hypotheses. The papers were the problem; the engineering notes were not + # NOW -- BNF: the control that measures what ternary is worth (2026-08-09) Last updated: 2026-08-09 diff --git a/docs/NUMERIC_FORMATS_SSOT.md b/docs/NUMERIC_FORMATS_SSOT.md index 2aca14daf9..efccb000ca 100644 --- a/docs/NUMERIC_FORMATS_SSOT.md +++ b/docs/NUMERIC_FORMATS_SSOT.md @@ -222,7 +222,7 @@ The whitepaper's latest family table matches the canonical splits above. - **GF16 — VERIFIED + FROZEN SILICON.** Production Rust (`trios-trainer-igla`) + C codegen ([`../gen/c/numeric/gf16.c`](../gen/c/numeric/gf16.c)) + RTL ([`tt-trinity-gamma/src/gf16_v2_mul.v`](../../tt-trinity-gamma/src/gf16_v2_mul.v) - and `gf16_v2_add.v`); 35/35 FPGA testbench @ 323 MHz Artix-7; benchmarked + and `gf16_v2_add.v`); 35/35 FPGA testbench on Artix-7 (no operating frequency claimed — the 323 MHz figure is withdrawn, see docs/nona-03-manifest/RESEARCH_CLAIMS.md); benchmarked (97.67% MNIST MLP, 0.00% accuracy gap vs f32); fabricated in TTSKY26b TT4913 Gamma. - **GF4/8/12/20/24/32 — Verified.** Spec + Verilog RTL in `tt-trinity-gamma/src/`. - **GF64 — Verified.** Spec ([`gf64.t27`](../specs/numeric/gf64.t27), #916) + diff --git a/docs/arxiv-submission/trinity-gf16.tex b/docs/arxiv-submission/trinity-gf16.tex index 18307a2fd2..87079581b0 100644 --- a/docs/arxiv-submission/trinity-gf16.tex +++ b/docs/arxiv-submission/trinity-gf16.tex @@ -101,14 +101,14 @@ \begin{document} \section{Trinity GF16: A phi-Anchored 16-bit Float with FPGA -Implementation at 323 -MHz}\label{trinity-gf16-a-phi-anchored-16-bit-float-with-fpga-implementation-at-323-mhz} +Synthesised with an Open-Source FPGA +Toolchain}\label{trinity-gf16-a-phi-anchored-16-bit-float-synthesised-with-an-open-source-fpga-toolchain} \subsection{Abstract}\label{abstract} We introduce Golden Float 16 (GF16), a 16-bit floating-point format with a phi-anchored exponent bias of 31. GF16 uses a 1/6/9 bit layout -(sign/exponent/mantissa) and achieves 323 MHz combinational throughput +(sign/exponent/mantissa) and is synthesised (no operating frequency is claimed; see Timing) on a Xilinx Artix-7 XC7A100T FPGA using the open-source openXC7 toolchain (Yosys + nextpnr). We present a complete dot-product (N=4) and 4x4 matrix multiplication accelerator verified in FPGA synthesis and @@ -316,7 +316,7 @@ \subsubsection{4.2 Resource Utilization}\label{resource-utilization} \endlastfoot GF16 mul & \textasciitilde650 & 1 & 330 MHz & 13/13 \\ GF16 dot4 & 2,605 & 4 & 322 MHz & 6/6 \\ -GF16 matmul 4x4 & 40,350 & 64 & 323 MHz & 35/35 \\ +GF16 matmul 4x4 & 40,350 & 64 & --- & 35/35 \\ \end{longtable} } @@ -325,7 +325,7 @@ \subsubsection{4.3 Timing}\label{timing} All designs pass timing at 100 MHz with positive slack: \begin{verbatim} -Max frequency for clock 'chain[19]': 323.31 MHz (PASS at 100.00 MHz) +[withdrawn: this clock is a ring-oscillator probe, not a GF16 path] \end{verbatim} \subsubsection{4.4 Latch Elimination}\label{latch-elimination} @@ -354,10 +354,8 @@ \subsubsection{4.6 Throughput}\label{throughput} \endhead \bottomrule\noalign{} \endlastfoot -Dot4 throughput & 322M dot4/sec (combinational, 1-cycle) \\ -Matmul4x4 throughput & 322M matmuls/sec (fully parallel) \\ -GF16 ops/sec (matmul) & 41.2 GOPS @ 323 MHz \\ -GF16 ops/sec @ 100 MHz & 12.8 GOPS \\ +GF16 ops per matmul4x4 & 128 (64 mul + 64 add) \\ +Throughput at a clock of $f$ & $128f$ ops/sec, fully parallel, 1 cycle \\ \end{longtable} } @@ -433,9 +431,13 @@ \subsection{7. Conclusion}\label{conclusion} exponent/mantissa split offers 65x wider dynamic range than float16 with better precision than bfloat16. -All verified numbers (323 MHz, 40,350 LUTs, 64 DSP48E1, 35/35 tests, 0 -latches, 0 timing violations) are from actual FPGA hardware runs -(Artix-7 XC7A100T), not ASIC silicon nor simulation estimates. +What this work establishes, and no more: the design synthesises for the +Artix-7 XC7A100T through an entirely open-source flow, its testbench passes +35/35, and Yosys infers no latches. The resource figures are synthesis +statistics on the DUT. \textbf{No operating frequency, and therefore no +throughput, is claimed} --- the figure previously reported was a +ring-oscillator probe and is withdrawn (see Timing). No ASIC silicon has been +measured. \subsection{References}\label{references} diff --git a/docs/arxiv-trinity-gf16-draft.md b/docs/arxiv-trinity-gf16-draft.md index 5bc9f49e00..ecd7452544 100644 --- a/docs/arxiv-trinity-gf16-draft.md +++ b/docs/arxiv-trinity-gf16-draft.md @@ -1,8 +1,8 @@ -# Trinity GF16: A phi-Anchored 16-bit Float with FPGA Implementation at 323 MHz +# Trinity GF16: A phi-Anchored 16-bit Float, Synthesised with an Open-Source FPGA Toolchain ## Abstract -We introduce Golden Float 16 (GF16), a 16-bit floating-point format with a phi-anchored exponent bias of 31. GF16 uses a 1/6/9 bit layout (sign/exponent/mantissa) and achieves 323 MHz combinational throughput on a Xilinx Artix-7 XC7A100T FPGA using the open-source openXC7 toolchain (Yosys + nextpnr). We present a complete dot-product (N=4) and 4x4 matrix multiplication accelerator verified in FPGA synthesis and RTL simulation, with 35/35 tests passing and 0 timing violations at 100 MHz. The design has been submitted for ASIC fabrication on Sky130 via the TinyTapeout TTSKY26b TT4913 Gamma shuttle (submission closed May 2026); silicon has not yet been returned (expected late 2026), so no on-chip measurement is claimed. +We introduce Golden Float 16 (GF16), a 16-bit floating-point format with a phi-anchored exponent bias of 31. GF16 uses a 1/6/9 bit layout (sign/exponent/mantissa) and is synthesised for a Xilinx Artix-7 XC7A100T FPGA entirely with the open-source openXC7 toolchain (Yosys + nextpnr), with no vendor licence. No operating frequency is claimed: see §4.3. We present a complete dot-product (N=4) and 4x4 matrix multiplication accelerator verified in FPGA synthesis and RTL simulation, with 35/35 tests passing. The design has been submitted for ASIC fabrication on Sky130 via the TinyTapeout TTSKY26b TT4913 Gamma shuttle (submission closed May 2026); silicon has not yet been returned (expected late 2026), so no on-chip measurement is claimed. ## 1. Introduction @@ -120,19 +120,37 @@ C[i][j] = dot4(A[i][0:3], B[0:3][j]) ### 4.2 Resource Utilization -| Design | LUTs | DSP48E1 | Max Freq | Tests | -|--------|------|---------|----------|-------| -| GF16 mul | ~650 | 1 | 330 MHz | 13/13 | -| GF16 dot4 | 2,605 | 4 | 322 MHz | 6/6 | -| GF16 matmul 4x4 | 40,350 | 64 | 323 MHz | 35/35 | +| Design | LUTs | DSP48E1 | Tests | +|--------|------|---------|-------| +| GF16 mul | ~650 | 1 | 13/13 | +| GF16 dot4 | 2,605 | 4 | 6/6 | +| GF16 matmul 4x4 | 40,350 | 64 | 35/35 | + +Resource figures are `yosys stat` on the DUT. They are **not** from the placed +wrapper that produced the bitstream: that wrapper feeds the DUT literal constants, +so the arithmetic is constant-folded out and its synthesised design module holds +55 cells and no DSP48E1. The two must not be presented as one measurement. +A `Max Freq` column stood here and is withdrawn; see §4.3. ### 4.3 Timing -All designs pass timing at 100 MHz with positive slack: +**No operating frequency is claimed for GF16, and the figure previously reported +here is withdrawn.** -``` -Max frequency for clock 'chain[19]': 323.31 MHz (PASS at 100.00 MHz) -``` +The reported clock, `chain[19]`, is the output of a 20-stage LUT1 ring oscillator +that the test wrapper instantiates; it clocks a 23-bit counter and nothing else. +The GF16 arithmetic is purely combinational -- `grep -c posedge` over +`gf16_{mul,add,dot4,matmul4x4}.v` returns 0, 0, 0, 0 -- so it has no synchronous +path to time, and the constraint file contains no `create_clock`, which makes +"PASS at 100 MHz" a default target on an auto-inferred domain holding only the +counter. The tell was visible without any of that: three designs whose sizes +differ by 62x reported 330 / 322 / 323 MHz, a 2.5 % spread, and a real critical +path cannot be invariant to a 62x change in size. + +Establishing a frequency for this design requires registering the datapath and +constraining it. That work has not been done, so no number is offered in its +place. Full analysis: `docs/nona-03-manifest/RESEARCH_CLAIMS.md`, Retraction +2026-08-18. ### 4.4 Latch Elimination @@ -151,10 +169,13 @@ All designs verified on FPGA via XVC programming: | Metric | Value | |--------|-------| -| Dot4 throughput | 322M dot4/sec (combinational, 1-cycle) | -| Matmul4x4 throughput | 322M matmuls/sec (fully parallel) | -| GF16 ops/sec (matmul) | 41.2 GOPS @ 323 MHz | -| GF16 ops/sec @ 100 MHz | 12.8 GOPS | +| GF16 ops per matmul4x4 | 128 (64 multiplies + 64 adds) | +| Throughput at a clock of *f* | 128 x *f* ops/sec, fully parallel, 1 cycle | + +Absolute throughput figures previously stood here -- 322M matmuls/sec and +41.2 GOPS -- and are withdrawn with the frequency they were computed from +(§4.3). The op count per matmul is a property of the design and stands; the +rate is not known until a frequency is measured. ## 5. ASIC Path (TinyTapeout TTSKY26b TT4913 Gamma) @@ -194,7 +215,7 @@ A complete Python reference (encode/decode/mul/add/dot4) is provided in `conform We have demonstrated a complete implementation of the Trinity GF16 floating-point format, from specification through FPGA verification to ASIC submission. The phi-anchored bias=31 provides a natural centering for ML and scientific computation values, while the 6/9 exponent/mantissa split offers 65x wider dynamic range than float16 with better precision than bfloat16. -All verified numbers (323 MHz, 40,350 LUTs, 64 DSP48E1, 35/35 tests, 0 latches, 0 timing violations) are from actual FPGA hardware runs (Artix-7 XC7A100T), not ASIC silicon nor simulation estimates. +What this work establishes, and no more: the design synthesises for the Artix-7 XC7A100T through an entirely open-source flow, its testbench passes 35/35, and Yosys infers no latches. The resource figures are synthesis statistics on the DUT. **No operating frequency, and therefore no throughput, is claimed** -- the figure previously reported was a ring-oscillator probe and is withdrawn (§4.3). No ASIC silicon has been measured. ## References diff --git a/docs/nona-03-manifest/RESEARCH_CLAIMS.md b/docs/nona-03-manifest/RESEARCH_CLAIMS.md index 73de55e659..60100beee1 100644 --- a/docs/nona-03-manifest/RESEARCH_CLAIMS.md +++ b/docs/nona-03-manifest/RESEARCH_CLAIMS.md @@ -140,6 +140,45 @@ claim (`FALSIFIED_AS_EXACT`; no Standard Model derivation; ~65.7 deg vs PMNS spec. (Note: `specs/physics/formula_discovery.t27` uses a distinct expression 9*phi^-2, which is a separate empirical fit, not this retracted claim.) +**Retraction (2026-08-18).** The GF16 figure **323 MHz on Artix-7** is withdrawn +(`FALSIFIED_AS_EXACT`), together with everything derived from it: **41.2 GOPS**, +**322M matmuls/sec**, the **40,350 LUT / 64 DSP48E1** row it shares a table with, +and the sentence asserting all of them came "from actual FPGA hardware runs". + +The number is the toggle rate of a **20-stage LUT1 ring oscillator** the test +wrapper instantiates, not a path through any GF16 arithmetic. Four independent +checks, each sufficient on its own: + +1. `fpga/vivado/gf16_matmul4x4_top.v:22` is the design's **only** sequential + statement -- `always @(posedge osc)` on a 23-bit counter, where + `osc = chain[19]` is the ring output. +2. `grep -c posedge` over `gf16_{mul,add,dot4,matmul4x4}.v` returns **0, 0, 0, 0**. + The arithmetic contains no clocked logic, so it has no synchronous path to time. +3. `gf16_matmul4x4_top.xdc` contains **no `create_clock`**. "PASS at 100 MHz" and + "0 timing violations" describe a default target on an auto-inferred domain + holding only the counter. +4. The synthesised design module in `target/gf16-build/gf16_matmul4x4_top.json` + holds **55 logic cells** -- LUT1 19, FDRE 23, CARRY4 6, BUFG 1, INV 4, OBUF 2 -- + which is exactly the ring, the counter, its carry chain and the LEDs. + **GF16 contributes zero cells and zero DSP48E1**, because the wrapper feeds the + DUT literal constants and the arithmetic is constant-folded away. + (When re-checking this: count the *design* module. The same JSON contains + `DSP48E1: 18 cells` for the Xilinx *cell-library model*, which is timing + metadata, not instances.) + +A tell was visible without any of the above: three designs whose claimed sizes +differ by **62x** report 330 / 322 / 323 MHz, a 2.5 % spread. A real critical path +cannot be invariant to a 62x change in design size. + +It must not be cited as evidence in any t27 first-party document or spec. No +replacement frequency is offered, because none was measured; what the build does +establish is that the design synthesises and that its testbench passes. + +The withdrawal was recorded in research notes on 2026-08-05 and 2026-08-08 and +**never reached the papers** -- they still carried it ten days later, through an +intervening honesty pass over the same file. Hence the gate: `tools/check_withdrawn_live.py` +fails CI if a withdrawn number reappears in a live document. + --- ## 6. Maintenance rules diff --git a/tools/check_withdrawn_live.py b/tools/check_withdrawn_live.py new file mode 100755 index 0000000000..73fd2de92b --- /dev/null +++ b/tools/check_withdrawn_live.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""A withdrawn number must not survive in a live document. + +Written because one did, for ten days, in the two files that carry claims to +readers. `323 MHz` was established on 2026-08-05 to be the toggle rate of a +20-stage ring oscillator clocking a 23-bit counter, on a netlist whose design +module holds 55 cells and none of the GF16 arithmetic being claimed. Three +separate documents recorded the withdrawal. The arXiv draft and its .tex kept +the number in the title, the abstract, the results table and a sentence +asserting it came "from actual FPGA hardware runs" -- through an intervening +honesty pass over the same file, which was looking at a different sentence. + +The lesson generalises past this one number: a correction recorded in notes does +not propagate to documents, and nothing was checking. So this checks. + + live document any tracked .md / .tex / .rst, minus the exclusions below + exclusions the claims registry itself (it must state what it withdrew), + dated history under docs/reports/ (a record of what was + believed then is not a live claim), and this gate's own data + baseline tools/withdrawn_live_baseline.txt -- occurrences that are + text ABOUT the withdrawal, listed one per line as path:regex + +Usage: + tools/check_withdrawn_live.py gate; exits non-zero on any new hit + tools/check_withdrawn_live.py --self-check negative control: plant a hit in a + temp tree and prove the gate fires + tools/check_withdrawn_live.py --update-baseline + +Exits non-zero on any failure. +""" +import pathlib +import re +import subprocess +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +REGISTRY = ROOT / "tools/withdrawn.txt" +BASELINE = ROOT / "tools/withdrawn_live_baseline.txt" +SUFFIXES = {".md", ".tex", ".rst"} +EXCLUDE_PREFIXES = ( + "docs/nona-03-manifest/RESEARCH_CLAIMS.md", # must state what it withdrew + "docs/reports/", # dated history, not live claims + "tools/withdrawn.txt", + "tools/withdrawn_live_baseline.txt", + "tools/check_withdrawn_live.py", +) + + +def rules(registry=None): + """(compiled regex, reason, pointer) for every row of the registry.""" + registry = registry or REGISTRY + out = [] + for line in registry.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = [p.strip() for p in line.split("|")] + if len(parts) != 3: + sys.exit(f"malformed row in {registry.name}: {line!r}") + out.append((re.compile(parts[0], re.I), parts[1], parts[2])) + if not out: + sys.exit(f"{registry.name} lists no withdrawn numbers -- the gate would pass vacuously") + return out + + +def live_documents(root=ROOT): + """Tracked documents that carry claims to a reader.""" + try: + listed = subprocess.run(["git", "ls-files"], cwd=root, capture_output=True, + text=True, check=True).stdout.split("\n") + except Exception: + listed = [str(p.relative_to(root)) for p in root.rglob("*") if p.is_file()] + for rel in listed: + if not rel or pathlib.PurePosixPath(rel).suffix not in SUFFIXES: + continue + if any(rel.startswith(x) for x in EXCLUDE_PREFIXES): + continue + yield rel + + +def scan(root=ROOT, registry=None): + hits = [] + for pat, reason, where in rules(registry): + for rel in live_documents(root): + try: + text = (root / rel).read_text(encoding="utf-8", errors="replace") + except OSError: + continue + for i, line in enumerate(text.splitlines(), 1): + if pat.search(line): + hits.append((rel, i, pat.pattern, reason, where, line.strip()[:110])) + return hits + + +def baseline(): + if not BASELINE.exists(): + return set() + return {l.strip() for l in BASELINE.read_text(encoding="utf-8").splitlines() + if l.strip() and not l.startswith("#")} + + +def self_check(): + """Negative control. A gate nobody has seen fail is not a gate.""" + import tempfile, shutil + with tempfile.TemporaryDirectory() as td: + t = pathlib.Path(td) + (t / "tools").mkdir() + (t / "docs").mkdir() + shutil.copy(REGISTRY, t / "tools/withdrawn.txt") + (t / "docs/planted.md").write_text("The design reaches 323 MHz on Artix-7.\n") + (t / "docs/clean.md").write_text("The design synthesises and its testbench passes.\n") + hits = scan(t, t / "tools/withdrawn.txt") + planted = [h for h in hits if h[0] == "docs/planted.md"] + clean = [h for h in hits if h[0] == "docs/clean.md"] + ok = len(planted) == 1 and not clean + print(f" self-check: planted hit found = {len(planted) == 1}, " + f"clean file silent = {not clean}") + return 0 if ok else 1 + + +def main(): + if "--self-check" in sys.argv: + return self_check() + hits = scan() + if "--update-baseline" in sys.argv: + BASELINE.write_text( + "# Occurrences that are text ABOUT a withdrawal, not a live claim.\n" + + "".join(f"{rel}:{pat}\n" for rel, pat in sorted({(h[0], h[2]) for h in hits})), + encoding="utf-8") + print(f" baseline written: {len({(h[0], h[2]) for h in hits})} entries") + return 0 + known = baseline() + new = [h for h in hits if f"{h[0]}:{h[2]}" not in known] + if not new: + print(f"OK: no withdrawn number is stated in a live document " + f"({len(list(live_documents()))} documents scanned)") + return 0 + print(f"FAIL: {len(new)} withdrawn number(s) stated in a live document\n") + for rel, line, pat, reason, where, text in new: + print(f" {rel}:{line}") + print(f" matches /{pat}/ -- {reason}") + print(f" see {where}") + print(f" > {text}") + print("\n Fix the document. If the line is text ABOUT the withdrawal, add it to") + print(" tools/withdrawn_live_baseline.txt with --update-baseline.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/withdrawn.txt b/tools/withdrawn.txt new file mode 100644 index 0000000000..04064be336 --- /dev/null +++ b/tools/withdrawn.txt @@ -0,0 +1,11 @@ +# Withdrawn numbers. A live document that states one of these fails CI. +# +# Format: regex | short reason | where the full analysis lives +# Lines starting with # are comments. The regex is matched case-insensitively. +# +# Add a row the moment a number is withdrawn -- not after the paper is fixed. +# The whole point is that the two events were ten days apart last time. + +323(\.[0-9]+)?\s*MHz | ring-oscillator toggle rate, not a GF16 path | RESEARCH_CLAIMS.md Retraction 2026-08-18 +41\.2\s*GOPS | derived from the withdrawn 323 MHz | RESEARCH_CLAIMS.md Retraction 2026-08-18 +322M\s*matmuls/sec | derived from the withdrawn 323 MHz | RESEARCH_CLAIMS.md Retraction 2026-08-18 diff --git a/tools/withdrawn_live_baseline.txt b/tools/withdrawn_live_baseline.txt new file mode 100644 index 0000000000..26cd96f3eb --- /dev/null +++ b/tools/withdrawn_live_baseline.txt @@ -0,0 +1,5 @@ +# Occurrences that are text ABOUT a withdrawal, not a live claim. +docs/NUMERIC_FORMATS_SSOT.md:323(\.[0-9]+)?\s*MHz +docs/arxiv-trinity-gf16-draft.md:322M\s*matmuls/sec +docs/arxiv-trinity-gf16-draft.md:323(\.[0-9]+)?\s*MHz +docs/arxiv-trinity-gf16-draft.md:41\.2\s*GOPS From 04effa3d896202d9be74d6dab2097001326ba9c7 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Tue, 18 Aug 2026 17:02:14 +0700 Subject: [PATCH 2/2] fix(gate): key the baseline on the line, not just the file The gate fired on its own change: the NOW.md entry this PR adds states 323 MHz while explaining the retraction, and NOW.md is a live document. Baselining (file, pattern) would have silenced it, and would also have silenced every FUTURE occurrence in that file. NOW.md is append-only, so that hole would widen with each entry -- a gate that stops catching is worse than no gate, because it reads as coverage. The baseline is now keyed (path, pattern, sha1 of the normalised line). Text about a withdrawal passes; a new line stating the number does not, in the same file. Editing a baselined line re-opens the gate on it, which is the behaviour we want. Closes #2179 Co-Authored-By: Claude Opus 5 --- tools/check_withdrawn_live.py | 21 +++++++++++++++------ tools/withdrawn_live_baseline.txt | 13 +++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tools/check_withdrawn_live.py b/tools/check_withdrawn_live.py index 73fd2de92b..232309a0ec 100755 --- a/tools/check_withdrawn_live.py +++ b/tools/check_withdrawn_live.py @@ -18,7 +18,10 @@ dated history under docs/reports/ (a record of what was believed then is not a live claim), and this gate's own data baseline tools/withdrawn_live_baseline.txt -- occurrences that are - text ABOUT the withdrawal, listed one per line as path:regex + text ABOUT the withdrawal, keyed by path + pattern + a hash + of the LINE. Keying on the line and not just the file matters + for append-only documents such as docs/NOW.md: baselining the + file wholesale would let a future genuine claim through. Usage: tools/check_withdrawn_live.py gate; exits non-zero on any new hit @@ -28,6 +31,7 @@ Exits non-zero on any failure. """ +import hashlib import pathlib import re import subprocess @@ -88,7 +92,9 @@ def scan(root=ROOT, registry=None): continue for i, line in enumerate(text.splitlines(), 1): if pat.search(line): - hits.append((rel, i, pat.pattern, reason, where, line.strip()[:110])) + key = hashlib.sha1(" ".join(line.split()).encode()).hexdigest()[:12] + hits.append((rel, i, pat.pattern, reason, where, + line.strip()[:110], key)) return hits @@ -125,18 +131,21 @@ def main(): if "--update-baseline" in sys.argv: BASELINE.write_text( "# Occurrences that are text ABOUT a withdrawal, not a live claim.\n" - + "".join(f"{rel}:{pat}\n" for rel, pat in sorted({(h[0], h[2]) for h in hits})), + "# Keyed path | pattern | sha1(line)[:12] -- editing the line re-opens the gate,\n" + "# which is what we want for append-only documents like docs/NOW.md.\n" + + "".join(f"{rel} | {pat} | {key}\n" + for rel, pat, key in sorted({(h[0], h[2], h[6]) for h in hits})), encoding="utf-8") - print(f" baseline written: {len({(h[0], h[2]) for h in hits})} entries") + print(f" baseline written: {len({(h[0], h[2], h[6]) for h in hits})} entries") return 0 known = baseline() - new = [h for h in hits if f"{h[0]}:{h[2]}" not in known] + new = [h for h in hits if f"{h[0]} | {h[2]} | {h[6]}" not in known] if not new: print(f"OK: no withdrawn number is stated in a live document " f"({len(list(live_documents()))} documents scanned)") return 0 print(f"FAIL: {len(new)} withdrawn number(s) stated in a live document\n") - for rel, line, pat, reason, where, text in new: + for rel, line, pat, reason, where, text, _key in new: print(f" {rel}:{line}") print(f" matches /{pat}/ -- {reason}") print(f" see {where}") diff --git a/tools/withdrawn_live_baseline.txt b/tools/withdrawn_live_baseline.txt index 26cd96f3eb..a5a4694561 100644 --- a/tools/withdrawn_live_baseline.txt +++ b/tools/withdrawn_live_baseline.txt @@ -1,5 +1,10 @@ # Occurrences that are text ABOUT a withdrawal, not a live claim. -docs/NUMERIC_FORMATS_SSOT.md:323(\.[0-9]+)?\s*MHz -docs/arxiv-trinity-gf16-draft.md:322M\s*matmuls/sec -docs/arxiv-trinity-gf16-draft.md:323(\.[0-9]+)?\s*MHz -docs/arxiv-trinity-gf16-draft.md:41\.2\s*GOPS +# Keyed path | pattern | sha1(line)[:12] -- editing the line re-opens the gate, +# which is what we want for append-only documents like docs/NOW.md. +docs/NOW.md | 323(\.[0-9]+)?\s*MHz | 18f57dc20b45 +docs/NOW.md | 323(\.[0-9]+)?\s*MHz | 700c6d6aca11 +docs/NOW.md | 323(\.[0-9]+)?\s*MHz | ce84017e739b +docs/NUMERIC_FORMATS_SSOT.md | 323(\.[0-9]+)?\s*MHz | bbc2adf030a9 +docs/arxiv-trinity-gf16-draft.md | 322M\s*matmuls/sec | 1026a341042f +docs/arxiv-trinity-gf16-draft.md | 323(\.[0-9]+)?\s*MHz | 6daf60f68b57 +docs/arxiv-trinity-gf16-draft.md | 41\.2\s*GOPS | 546e04c73288