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
54 changes: 54 additions & 0 deletions .claude/skills/ci-gates/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7734,3 +7734,57 @@ shape, and the first where the reading is the whole safety argument.
All three are in my own notes with names. **Knowing a trap and recognising it in
your own output are different skills**, and the second one only comes from
reading the output — which is what the note should say and did not.

## 270. Six red checks on six pull requests, all six merged

Before adding anything, the question worth asking was whether the signal had
ever reached the author. It had:

#2841 coverage=fail #2849 coverage=fail
#2844 coverage=fail #2856 coverage=fail
#2845 coverage=fail #2859 coverage=fail

Six emitter PRs, the seal gate red **on the pull request itself**, every one
merged. So the barrier was not knowledge, not tooling, and not timing — the
check fired in the right place at the right time, six times.

The output is why. The legend explained `stale`, `dangling` and `phantom`. The
kind that fired was **`gen-drift`, which had no entry**, and the only repair the
page named was `--update-baseline` — which for that kind records the drift as
accepted debt instead of recording what the compiler now produces. **The one
actionable line on the page was the wrong action.**

**A check nobody acts on may be a check nobody can act on.** Read the failure
output as the author sees it before concluding they ignored it.

## 271. Three kinds, five kinds, eight kinds

The fix looked like two legend lines. Instead the legend became data and a
`--self-check` guard read the **source** for every kind it can attach:

legend covers 5 of 8 kind(s) MISSING: no-spec-hash, no-spec-path, unreadable

Five, not five-of-five: the guard found **three more kinds** I had not seen
while writing the fix for the two I had. The script attaches eight; the legend
explained three.

Second guard this session to earn itself on its first execution, after
`tri types classified` reporting `HealthStatus` UNJUDGED.

**Count the cases from the code, never from the reading that motivated you.** I
grepped for the kind that was failing and found two; the source knew about
eight.

## 272. A legend that lists what did not happen

While making the legend data, it also became **selective**: only the kinds
actually present in this run are printed.

Before, an author with 134 `gen-drift` rows read three paragraphs about stale,
dangling and phantom seals — none of them theirs. Now they get one paragraph,
which is theirs, and the command that fixes it.

Controlled both ways: removing a kind's entry gives `MISSING: gen-drift` and
exit 1; adding an entry nothing attaches gives `UNREACHABLE: invented-kind` and
exit 1. **An explanation for a state that cannot occur is the same defect as a
state with no explanation** — one wastes the reader, the other strands them.
25 changes: 25 additions & 0 deletions docs/now/2026-08-30-a-legend-that-omits-a-kind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# NOW -- A legend that omits a kind (2026-08-30)

## A legend that omits a kind (Refs #2851)

- Six emitter pull requests carried a RED seal-coverage check **on the pull
request itself** -- #2841, #2844, #2845, #2849, #2856, #2859 -- and all six
merged. The signal reached the author six times out of six.
- **The output is why.** The legend explained `stale`, `dangling` and `phantom`.
The kind that fired was `gen-drift`, which had NO entry, and the only repair
the page named was `--update-baseline` -- which for that kind records the drift
as accepted debt instead of recording what the compiler now produces. The one
actionable line was the wrong action.
- The legend is now data, and `--self-check` reads THIS FILE'S SOURCE for every
kind it can attach. Its first run: `legend covers 5 of 8 kind(s) MISSING:
no-spec-hash, no-spec-path, unreadable` -- **three kinds beyond the two I was
fixing.** The script attaches eight; the legend explained three.
- It is also SELECTIVE now: only the kinds present in this run print. An author
with 134 `gen-drift` rows used to read three paragraphs about stale, dangling
and phantom seals, none of them theirs.
- `gen-drift` names `tri seals drift --fix` and says to read `t27c corpus` first,
because re-sealing is a statement that the new output is the one you want.
- Controls both directions: removing a kind's entry gives `MISSING: gen-drift`
and exit 1; adding an entry nothing attaches gives `UNREACHABLE` and exit 1; a
planted drift prints the new paragraph and none of the others.
- ci-gates 270-272.
104 changes: 96 additions & 8 deletions tools/check_seal_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,63 @@ def baseline():
# because the ledger was written when CI ran on a depth-1 clone, and read
# `dangling` now that #2445 gave it history. That is the instrument being
# fixed, not the tree changing.
# Every kind `scan` can attach, and what to do about it.
#
# W721: this used to be three `print` calls covering stale, dangling and
# phantom. The script can attach FIVE kinds, and the two it did not explain --
# `gen-drift` and `gen-unreadable` -- are the ones an emitter change produces.
# Measured: six consecutive gen-c pull requests carried a red `coverage` check
# for `gen-drift`, all six merged, and the only repair the output named was
# `--update-baseline`, which for that kind is exactly wrong.
#
# `KINDS_EXPLAINED` is checked against the kinds the source can emit, so a new
# kind cannot be added without a line telling its reader what to do.
LEGEND = {
"stale": [
"\n stale the spec changed after sealing, so the four gen_hashes describe",
" something it no longer produces. Re-seal it:",
" t27c seal <spec> --save && tri seals sync-twins",
],
"dangling": [
"\n dangling the spec was committed and later deleted. Remove the seal with it,",
" or restore both.",
],
"phantom": [
"\n phantom the spec appears in NO commit. The seal's spec_hash and four",
" gen_hashes name a file nobody can fetch, so there is nothing in",
" the record to check. Find the spec or drop the seal.",
],
"gen-drift": [
"\n gen-drift the spec is UNCHANGED and its generated output is not. This is",
" what an emitter fix looks like from here, and it is normal --",
" the seals simply have not been told yet:",
" tri seals drift --fix",
" Re-sealing is a STATEMENT that the new output is the one you",
" want, so read the acceptance columns first:",
" t27c corpus",
],
"unreadable": [
"\n unreadable the seal file is not parseable JSON. Nothing in it was read,",
" so nothing about the spec is claimed either way. Fix the file",
" or delete it -- a seal nobody can parse records nothing.",
],
"no-spec-path": [
"\n no-spec-path the seal does not say which spec it describes. Its five",
" hashes are unattributable, so no spec work can ever retire it.",
" Name the spec or drop the seal.",
],
"no-spec-hash": [
"\n no-spec-hash `spec_hash` is not a sha256 digest, so the seal never",
" described the spec at any commit. A permanent floor wearing",
" the label of work someone could do. Re-seal or drop it.",
],
"gen-unreadable": [
"\n gen-unreadable `t27c seal <spec>` did not succeed, so nothing was compared.",
" Not a seal problem: the spec does not get as far as generating.",
" Run that command and read its error.",
],
}

_HISTORY_PAIR = {"phantom", "dangling"}


Expand Down Expand Up @@ -511,7 +568,39 @@ def spawned(label, want_exit, present, absent, seals, ledger=None, args=()):
("FAIL:", "OK:", DRIFT, CHANGED, DEPARTED),
ONE_STALE, args=("--update-baseline",))

return 0 if (ok and _check_compare()) else 1
return 0 if (ok and _check_compare() and _check_legend()) else 1


def _check_legend():
"""Every kind this file can ATTACH has a line telling its reader what to do.

W721: `scan` could attach five kinds and the printed legend explained three.
The two it skipped -- `gen-drift` and `gen-unreadable` -- are the ones an
emitter change produces, and six consecutive gen-c pull requests carried a
red check whose only named repair (`--update-baseline`) was wrong for their
kind. All six merged.

Read from THIS FILE's source, so a sixth kind cannot be added without a
reader being told what it means.
"""
import re

src = pathlib.Path(__file__).read_text()
attached = set(re.findall(r'bad\.append\(\s*\(\s*name\s*,\s*"([a-z-]+)"', src))
attached |= set(re.findall(r'bad\.append\(\s*\(\s*name\s*,\s*"([a-z-]+)" if ', src))
# `dangling`/`phantom` share one append written as a conditional.
attached |= {"dangling", "phantom"}
missing = sorted(attached - set(LEGEND))
extra = sorted(set(LEGEND) - attached)
print(f" legend covers {len(attached & set(LEGEND))} of {len(attached)} kind(s)"
f"{'' if not missing else ' MISSING: ' + ', '.join(missing)}"
f"{'' if not extra else ' UNREACHABLE: ' + ', '.join(extra)}")
if missing:
print(" A kind with no legend line is a verdict its reader cannot act on.")
if extra:
print(" A legend line for a kind nothing attaches describes a state that")
print(" cannot occur -- delete it or the code that stopped producing it.")
return not missing and not extra


def main():
Expand Down Expand Up @@ -584,14 +673,13 @@ def main():
for n, k, d in new:
print(f" {n} [{k}]")
print(f" {d}")
print("\n stale the spec changed after sealing, so the four gen_hashes describe")
print(" something it no longer produces. Re-seal it.")
print(" dangling the spec was committed and later deleted. Remove the seal with it,")
print(" or restore both.")
print(" phantom the spec appears in NO commit. The seal's spec_hash and four")
print(" gen_hashes name a file nobody can fetch, so there is nothing in")
print(" the record to check. Find the spec or drop the seal.")
for kind in sorted({k for _, k, _ in new}):
for line in LEGEND[kind]:
print(line)
print(f"\n Deliberate debt goes in {BASELINE.name} via --update-baseline.")
print(" That is the WRONG repair for gen-drift: baselining it records the")
print(" drift as accepted debt instead of recording what the compiler now")
print(" produces.")
return 1


Expand Down
Loading