Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .claude/skills/ci-gates/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 45 additions & 0 deletions .github/workflows/conflict-markers.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
197 changes: 197 additions & 0 deletions tools/check_conflict_markers.py
Original file line number Diff line number Diff line change
@@ -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:]))
8 changes: 8 additions & 0 deletions tools/conflict_markers_baseline.txt
Original file line number Diff line number Diff line change
@@ -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: <path> | 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.
Loading