From 3a1281c29589dda23dc2527274539454b0912210 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 02:39:38 +0700 Subject: [PATCH] Refuse a tracked file that still carries a conflict marker `scripts/verify_all_152.py` -- a verification script by its name, whose docstring promises 152 formulas at 50-digit precision -- carries eight unresolved `Updated upstream` / `Stashed changes` conflicts, two of them nested. `ast.parse` on it is a SyntaxError. It has been that way since f1fb1456b, the commit that introduced it: checked, there is no earlier clean revision to restore. Nothing imports it and nothing runs it. Nothing looked for the shape. A second marker sat in `.claude/skills/ci-gates/SKILL.md` from 2c0ce54e8 (#2574) until #2865 removed it by hand during an unrelated merge. `tri skill check` read that file and reported OK, because it checks section numbering. tools/check_conflict_markers.py 7592 files read, 1 carrying, 60 not read It abstains on a bare seven-equals line: that is an ordinary Markdown rule and this repository has hundreds, while git always writes the divider BETWEEN two LABELLED markers, so the labelled pair alone is sufficient. The gate builds its own patterns from `"<" * 7` rather than literals -- the first draft contained them and refused its own source, which the self-check caught. The workflow carries no `paths:` filter, and the reason is specific rather than inherited: a marker can land in any file, so a filter would be a filter on where the defect may hide. `verify_all_152.py` is recorded in the baseline WITH ITS REASON rather than repaired. Resolving it means choosing which of 152 numeric formulas is right, and that is the owner's call. The gate reports a baseline line that has outlived its debt, so the record cannot quietly become an excuse. Self-check covers six branches including both controls: a clean tree must stay silent, a bare divider must abstain, a planted marker must be seen at the right lines, a baselined file must be counted but not new, a baseline outliving its debt must be reported, and the gate's own source must be clean. Refs #2873, #2756, #2574 Co-Authored-By: Claude Opus 5 --- .claude/skills/ci-gates/SKILL.md | 31 +++ .github/workflows/conflict-markers.yml | 45 ++++ ...amed-verify-all-152-py-has-never-parsed.md | 8 + tools/check_conflict_markers.py | 197 ++++++++++++++++++ tools/conflict_markers_baseline.txt | 8 + 5 files changed, 289 insertions(+) create mode 100644 .github/workflows/conflict-markers.yml create mode 100644 docs/now/2026-08-30-a-script-named-verify-all-152-py-has-never-parsed.md create mode 100644 tools/check_conflict_markers.py create mode 100644 tools/conflict_markers_baseline.txt diff --git a/.claude/skills/ci-gates/SKILL.md b/.claude/skills/ci-gates/SKILL.md index 18178d7d38..5b66199966 100644 --- a/.claude/skills/ci-gates/SKILL.md +++ b/.claude/skills/ci-gates/SKILL.md @@ -7993,3 +7993,34 @@ assert_eq!(k.matches("parse error").count(), 1, "{k}"); And its control — two DIFFERENT causes must not collapse into one bucket — matters more than the merge test. Over-merging turns 39 kinds into 4 and reads as excellent grouping. + +## 316. A verification script that has never parsed + +`scripts/verify_all_152.py` — the name says instrument — carries eight +unresolved `Updated upstream` / `Stashed changes` conflicts, two of them +nested. `ast.parse` on it is a SyntaxError. It has been that way since the +commit that INTRODUCED it, so there is no clean revision to restore, and +nothing in the repository imports or runs it. + +Nothing looked for the shape. A second marker sat in this very skill file for +weeks and was found by hand while resolving an unrelated merge. `tri skill +check` read that file and reported OK, because it checks section numbering. + +Three things worth keeping: + +1. **A file's NAME is a claim.** Anyone scanning the tree sees + `verify_all_152.py` and concludes the 152 formulas are verified. Grep for + what runs an instrument before believing the instrument exists. +2. **Design the abstention first.** The gate refuses a labelled `<<<<<<< x` or + `>>>>>>> x` and says nothing about a bare seven-equals line — that is an + ordinary Markdown rule and this repository has hundreds. Git always writes + the divider BETWEEN two labelled markers, so the pair alone is sufficient + and invents no false positives. +3. **A gate that names marker shapes must not contain one.** Built from + `"<" * 7` rather than a literal, or the gate refuses its own source. The + first draft did exactly that. + +And the fix that was NOT made: resolving the file means choosing which of 152 +numeric formulas is right. A gate may record that debt with its reason; it may +not invent the content. The baseline line says why, and the gate reports when +a baseline entry outlives its debt. diff --git a/.github/workflows/conflict-markers.yml b/.github/workflows/conflict-markers.yml new file mode 100644 index 0000000000..60a21d439d --- /dev/null +++ b/.github/workflows/conflict-markers.yml @@ -0,0 +1,45 @@ +# A tracked file must not carry a merge conflict marker. +# +# `scripts/verify_all_152.py` -- a verification script by its name -- has held +# eight unresolved `Updated upstream` / `Stashed changes` conflicts since the +# commit that introduced it. It has never parsed. A second marker sat in +# `.claude/skills/ci-gates/SKILL.md` for weeks and was found by hand while +# resolving an unrelated merge, not by any check. +# +# No `paths:` filter, and the reason is specific to this gate rather than +# inherited: a marker can land in ANY file, so a filter here would be a filter +# on where the defect is allowed to hide. It is also the conflicting PR -- +# whose merge diff may not compute -- that most needs the reading. +# +# The self-check runs first and its exit code is read. A gate whose own +# failure path is untested is an assertion, not a measurement. +name: Conflict Markers + +on: + pull_request: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: conflict-markers-${{ github.ref }} + cancel-in-progress: true + +jobs: + markers: + runs-on: ubuntu-latest + name: Conflict markers (no unresolved merges) + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: The gate can fail + run: python3 tools/check_conflict_markers.py --self-check + + - name: No tracked file carries a marker + run: python3 tools/check_conflict_markers.py diff --git a/docs/now/2026-08-30-a-script-named-verify-all-152-py-has-never-parsed.md b/docs/now/2026-08-30-a-script-named-verify-all-152-py-has-never-parsed.md new file mode 100644 index 0000000000..ee770c2bc1 --- /dev/null +++ b/docs/now/2026-08-30-a-script-named-verify-all-152-py-has-never-parsed.md @@ -0,0 +1,8 @@ +# NOW -- A script named verify_all_152.py has never parsed (2026-08-30) + +## A script named verify_all_152.py has never parsed (Refs #2873) + +- scripts/verify_all_152.py carries eight unresolved conflict markers, two nested, present since the commit that introduced it -- checked: no earlier clean revision exists. ast.parse is a SyntaxError. Nothing imports or runs it. +- New gate tools/check_conflict_markers.py: 7592 tracked files read, 1 carrying markers, 60 not read and said so. Abstains on a bare seven-equals divider, which is an ordinary Markdown rule here. +- Recorded in tools/conflict_markers_baseline.txt with the reason rather than repaired: resolving it means choosing which of 152 numeric formulas is right, which is not a gate's judgement. +- Workflow has no paths filter on purpose -- a marker can land in any file, and the conflicting PR most needs the reading. diff --git a/tools/check_conflict_markers.py b/tools/check_conflict_markers.py new file mode 100644 index 0000000000..bc1b58afbc --- /dev/null +++ b/tools/check_conflict_markers.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Refuse a tracked file that still carries a merge conflict marker. + +WHY THIS EXISTS +--------------- +`scripts/verify_all_152.py` -- a verification script, by its name -- has +carried eight unresolved `Updated upstream` / `Stashed changes` conflicts +since the commit that introduced it (f1fb1456b). It has never parsed. Nothing +imports it, nothing runs it, and no check in this repository looks for the +shape. A second one sat in `.claude/skills/ci-gates/SKILL.md` for weeks and +was found by hand while resolving an unrelated merge. + +A conflict marker is not a style question. In Python it is a SyntaxError; in +Markdown it is two contradictory paragraphs presented as one; in YAML it is a +workflow that will not load. The file is broken in every case, and the shape +is unambiguous enough to be checked in one pass. + +WHAT IT ABSTAINS ON +------------------- +A bare `=======` line. Seven equals signs with nothing after them is a common +Markdown rule and a common ASCII divider, and this repository has hundreds. +Git always writes it BETWEEN an opening and a closing marker, both of which +carry a label, so refusing on those two alone loses nothing and invents no +false positives. +""" +import subprocess +import sys +from pathlib import Path + +# Built from pieces on purpose: a literal marker in this file would make the +# gate refuse its own source, which is the trap the first draft fell into. +OPEN = "<" * 7 + " " +CLOSE = ">" * 7 + " " + +BASELINE = Path("tools/conflict_markers_baseline.txt") + +SKIP_SUFFIX = {".lock", ".png", ".jpg", ".gif", ".pdf", ".bin", ".bit", ".fasm"} + + +def tracked_files(root: Path): + out = subprocess.run( + ["git", "ls-files", "-z"], cwd=root, capture_output=True, text=True + ) + for name in out.stdout.split("\0"): + if not name: + continue + p = root / name + if p.suffix in SKIP_SUFFIX or not p.is_file(): + continue + yield name, p + + +def markers_in(path: Path): + """Line numbers carrying an opening or closing marker, or None if unread.""" + try: + text = path.read_text(encoding="utf-8") + except (UnicodeDecodeError, OSError): + return None # binary or unreadable: not this gate's question + hits = [] + for n, line in enumerate(text.splitlines(), 1): + if line.startswith(OPEN) or line.startswith(CLOSE): + hits.append(n) + return hits + + +def load_baseline(root: Path): + f = root / BASELINE + if not f.is_file(): + return set() + known = set() + for line in f.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#"): + known.add(line.split("|")[0].strip()) + return known + + +def scan(root: Path): + known = load_baseline(root) + found, unread, stale = {}, 0, [] + for name, p in tracked_files(root): + hits = markers_in(p) + if hits is None: + unread += 1 + continue + if hits: + found[name] = hits + for name in sorted(known): + if name not in found: + stale.append(name) + return found, known, unread, stale + + +def main(argv): + root = Path(__file__).resolve().parent.parent + if "--self-check" in argv: + return self_check(root) + + found, known, unread, stale = scan(root) + new = {k: v for k, v in found.items() if k not in known} + + print(f" tracked files read {sum(1 for _ in tracked_files(root)) - unread}") + print(f" carrying a conflict marker {len(found)}") + if known: + print(f" ... of those, known debt {len(found) - len(new)}") + if unread: + print(f" NOT READ, nothing claimed {unread}") + + for name in sorted(stale): + print() + print(f" {name} is in the baseline and is CLEAN now.") + print(" Remove its line: a baseline that outlives its debt starts") + print(" excusing a defect nobody has re-introduced yet.") + + if not new: + print() + if found: + print(" Every marker found is recorded as debt. Nothing new.") + else: + print(" No tracked file carries a conflict marker.") + return 1 if stale else 0 + + for name, lines in sorted(new.items()): + shown = ", ".join(str(n) for n in lines[:6]) + more = "" if len(lines) <= 6 else f" (+{len(lines) - 6} more)" + print() + print(f" {name}") + print(f" conflict marker on line {shown}{more}") + print() + print(" A conflict marker is a broken file, not a formatting question.") + print(" Resolve it, or -- if the content cannot be judged -- record it in") + print(f" {BASELINE} with the reason, so the debt is named rather than green.") + return 1 + + +def self_check(root: Path): + """Plant a marker, demand it is seen; remove it, demand it is not.""" + import tempfile + + ok = True + with tempfile.TemporaryDirectory() as d: + t = Path(d) + subprocess.run(["git", "init", "-q"], cwd=t, check=True) + (t / "a.py").write_text("x = 1\n") + (t / "b.md").write_text("Title\n" + "=" * 7 + "\n\nbody\n") + subprocess.run(["git", "add", "-A"], cwd=t, check=True) + + found, _, _, _ = scan(t) + clean = not found + print(f" clean tree {'no marker seen' if clean else 'FALSE POSITIVE'}") + ok &= clean + + # A bare ======= divider must NOT fire: it is the abstention. + bare = "b.md" not in found + print(f" bare {'=' * 7} divider {'abstained' if bare else 'FALSE POSITIVE'}") + ok &= bare + + (t / "a.py").write_text(f"x = 1\n{OPEN}HEAD\ny = 2\n" + "=" * 7 + f"\ny = 3\n{CLOSE}other\n") + subprocess.run(["git", "add", "-A"], cwd=t, check=True) + found, _, _, _ = scan(t) + # The closing marker is line 6, not 5: the planted text has a + # divider between the two halves. Counted wrong on the first run, + # and the self-check is what said so. + seen = found.get("a.py") == [2, 6] + print(f" planted marker {'seen at 2 and 6' if seen else f'MISSED: {found}'}") + ok &= seen + + # A baselined file must fall out of the NEW set but stay counted. + (t / "tools").mkdir() + (t / BASELINE).write_text("# debt\na.py | never parsed\n") + subprocess.run(["git", "add", "-A"], cwd=t, check=True) + found, known, _, stale = scan(t) + excused = "a.py" in found and "a.py" in known and not stale + print(f" baselined {'counted, not new' if excused else 'BASELINE BROKEN'}") + ok &= excused + + # And a baseline that outlived its debt must be reported. + (t / "a.py").write_text("x = 1\n") + subprocess.run(["git", "add", "-A"], cwd=t, check=True) + _, _, _, stale = scan(t) + caught = stale == ["a.py"] + print(f" baseline outlived its debt {'reported' if caught else f'MISSED: {stale}'}") + ok &= caught + + # This file names the marker shapes without containing one. + src = Path(__file__).read_text() + selfclean = not any(l.startswith(OPEN) or l.startswith(CLOSE) for l in src.splitlines()) + print(f" the gate's own source {'clean' if selfclean else 'TRIPS ITSELF'}") + ok &= selfclean + + print() + print(" self-check PASSED" if ok else " self-check FAILED") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tools/conflict_markers_baseline.txt b/tools/conflict_markers_baseline.txt new file mode 100644 index 0000000000..e08526c26e --- /dev/null +++ b/tools/conflict_markers_baseline.txt @@ -0,0 +1,8 @@ +# Tracked files that still carry a merge conflict marker. +# +# Each line is a broken file, not an accepted style. Remove the line when the +# file is resolved; the gate then holds it resolved, and says so if the line +# outlives the debt. +# +# Format: | why it is here rather than fixed +scripts/verify_all_152.py | eight conflicts, some nested, present since the commit that introduced the file (f1fb1456b) -- it has never parsed, so there is no clean revision to restore. Nothing imports or runs it. Choosing a side would be inventing which of 152 numeric formulas is right; that is the owner's call, not a gate's.