Skip to content

feat(debt): a Debtfile and a debt runner — measured debt that can only fall - #591

Merged
hyperpolymath merged 7 commits into
mainfrom
feat/debtfile
Aug 7, 2026
Merged

feat(debt): a Debtfile and a debt runner — measured debt that can only fall#591
hyperpolymath merged 7 commits into
mainfrom
feat/debtfile

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Why

A GitHub issue is a boolean. Much of what this estate tracks in issues is an integer that changes every week.

An audit of the 697 open estate issues on 2026-08-07 found what that costs:

Issue Claimed Measured 2026-08-07
standards#309 ~45 banned .py files 176 ~4× wrong, unchallenged for ten weeks
standards#308 ~12,000 PMPL headers 4,968 59% paid down, unrecorded
reposystem#138 Nix in 63 repos 67 grew, unnoticed

The purest case is .git-private-farm, where a bot mints the same measurement as a new issue on every run:

#104 07-05  rustsec: 138 open advisories (34 high/critical)
#105 07-06  rustsec: 136 open advisories (34 high/critical)
#107 07-07  rustsec: 139 open advisories (34 high/critical)
#117 08-01  rustsec: 156 open advisories (54 high/critical)
#119 08-02  rustsec: 156 open advisories (54 high/critical)
#120 08-03  rustsec: 155 open advisories (53 high/critical)

Six issues carrying one number, plus three more for mirror drift. This PR adds a register that holds the integer, so the tracker can go back to holding booleans.

What

File Role
.machine_readable/Debtfile.a2ml the register — same ### <id> block shape as Mustfile.a2ml
scripts/check-debtfile-structure.sh structural half — rejects a hollow entry (a number with no probe)
scripts/run-debtfile.sh executable half — re-measures every probe; --write lowers ceilings
scripts/check-debt-ratchet.sh ceilings may fall, never rise silently
.github/workflows/debt-measure.yml weekly re-measure → opens one PR, never an issue

Why not a contractile

The name fits the convention exactly (Mustfile, Trustfile, Bustfile, Dustfile, Intentfile). The placement does not:

  1. The contractile CLI does not exist. reposystem/contractiles/cli/ is an empty directory; five of six verbs have no runner.
  2. Only must executes, via hand-written bash — the pattern copied here.
  3. EXEMPTION-MECHANISMS.adoc already rejects declarations with no reader (.hypatia-ignore: "never read by anything. Reject."). A seventh verb would be exactly that.
  4. No verb fitsbust is fault-injection drills, dust is exnovation, intend is roadmap.

If the CLI is ever built, this absorbs as the debt verb with no rename.

Why not a sixth exemption mechanism

It absorbs two conventions the estate already invented:

  • debt.a2ml (gitbot-fleet, flat-mate, rpa-elysium, chronicles-of-slavia; ADR-002)
  • Exemption Layer 4b — debt stored as a Markdown table inside .claude/CLAUDE.md, rows \| glob \| count \| rationale \| unblock-condition \|, regex-parsed, in 19 repos

Sibling to the exemption ratchet, governing a different population:

  • check-exemption-ratchet.sh — debt you excused: "did you quietly excuse more?"
  • check-debt-ratchet.sh — debt you measured: "did the number go up?"

Probe discipline — all learned the hard way in this PR

  • No fallback. A probe that fails is a hard failure. A counter returning 0 on error is indistinguishable from zero debt, and zero is the state that passes.
  • Three candidate probes were rejected for manufacturing debt. Two matched comments (usage examples in codeql-reusable.yml; a comment explaining why a script does not use python3); one counted the test files themselves.
  • The register must not count itself. The first live run breached on todo-fixme-markers (278 > 277) and pmpl-licence-headers (590 > 589) purely because writing the entries down created the strings they search for.

Licensing debt is policy: flag-only — measured and reported, never acted on, per the standing owner directive in .claude/CLAUDE.md.

Verification

debtfile-structure-test.sh   14 passed, 0 failed
run-debtfile-test.sh         16 passed, 0 failed
debt-ratchet-test.sh         12 passed, 0 failed

just debt-measure
  ·  HOLDING  [low     ] docs-md-not-adoc              309
  ·  HOLDING  [high    ] gate-scripts-without-tests     31
  ·  HOLDING  [medium  ] shell-scripts-missing-spdx      2
  ·  HOLDING  [low     ] todo-fixme-markers            277
  ·  HOLDING  [medium  ] pmpl-licence-headers          589

Each suite proves its gate can fail and was mutation-tested. The runner suite was itself found to be a fake gate during authoring — piping into expect ran it in a subshell, so counters were discarded and a real failure would still have exited 0 — and fixed.

Workflows re-verified as parsing (debt-measure.yml initially did not: a multi-line git commit -m put continuation lines at column 1 and ended the YAML block scalar — the estate's own broken-workflow class, caught before merge). actions.lock updated; duplicate-key linter clean across 39 files.

Rollout

Wired as job debt-ratchet in governance-reusable.yml, so every caller picks it up on next run with no per-repo PR.

Tier 🟡 CHECK — MUST NOT be a required status check. Promotion to 🔴 GATE only after two clean weekly measure cycles.

Adoption is opt-in: a repo with no Debtfile.a2ml is not in violation; a repo that has one must have a well-formed one.

🤖 Generated with Claude Code

…y fall

A GitHub issue is a boolean. Much of what this estate tracks in issues is an
integer that changes every week. An audit of the 697 open issues on 2026-08-07
found what that costs:

  standards#309   claimed ~45 banned .py files     measured 176   (~4x wrong, 10 weeks)
  standards#308   claimed ~12,000 PMPL headers     measured 4,968 (59% paid, unrecorded)
  reposystem#138  claimed Nix in 63 repos          measured 67    (grew, unnoticed)

The purest case was .git-private-farm, where a bot minted the same measurement
as a NEW ISSUE every run: rustsec 138, 136, 139, 156, 156, 155 — six issues
carrying one number, plus three more for mirror drift.

This adds a register that holds the integer, so the tracker can go back to
holding booleans.

  .machine_readable/Debtfile.a2ml     the register — same ### block shape as
                                      Mustfile.a2ml, so the parsers agree
  scripts/check-debtfile-structure.sh structural half — rejects a HOLLOW ENTRY
                                      (a number with no probe behind it)
  scripts/run-debtfile.sh             executable half — re-measures every probe
  scripts/check-debt-ratchet.sh       ceilings may fall, never rise silently
  .github/workflows/debt-measure.yml  weekly re-measure; opens ONE PR, never an
                                      issue. That is the whole point.

Not a contractile, despite the name fitting the convention exactly. The
contractile CLI does not exist — reposystem/contractiles/cli/ is an empty
directory and five of the six verbs have no runner. Only `must` executes, via
hand-written bash, and that is the pattern copied here.
docs/EXEMPTION-MECHANISMS.adoc already rejects declarations with no reader
("never read by anything. Reject"), which a seventh verb would be.

Not a sixth exemption mechanism either. It absorbs two conventions the estate
already invented: debt.a2ml (4 repos, ADR-002) and Exemption Layer 4b, which
stores debt as a Markdown table inside .claude/CLAUDE.md carrying a count and
an unblock condition, in 19 repos.

Sibling to the exemption ratchet, governing a different population:
  check-exemption-ratchet.sh  debt you EXCUSED  — "did you quietly excuse more?"
  check-debt-ratchet.sh       debt you MEASURED — "did the number go up?"

Deletion is a violation. An entry leaves by reaching zero, not by being
deleted — the same reasoning that rejects `**` in the migration ledger.

Probe discipline, all of it learned the hard way while writing this:

  * NO FALLBACK. A probe that fails is a hard failure. A counter returning 0 on
    error is indistinguishable from zero debt, and zero is the state that passes.
  * Three candidate probes were written and rejected for manufacturing debt —
    two matched COMMENTS (usage examples in codeql-reusable.yml; a comment
    explaining why a script does not use python3), one counted the test files
    themselves.
  * The register must not count itself. The first live run breached on
    todo-fixme-markers (278 > 277) and pmpl-licence-headers (590 > 589) purely
    because writing the entries down created the strings they search for.

Licensing debt is policy: flag-only. Measured and reported, never acted on,
per the standing owner directive in .claude/CLAUDE.md.

Tests prove each gate CAN fail — 14 + 16 + 12 cases, discovered automatically by
self-test.yml, each mutation-tested. The runner suite was itself found to be a
fake gate during authoring (piping into `expect` ran it in a subshell, so
counters were discarded and a failure would still have exited 0) and fixed.

Wired as job `debt-ratchet` in governance-reusable.yml, so every caller picks it
up with no per-repo PR. Tier 🟡 CHECK — MUST NOT be a required status check.
Adoption is opt-in: a repo with no Debtfile is not in violation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gitar-bot

This comment has been minimized.

@gitar-bot
gitar-bot Bot enabled auto-merge (squash) August 7, 2026 11:29
@gitar-bot gitar-bot Bot added the gitar-approved Added by Gitar label Aug 7, 2026
gitar-bot[bot]
gitar-bot Bot previously approved these changes Aug 7, 2026

@gitar-bot gitar-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitar has auto-approved this PR and enabled auto-merge (configure)

The Debt ratchet job failed on the very PR that introduced it.

Root cause is the bootstrap trap: the job sparse-checks-out
hyperpolymath/standards@main to get its scripts, but check-debt-ratchet.sh and
check-debtfile-structure.sh do not exist on main yet — they are what this PR
adds. The `cp` failed and the step exited non-zero. A gate that fetches its own
script from main cannot run on the pull request that introduces that script.
Hypatia's self-gating has the same shape.

When the repository under test IS standards, run the scripts from the PR's own
tree. The guard is on the repository NAME, not on file existence: a consumer
repo that happened to contain a file at scripts/check-debt-ratchet.sh must not
be able to substitute its own gate for the estate's.

Also regenerates .machine_readable/REGISTRY.a2ml + TOPOLOGY.md, which went
stale when this PR added files (33 specs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
REGISTRY.a2ml is GENERATED: conflict resolved by regenerating with 'just registry', never by mixing sides. Hand-mixing a generated file is a known 3-day-red failure in this estate.
pmpl-licence-headers 589->584 (consent-aware-http extracted in #593), todo-fixme-markers 277->276. Ceilings follow the measurement down; nothing raises them.
Brings in the CODEOWNERS co-owner fix (#599). REGISTRY.a2ml is GENERATED — if it conflicted it was regenerated with 'just registry', never hand-merged.
…ount

pmpl-licence-headers was 'git grep -l PMPL-1.0', which counts every file that MENTIONS the licence: policy docs, manifests listing licences, CITATIONS.adoc, and other debt registers discussing this very debt. It does not count files licensed under it.

standards: 587 mentions vs 7 declarations (84x). Estate-wide: 4,952 vs 1,768. The inflated figure is the order of number standards#308 has been quoting for the PMPL->MPL campaign, which is therefore roughly a third the size the issue believes.

A licence is a declaration, so match the declaration. Third instance of the same lesson in this PR; now stated explicitly in DEBTFILE-SPEC.adoc.
@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath disabled auto-merge August 7, 2026 14:39
@hyperpolymath
hyperpolymath merged commit 43ab303 into main Aug 7, 2026
36 of 37 checks passed
@hyperpolymath
hyperpolymath deleted the feat/debtfile branch August 7, 2026 14:39
hyperpolymath added a commit that referenced this pull request Aug 7, 2026
… audit name (#601)

**The rename first, because it was my error.** I added `Debtfile.adoc`
at the repo root in #598. `.machine_readable/Debtfile.a2ml` already
existed from #591 — a probe-backed, monotone-decreasing register with
its own spec, runner, ratchet and 30 tests. Two files a directory apart,
both called Debtfile, meaning different things, is exactly the drift
this canon exists to prevent. The prose one is an **audit** and now says
so: `docs/audits/2026-08-07-estate-cicd-debt.adoc`.

**Then the part that matters.** Deno removal is now a *measured* entry,
not a sentence. DEBTFILE-SPEC's own opening example is what happens
otherwise: `standards#309` claimed ~45 banned `.py` files; measured,
**176** — wrong by 4×, unchallenged for ten weeks.

| entry | probe | count |
|---|---|---:|
| `deno-residue` | workflows/scripts that **invoke** deno | 4 |
| `deno-artefacts` | files existing only to serve Deno | 6 |

⚠ The owner has ruled Deno removed and Bun permanent **three times**,
yet `governance-reusable.yml:264` still runs `denoland/setup-deno` —
**installing Deno on every estate repo on every run** — and
`docs/migrations/npm-to-deno-template/` is a live recipe pointing repos
*at* the retired runtime. That is why the ruling keeps needing
repeating: the canon has been propagating the opposite.

⚠ Both probes deliberately **exclude `*/bindings/deno/`** — interop so
*other people's* Deno code can call estate libraries, the same shape as
the `proven` polyglot exception. "We do not use Deno" and "nobody may
call us from Deno" are different statements; only the first has been
made.

**Verified**: structure OK (7 entries) · both probes HOLDING at 4 and 6
· structure tests 14/14 · runner tests 16/16.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gitar-approved Added by Gitar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant