Skip to content

feat(gate): fail on stale launcher-standard refs (standards#960 AC4) - #983

Merged
hyperpolymath merged 2 commits into
mainfrom
fix/960-launcher-standard-drift
Sep 22, 2026
Merged

hyperpolymath merged 2 commits into
mainfrom
fix/960-launcher-standard-drift

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Closes two of the four acceptance criteria on #960, and corrects three claims in the mapping spec that measurement has since falsified.

AC4 — the check (815531de)

scripts/check-launcher-standard-currency.sh (100755, 9,987 B) fails a repo that claims launcher-standard compliance while naming a filename or version that no longer exists upstream. #952 (be6c9580, 2026-09-22) deleted launcher/launcher-standard.a2ml and replaced it with launcher/launcher-standard_praxis.deed at v0.4.0; every downstream citation became dangling, not merely stale.

The two defect classes fail independently, which is the point — a repo that updated the version but not the filename is still broken:

filename version verdict
launcher-standard.a2ml v0.3.0 FAIL (both)
launcher-standard.a2ml v0.4.0 FAIL — filename alone
launcher-standard_praxis.deed v0.3.0 FAIL — version alone
launcher-standard_praxis.deed v0.4.0 PASS

Three design choices worth stating, because each has a tempting wrong answer:

  • The expected version is not magic. CURRENT_VERSION="0.4.0" is a named constant, and the test suite asserts it equals the :standard-version in the real deed. A built-in default is fine for a consumer; this repo owns the standard, so its own test has to close the drift loop.
  • It reads :standard-version, never :schema-version. A deed carries two versions — the DEED grammar (1.0.0) and the document (0.4.0). Reading the first yields a number that looks like a newer spec, so the error would read as an upgrade rather than as drift.
  • The historical-record exemption is a PATH allowlist, not a content heuristic. A heuristic sparing lines containing "formerly" or "translated from" would spare the live defect too. The allowlist was derived by measuring this repo's tree, not guessed: exactly four legitimately-historical paths, plus the standard itself and the two new script paths.

Proof: 19 assertions across 7 sections, 19/19 passing. Run against metadatastician/stapeln it finds 4 real defects; against a clean tree, zero. The anti-drift mutant (lowering CURRENT_VERSION to 0.3.0) produces 2 failures, not 1 — it trips both the anti-drift assertion and the gate's own --self-test, whose stale-version fixture then reads as current. Both are coherent and expected.

The mapping-spec corrections (66eb76b9)

Three claims in 1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc were true when written and are false now. Each is withdrawn in place and dated, rather than silently edited — a spec that quietly changes its mind teaches the next reader to distrust the parts that did not change.

  1. "There is no Rust .deed parser in the estate" — false since D73-C part 2. launcher-common/src/deed.rs is a purpose-built recursive-descent DEED parser, 37,039 B, pub mod deed; at lib.rs:23. This claim had propagated into a memory file and into this campaign's notes; both are corrected.
  2. "10 downstream *.launcher.a2ml files" — an undercount by more than 5×. Re-measured today: 23 distinct descriptors across 52 on-disk paths, 39 of those paths citing the deleted file. Written as a dated measurement, not a standing fact, because it moves every time a repo is cloned twice. (D73-C downstream: 21 launcher descriptors + trigger/ still name the deleted launcher-standard.a2ml #960's "21" is correct for the question it asked — it counted app names; two of the 23 are set-level.)
  3. "the vendored launch-scaffolder copy is still v0.2.0, still missing [resolution], still .a2ml" — false in all three parts. diff -q against canon reports no difference: 13,803 B at :standard-version "0.4.0".

Status, stated honestly (AGENTS.md §6)

The gate is implemented and tested. It is not wired: no workflow names it. It is executed on every PR here, because scripts/run-shell-test-suite.sh discovers scripts/tests/*.sh by glob and invokes bash "$f" — which also makes the test file's 100644 mode safe, rather than an exit-126 trap waiting in CI.

No consumer repo invokes it, deliberately. Wiring it into the 412 consumers would arm a new scan across all of them on their next pin bump. That is an owner decision, and it is recorded on #960 rather than smuggled in here.

Not in scope

docs/UX-standards/launcher-standard.adoc declares no version, yet two files cite it at v0.2.0 (docs/audits/launcher-standard-review-2026-05-26.adoc:33, docs/UX-standards/LM-LA-LIFECYCLE-STANDARD.adoc:61). A separate defect, deliberately outside this gate.

Refs #960 · #952 · #837 · D73-C in #787

🤖 Generated with Claude Code

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

hyperpolymath and others added 2 commits September 22, 2026 13:54
standards#960 acceptance criterion 4.

launcher/launcher-standard.a2ml was deleted on 2026-09-22 (#952,
be6c958) and replaced by launcher/launcher-standard_praxis.deed at
:standard-version 0.4.0. Nothing told the downstream repos. A census
today finds 21 distinct *.launcher.a2ml descriptors (36 on-disk copies)
plus trigger/scripts/trigger-launcher.sh still declaring compliance with
"launcher-standard.a2ml v0.3.0" -- a filename that is gone and a version
that no longer exists. Both claims read as compliance; neither is
checkable. That is the vacuous-gate pattern the estate rules against.

The two defects fail INDEPENDENTLY:

  launcher-standard.a2ml         any version -> FAIL (retired filename)
  launcher-standard_praxis.deed  v0.3.0      -> FAIL (stale version)
  launcher-standard.a2ml         v0.4.0      -> FAIL (filename only)
  launcher-standard_praxis.deed  v0.4.0      -> PASS

Three things this gate does deliberately:

1. The expected version is not a magic number. CURRENT_VERSION is a
   default for consumers, who do not carry the standard; the test
   asserts it equals :standard-version in the real deed, so bumping the
   standard without updating the gate turns standards' own CI red. A
   checker whose expected value can drift from the thing it checks is
   worse than no checker.

2. It reads :standard-version, never :schema-version. A deed carries
   two versions -- the grammar (1.0.0) and the document (0.4.0).
   Reading the first yields a number that looks like a NEWER spec, so
   the error reads as an upgrade rather than as drift. A fixture makes
   the two impossible to confuse.

3. AC1's "dated historical record" exemption is a PATH allowlist, not a
   content heuristic. A heuristic sparing lines that say "formerly" or
   "translated from" would spare the live defect too, because that is
   how a stale descriptor comment is worded. standards' own tree really
   does name the retired file in four places (the conversion mapping
   doc, two dated audits, the deed's own ;; provenance header); the
   allowlist absorbs exactly those.

Proved, not assumed:
- 19/19 in the fixture suite.
- Real defect killed: run against metadatastician/stapeln it reports 4
  defects, with both classes firing independently on line 5.
- Anti-drift mutant killed: setting CURRENT_VERSION=0.3.0 turns the
  suite red (2 failures) rather than passing quietly.
- Negative control: standards' own tree is clean under the allowlist.

No workflow wiring is added. scripts/run-shell-test-suite.sh discovers
by glob (find scripts/tests -maxdepth 1 -name '*.sh') and invokes via
bash "$f", so the suite runs on every PR already and is immune to the
exit-126 non-executable trap. Grepping .github/workflows for the
filename finds nothing and is the wrong way to ask whether it runs.

Wiring the gate into consumer repos is deliberately NOT done here: it
would arm a new scan across 412 consumers on their next pin bump. That
is an owner decision, recorded on #960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo
All three were true when written and are false now. Each is withdrawn in
place, dated, with the measurement that replaced it -- rather than silently
edited -- because a spec that quietly changes its mind teaches the next
reader to distrust the parts that did not change.

1. §7 item 1: "There is no Rust `.deed` parser in the estate."
   FALSE since D73-C part 2. `crates/launcher-common/src/deed.rs` is a
   purpose-built recursive-descent DEED parser, 37,039 B, exported as
   `pub mod deed;` (lib.rs:23), with `pub mod standard;` (lib.rs:30) on top
   of it. The same item's `[resolution]`-omission defect is closed too: the
   vendored copy is byte-identical to canon. What remains true is the reason
   `deed-ecosystem/rs` was not the starting point -- it is still the
   unmigrated A2ML crate, and still not to be touched (deed-ecosystem#67).

2. §7 out-of-scope: "10 downstream `*.launcher.a2ml` files."
   An undercount by more than 5x. Re-measured today with the vendored
   ripgrep (the only estate-wide sweep that completes), excluding both
   `.claude/worktrees/` and `developer/worktrees/` so the sweep does not
   count this session's own worktrees: 23 distinct descriptors across 52
   on-disk paths, 39 of those paths citing the deleted file. Two of the 23
   (`_DATABASE`, `_ESOTERIC_GROUP`) are set-level, not per-application,
   which is why an app-name census returns 21 -- #960's figure, and correct
   for the question it asked. The count is written as a DATED measurement,
   not a standing fact, because it moves every time a repo is cloned twice.

3. §8: the vendored launch-scaffolder copy is "still v0.2.0, still missing
   `[resolution]`, and still `.a2ml`."
   False in all three parts. `diff -q` against the canonical file reports no
   difference: 13,803 B, `:standard-version "0.4.0"`, `:standard-date
   "2026-09-22"`. Withdrawn rather than deleted, since the claim was quoted
   forward into this campaign's notes and a reader needs to see it retracted.

This file is on the AC4 gate's path allowlist
(`1-formats/deed/mappings/*`), so the retired filename quoted above does not
trip it. That is the allowlist working as designed: a historical-record
exemption has to be a path allowlist, because a content heuristic sparing
lines that say "formerly" would spare the live defect too.

Refs: #960, #837

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 20 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 790e9cf4-7d05-486d-ace8-c3b47d44fa90

📥 Commits

Reviewing files that changed from the base of the PR and between 79991c7 and 66eb76b.

📒 Files selected for processing (3)
  • 1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc
  • scripts/check-launcher-standard-currency.sh
  • scripts/tests/check-launcher-standard-currency-test.sh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit edac0d6 into main Sep 22, 2026
47 checks passed
@hyperpolymath
hyperpolymath deleted the fix/960-launcher-standard-drift branch September 22, 2026 13:13
hyperpolymath added a commit to hyperpolymath/trigger that referenced this pull request Sep 22, 2026
…14)

19 citations in this repo named `launcher-standard.a2ml v0.3.0`. That
file was **deleted upstream** on 2026-09-22 by
hyperpolymath/standards#952 (`be6c9580`, owner ruling D73-C) and
translated to `launcher/launcher-standard_praxis.deed`. Every one of
these pointed at a path that no longer resolves.

This is AC3 of hyperpolymath/standards#960.

## Why the v0.4.0 claim is earned, not assumed

Rewriting `v0.3.0` → `v0.4.0` is a **compliance claim**, not a rename,
so it had to be checked rather than swept. The deed states its own delta
at lines 22–24:

> Bumped 0.3.0 -> 0.4.0 because the resolution ladders below now name
the .deed file, which is consumer-visible.

That delta lands entirely in `(resolution (standard-search` — the ladder
a consumer uses to **locate the standard at runtime**. This launcher
never reads the standard at runtime: it inlines the `(resolution
(desktop-tools-search` ladder, whose six priorities match the deed's in
the same order and are untouched by 0.4.0. So nothing 0.4.0 added is
unimplemented here. If that had not held, the honest move would have
been to name 0.4.0 **and record the gap** — it did hold.

## Why this is safe

Comments only, with one deliberate exception: the `--version` output
prints a `Standards Compliance:` line, rewritten because a compliance
claim has to name the standard that exists. The deed's `(version-output`
clause constrains only `:first-line-format`, which is untouched and
still conforms.

`bash -n` passes. The launcher was **not executed** — it `source`s
`keepopen.sh` unconditionally at top level.

**No consumer breaks.** `launch-scaffolder`'s metadata parser keys on a
literal `# @a2ml-metadata begin` delimiter (`metadata_block.rs:109`);
this launcher has no such line and is not parseable by it today, before
or after this change. All eleven fields the deed's `(metadata-block
:required-fields` demands are present and untouched.

## Clause notation

`§x` → `(x ...)`, the deed's s-expression heads. Three were not
mechanical and were each verified against the deed rather than guessed:

| was | now | why |
|---|---|---|
| `§a2ml-metadata-block` | `(metadata-block ...)` | the deed's only
clause rename, authorised by its own comment: *"A2ML is a dead format
name and the deed era does not carry it forward"* |
| `§disinteg.remove` | `(disinteg :remove ...)` | `:remove` is a **key**
on `(disinteg`, not a subclause — deed:228 |
| "ladder from launcher-standard.a2ml" | `(resolution
(desktop-tools-search ...))` | the deed has **two** ladders; this one
resolves `.desktop-tools`, so it is the first, not `(fallback-ladder` |

The old filename is deliberately **not** carried as a "formerly …"
gloss. The #960 AC4 gate flags a retired filename at any version, by
design — the history belongs in the commit message, not in the source.

## Proved

The AC4 gate (`scripts/check-launcher-standard-currency.sh`,
hyperpolymath/standards#983) run against this worktree:

- **before:** 23 defect rows over 19 lines, rc=1 — four lines carry
*both* a retired-filename and a stale-version defect, which is why 19
lines yield 23 rows
- **after:** `No stale launcher-standard references.`, rc=0

This is also the gate's first run against a hand-written launcher rather
than a generated descriptor.

## What is deliberately left alone

`dev-notes/HANDOVER.adoc` keeps its two v0.3.0 citations. It carries
`:revdate: 2026-08-13` on its face and is a historical record; the gate
allowlists it by path for exactly that reason. Rewriting a handover to
name a file that did not exist at handover time would falsify the
record.

Refs hyperpolymath/standards#960 · hyperpolymath/standards#952 ·
hyperpolymath/standards#983

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@hyperpolymath

Copy link
Copy Markdown
Owner Author

⚠ Correction to point 2 of this PR's description

This PR's description says:

  1. "10 downstream *.launcher.a2ml files" — an undercount by more than 5×. Re-measured today: 23 distinct descriptors across 52 on-disk paths, 39 of those paths citing the deleted file. … (D73-C downstream: 21 launcher descriptors + trigger/ still name the deleted launcher-standard.a2ml #960's "21" is correct for the question it asked — it counted app names; two of the 23 are set-level.)

That is wrong, and the parenthetical explaining away the difference from #960 was a rationalisation of a control that was correctly firing.

The sweep behind it had been run from developer/ with no scope restriction, so it counted two populations that are not live repositories: 9 paths under archive/ quarantine snapshots and 7 under tools/opt/cargo/git/checkouts/ (burble ×5, gossamer ×2). 52 − 16 = 36. And 10 → 21 is roughly 2×, not "more than 5×".

#960's figure of 21 was right. The 23 was mine and it was wrong.

Corrected census, scoped to hyper-repos and meta-repos:

metric value
*.launcher.a2ml paths on disk 36
distinct descriptor basenames 21
paths citing the deleted launcher-standard.a2ml 29
distinct descriptors citing it 17
distinct upstream repositories affected 18
citing lines, all # comments, in six shapes 71

Nothing merged here depends on the figure — the gate and its 19 controls are unaffected, and both re-run clean against origin/main. The wrong number reached main inside §7 of the mapping doc, and #984 corrects it there in place and dated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

@hyperpolymath

Copy link
Copy Markdown
Owner Author

⚠ Follow-up: the path counts in my comment above were also slightly wrong

The substantive claim stands — #960's 21 was right and my 23 was wrong — but the re-measurement I posted (36 paths / 29 citing / 71 lines) was itself made with a filter that silently did nothing.

A ripgrep -g '!.claude/worktrees/**' exclude only matches at the search root, so it excluded nothing where worktrees are nested inside repos. No error, no warning; it reads as scoping and supplies none. The working form is -g '!**/.claude/worktrees/**'.

metric posted above actual
paths on disk 36 31
distinct descriptor basenames 21 21
citing paths 29 26
distinct descriptors citing 17 17
distinct upstream repos 18 18
citing lines 71 66

Only the path counts moved. The distinct counts did not — which is exactly what removing duplicates should do, and is the reason the decision-relevant figures (21 descriptors, 17 citing, 18 repos) were sound all along. #984 carries both corrections.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

hyperpolymath added a commit that referenced this pull request Sep 22, 2026
Corrects a number **I wrote and merged** in `edac0d6a` (#983,
standards#960 AC4) — and then corrects that correction, because the
first re-measurement was made with a silently-broken filter.

## What was wrong on `main`

§7 of `1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc`
recorded:

> an undercount by more than 5×… **23 distinct descriptors across 52
on-disk paths**… **39 of those paths cite the deleted
`launcher-standard.a2ml`**

and explained the disagreement with `standards#960`'s own figure of 21
by asserting that two set-level directories (`_DATABASE`,
`_ESOTERIC_GROUP`) inflate an app-name census.

**All three numbers were wrong, the multiplier was wrong, and the
explanation was a rationalisation of a control that was correctly
firing.** #960's 21 was right; the 23 in this document was wrong.

## Why

The sweep had been run from `developer/` with no scope restriction, and
its worktree exclusion did not work. Three populations were counted that
are not live repositories:

| counted, not a live repo | paths |
|---|---|
| `archive/` quarantine snapshots | 9 |
| `tools/opt/cargo/git/checkouts/` (`burble` ×5, `gossamer` ×2) | 7 |
| nested `.claude/worktrees/` copies | 5 |

52 − 21 = **31**. And 10 → 21 is roughly **2×**, not "more than 5×".

## ⚠ The trap worth carrying: a `-g` exclude without `**/` fails
silently

`-g '!.claude/worktrees/**'` looks right, runs without error, and
**excludes nothing** where worktrees are nested inside repos — a ripgrep
glob only matches at the *search root*. It reads as scoping and supplies
none, the same class of defect as a vacuous gate. The working form is
`-g '!**/.claude/worktrees/**'`, with a positive control asserted on the
filter's own output.

## The corrected census

Scoped to `hyper-repos` and `meta-repos`, nested worktrees verified
absent from the result:

| metric | value |
|---|---|
| `*.launcher.a2ml` paths on disk | 31 |
| **distinct descriptor basenames** | **21** (confirms #960) |
| paths citing the deleted `launcher-standard.a2ml` | 26 |
| distinct descriptors citing it | 17 |
| **distinct upstream repositories affected** | **18** |
| citing lines, all `#` comments, in six shapes | 66 |

**The invariant that says which figures to trust:** fixing the exclusion
moved the path counts (36→31, 29→26, 71→66) and left **21 / 17 / 18
unchanged** — exactly what removing duplicates should do. The distinct
counts were sound throughout; only the path counts were ever inflated.
The document now marks the path counts as dated and the distinct counts
as standing.

## Shape of the fix

Corrected **in place and dated**, not silently overwritten — the same
shape as the two `⚠ WITHDRAWN 2026-09-22` blocks already in this
document.

## Verification

Gate and suite re-run after each commit, since the doc names the deleted
filename and depends on the allowlist:

```
check-launcher-standard-currency: canonical=launcher-standard_praxis.deed expected=v0.4.0
No stale launcher-standard references.
gate rc=0

=== summary: 19 passed, 0 failed ===
```

Docs-only, one file, two commits.

Refs standards#960, #983.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Sep 22, 2026
standards#960 AC4 was implemented and tested but never wired -- no workflow
in any repository invoked it. This arms it in the `quality` job, in tiers.

The gate is copied out inside the existing `Check documentation` step,
before that step's `rm -rf .standards-checkout`, and run from $RUNNER_TEMP
afterwards. That shape is forced, and it is this file's own established
idiom (see validate-hypatia-baseline): the sparse checkout puts standards'
scripts at .standards-checkout/scripts/, and the gate's allowlist entries
for its own source carry no '*/' prefix, so run in place it reports 16
defects on itself and would red every consumer.

Self-lint fallback, likewise copied from the neighbouring gates: prefer the
caller's own scripts/check-launcher-standard-currency.sh when present, so
standards validating itself runs the tree under test rather than main's
copy. Without it this pull request's own CI would judge the fix by the
version it replaces.

Tiers, with the evidence recorded in standards#991:

  retired-filename BLOCKS. The predicate is stable -- launcher-standard.a2ml
  was deleted upstream (standards#952) and stays deleted, so a clean caller
  cannot become defective without editing the citation. Measured over 26
  consumers: 3 carry defects, all archived or on the DO-NOT-MERGE list, so
  arming this reds zero live callers.

  stale-version WARNS. The predicate is time-dependent -- the gate compares
  against its own CURRENT_VERSION, so every correctly-citing caller flips to
  defect at the next bump having done nothing. A cutoff date does not cure
  that; each CURRENT_VERSION bump is a measure-then-arm event.

The split greps the gate's typed stdout lines rather than its exit code,
because both classes return 1. rc=2 (usage error, or a surviving self-test
mutant) fails before the split, so a broken invocation can never read as a
clean pass, and the gate's header is echoed unconditionally so a vacuous
run is visible.

Proven, not asserted: the wrapper was run under `bash -eo pipefail` against
seeded gates emitting clean / retired-only / stale-only / mixed / rc=2 /
rc=1-with-no-defect, and each branch returned the intended exit code --
stale-only exits 0 with a ::warning::, retired-only exits 1. The fixed gate
returns rc=0 with zero defects on standards' own tree at origin/main, so
the self-lint path does not red this pull request. actionlint reports the
same 5 pre-existing findings before and after, and no `uses:` is added, so
actions.lock is untouched.

Refs: #960, #983, #990, #991, #952, #505

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
hyperpolymath added a commit that referenced this pull request Sep 22, 2026
)

Closes the last open half of **#960 AC4**. The gate merged in **#983**
was
`implemented` and `tested` but **never `wired`** — no workflow in any
repository
invoked it. This pull request does both halves, and they have to travel
together:
consumers fetch the gate from `ref: main`, so wiring before the cure
would arm the
false positives estate-wide, and curing without wiring leaves a gate
nothing runs.

---

## Part 1 — the cure: three false positives

Measured across 27 repositories, the gate reported **19** defects. Three
were
implementation defects in the gate itself.

**G1 — version conflation.** The gate accepted any version number within
24
non-digit characters of the filename. A line reading
`` `launcher-standard_praxis.deed` (DEED v1.0.0). Per-app config: ``
captured the
**grammar** version `1.0.0` and reported it as **document** drift. The
gate's own
header distinguishes `:schema-version` (grammar, 1.0.0) from
`:standard-version`
(document, 0.4.0) — this is the guard-asks-a-different-question trap
inside the
guard's own implementation. Cured by reading the gap between filename
and number:
`deed` / `schema` / `grammar` in the gap means a grammar version, not a
conformance claim.

**G2 — the allowlist was keyed to `standards`' own layout.**
`launcher-standard_praxis.deed`
was exempt at a repository root but not when **vendored at any depth**
by a
consumer. Cured with a `*/launcher-standard_praxis.deed` entry.

**G3 — no exemption for dated ADR carriers.** `*/descriptiles/META.a2ml`
and the
deprecated `*/6a2/META.a2ml` spelling are dated architecture decision
records;
naming a retired file is precisely what they are for. Both are on disk
today
(measured 2026-09-22: 164 `descriptiles` directories, 770 deprecated).
This is the
repo-that-migrated-correctly-looks-worst-to-a-grep pattern —
`launch-scaffolder`
ranked worst on AC1 *because* it recorded its own migration.

**Acceptance control, measured before and after:**

| repository | before | after |
|---|---|---|
| `launch-scaffolder` | 4 | **0** |
| `the-nash-equilibrium` | 4 | 4 |
| `tma-mark2` | 7 | 7 |
| `canonical-ums` | 4 | 4 |
| **total over 27 repos** | **19** | **15** |

Only genuine defects survive. The shell suite is `19 passed, 0 failed`,
rc=0.

---

## Part 2 — the wiring, and the depth trap that forces its shape

The gate is copied out **inside the existing `Check documentation`
step**, before
that step's `rm -rf .standards-checkout`, and run from `$RUNNER_TEMP` in
a new step
afterwards. That shape is not a preference — it is forced, and it is
this file's own
established idiom. `validate-hypatia-baseline` already carries the
reasoning verbatim:

> Move the baseline filter OUT of the scanned tree, then delete the
standards
> checkout, so `hypatia scan .` only ever sees the CALLER's own files.

The docs-gate step sparse-checks `standards`' `scripts/` into
`.standards-checkout/scripts/`. The gate's allowlist entries for **its
own source**
(`scripts/check-launcher-standard-currency.sh`) carry no `*/` prefix, so
they stop
matching at that depth. Run in place, the gate reports **16 defects on
itself** —
which would red every consumer.

**Self-lint fallback**, copied from the neighbouring gates: prefer the
caller's own
`scripts/check-launcher-standard-currency.sh` when present, so
`standards`
validating itself runs the tree under test rather than `main`'s copy.
Without it
**this pull request's own CI would judge the fix by the version it
replaces**.

**No second `actions/checkout`** — a new `uses:` would force an
`actions.lock`
regeneration. The diff adds **zero** `uses:` lines; `git diff -U0 | grep
-E "^[+-] *uses:"` is empty.

---

## The tiers, and why they differ

Recorded with its evidence in **#991**.

| class | predicate | tier |
|---|---|---|
| `retired-filename` | **stable** — `launcher-standard.a2ml` was deleted
upstream (#952) and stays deleted, so a clean caller cannot become
defective without editing the citation itself | **fails the job** |
| `stale-version` | **time-dependent** — the gate compares against its
own `CURRENT_VERSION`, so every correctly-citing caller flips to defect
the moment the standard bumps, having done nothing | **`::warning::`,
non-blocking** |

A baked-in cutoff **date** does not cure `stale-version`. The #505 docs
split above
can use one because its missing-`CONTRIBUTING` population is **static**
— it only
shrinks. This population is **regenerated at every bump**. So each
`CURRENT_VERSION`
bump is a measure-then-arm event, not a date, and #991 records that rule
plus the
open question of whether the class should ever be promoted.

**Arming `retired-filename` today reds zero live callers.** Measured
over 26
consumers: 23 clean, 3 defective — `tma-mark2` (archived),
`canonical-ums`
(archived), `the-nash-equilibrium` (live but AC1-exempt and on the
DO-NOT-MERGE
list). All three pin a SHA, not `@main`; the 13 `@main`-pinned consumers
carry none.

The split greps the gate's **typed stdout lines**, not its exit code —
both classes
return 1, and `DEFECT` lines go to stdout, not stderr. `rc=2` (a usage
error, or a
seeded self-test mutant that survived) fails **before** the split, so a
broken
invocation can never read as a clean pass. The gate's `canonical=…
expected=…`
header is echoed unconditionally, so a vacuous run is visible rather
than green.

---

## Proven, not asserted

- **The wrapper's mutants die.** Run under `bash -eo pipefail` against
seeded gates
emitting clean / retired-only / stale-only / mixed / rc=2 /
rc=1-with-no-defect:
**6 of 6** branches returned the intended exit code. `stale-only` exits
**0 with a
`::warning::`**; `retired-only` exits **1**. `::warning::` cannot fail a
job, so a
tier split that is merely *written* proves nothing — this one was
executed.
- **⭐ The armed fail tier caught the patch that armed it.** This is the
finding
worth keeping, and it is stronger evidence than the mutant harness —
that was
simulated, this was live. Commit `418662fb` wired the gate and went
**red on its
own CI**: the new step's arming-policy comment and its `::error::` both
named
`launcher-standard.a2ml` literally, and `governance-reusable.yml` is
part of every
  caller's scanned tree, so the gate read its own prose and reported two
`retired-filename` defects on it. Correct behaviour, not a gate defect:
the fail
  tier demonstrably reds a real tree.
- **Cured in `013fa07d`** by naming the *concept*, not the literal.
Nothing is
lost — the step already `cat`s the gate's output, which prints the
offending
`file:line` **and** a fuller remedy than the `::error::` was restating.
- **Rejected:** allowlisting `*/governance-reusable.yml`. A consumer
workflow
naming the retired file **is** a genuine defect, and the allowlist would
hide it.
- **Why the go/no-go missed it:** it ran against `origin/main`'s tree —
*without*
this patch — and returned rc=0. **A pre-patch clean run is not evidence
the
post-patch tree is clean.** Re-run on the patched tree: **rc=0, zero
`DEFECT`
lines.** This is the "run the tree under test, not `main`'s copy" rule
that Part 2
    invokes, firing against its own author.
- **No new lint debt.** `actionlint` reports the same **5** pre-existing
findings
  before and after the patch; zero introduced.
- **The tested wrapper IS the shipped wrapper.** It is extracted from
this file by
`yq '.jobs.quality.steps[7].run'` rather than maintained as a parallel
copy, so
there is no second artefact that can drift out of step. The 6-of-6
mutant run
above was re-executed against that extraction **after** the `013fa07d`
fix, not
  only before it.

## After this lands

**#991** carries the arming policy, the re-arm rule, and the one open
owner
question (`stale-version`: warn permanently, or
warn-until-a-sweep-confirms-zero
then promote). #960's AC4 line can be marked `wired`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

---------

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
Signed-off-by: Jonathan D.A. Jewell <jonathan.jewell@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant