backlog: file #1242 -- asvs-apply-cells.py is lossy in four ways and its guard sees one #904
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Backlog status hygiene — stop `docs/BACKLOG.md` from lying about build state. | |
| # | |
| # WHY. Work ships, the item's banner is never updated, and the backlog goes on describing finished | |
| # work as open. A 2026-07-09 audit found 11 items misfiled as open — including #60 (turnkey DR), | |
| # which shipped with ADR 0049 and a working `backup` / `restore-verify` CLI while its banner still | |
| # read "PRE-RESERVED, owner-gated". That stale banner was then repeated as fact in a merged PR. The | |
| # same rot left the Corepoint gap analysis ~22% obsolete. A doc that lies about build state is worse | |
| # than no doc: it silently misdirects planning. | |
| # | |
| # TWO HALVES. | |
| # * The STRUCTURAL half ("every item declares exactly one status") is enforced by pytest — | |
| # `tests/test_backlog_status_check.py` runs the checker against the real file on every PR, so it | |
| # rides the existing test matrix and needs no job here. | |
| # * The BEHAVIOURAL half is here, because only the PR context can see it: if a PR claims to | |
| # implement a backlog item (`BACKLOG #N` in its title or body) and touches engine/IDE code, then | |
| # it must also update `docs/BACKLOG.md`. That is the step whose omission caused #60. | |
| # | |
| # THE JOB BELOW NOW RUNS TWO CHECKS, and its `name:` describes only the first. That is deliberate and | |
| # must stay: the name is the branch-protection CONTEXT STRING, so renaming it makes the required | |
| # context stop reporting and wedges every PR (see .github/required-contexts.txt). The second check is | |
| # the CITATION gate (BACKLOG #1095) — every backlog number this PR adds beside a ledger path must | |
| # name the file that item actually lives in. It rides this job rather than a new one for the same | |
| # reason: an unrequired context is decoration, since auto-merge blocks only on required ones, and | |
| # adding one to branch protection is not an in-repo change. | |
| # | |
| # Read-only. No secrets. Workflow expressions are hoisted into `env` and never interpolated into a | |
| # `run:` body (zizmor: a PR title/body is attacker-controlled on a fork PR and must arrive as data). | |
| name: backlog-hygiene | |
| on: | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: backlog-hygiene-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| banner-on-implementation: | |
| # QUOTED: an unquoted YAML scalar ends at " #" (comment start), which silently truncated this to | |
| # "a PR that implements BACKLOG". The job name is the required-status-check *context* string, so | |
| # a truncated name is what you would have to add to branch protection. | |
| name: "a PR that implements BACKLOG #N must update BACKLOG.md" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Require a backlog update when a PR claims to implement an item | |
| env: | |
| # Hoisted, never interpolated into the script body (zizmor: template injection). | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Does this PR *claim* to implement a backlog item? Only the explicit `BACKLOG #N` token | |
| # counts — a bare `#123` is ambiguous in this repo (it is usually a PR number). | |
| claim="$(printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -oiE 'BACKLOG #[0-9]+' | head -1 || true)" | |
| if [ -z "$claim" ]; then | |
| echo "No 'BACKLOG #N' claim in this PR — nothing to enforce." | |
| echo "(If this PR completes a backlog item, say so with 'BACKLOG #N' and update its banner.)" | |
| exit 0 | |
| fi | |
| # THREE-dot, deliberately. The two-dot form asks "how do these two trees differ", which | |
| # includes everything main gained since this PR branched -- as a REVERSE delta on paths the | |
| # PR never touched. Any main-side change to docs/BACKLOG.md (the archive move being one | |
| # large one) would then be credited to every open PR with an older base, and this gate would | |
| # pass while enforcing nothing. Three-dot asks the question actually being posed: what did | |
| # THIS PR change relative to the merge base. `ci.yml` already uses three-dot for the same | |
| # question. fetch-depth: 0 above guarantees the merge base is present. | |
| changed="$(git diff --name-only "$BASE_SHA...$HEAD_SHA")" | |
| touches_code=false | |
| case "$changed" in | |
| *messagefoundry/*|*ide/*|*messagefoundry_webconsole/*) touches_code=true ;; | |
| esac | |
| if [ "$touches_code" != true ]; then | |
| echo "PR claims '$claim' but changes no engine/IDE code — no banner update required." | |
| exit 0 | |
| fi | |
| # The item's banner lives in docs/BACKLOG.md while the item is open, and moves verbatim into | |
| # docs/archive/backlog/ once it is retired. Updating a retired item's banner is a legitimate | |
| # satisfaction of this rule, so both locations count -- otherwise the one PR shape that | |
| # correctly amends an archived item fails a REQUIRED check with no way to comply. | |
| if printf '%s\n' "$changed" | grep -qE '^(docs/BACKLOG\.md|docs/archive/backlog/.+\.md)$'; then | |
| echo "OK — PR claims '$claim', touches code, and updates the backlog item namespace." | |
| exit 0 | |
| fi | |
| n="$(printf '%s' "$claim" | grep -oE '[0-9]+')" | |
| cat >&2 <<EOF | |
| ERROR: this PR says it implements '$claim' and changes engine/IDE code, but it does not | |
| touch docs/BACKLOG.md or any file under docs/archive/backlog/. | |
| Update item #$n's status banner in the same PR — in docs/BACKLOG.md if the item is still | |
| open, or in its docs/archive/backlog/ file if it has already been retired. If the work is | |
| complete: | |
| > ✅ **SHIPPED in <version> (<ADR / PR>).** <one line of evidence> | |
| and remove its '🔢 Re-scored' banner (an item must declare exactly one status). | |
| This gate exists because #60 shipped while its banner still said "PRE-RESERVED", and that | |
| stale banner was later repeated as fact. A doc that lies about build state silently | |
| misdirects planning. | |
| If this PR only *partially* implements the item, keep the open banner and say so in the | |
| item body — then drop the 'BACKLOG #$n' token from this PR's title/body. | |
| EOF | |
| exit 1 | |
| # BACKLOG #1095. Retiring an item MOVES it verbatim from docs/BACKLOG.md into | |
| # docs/archive/backlog/, and every citation that named the live file keeps pointing at a file | |
| # the item is no longer in. No link checker can see this: docs/BACKLOG.md resolves perfectly, | |
| # and only the human-readable number beside it is stale. | |
| # | |
| # DIFF-SCOPED, and that is the design rather than a convenience. PR #271 declined a gate partly | |
| # because "a gate that fails on a legitimate archive is one people delete"; with pre-existing | |
| # violations a corpus-wide gate is red on day one and gets suppressed. --base/--head restricts | |
| # findings to lines THIS PR added, so it can only be red about something the PR wrote. Run the | |
| # script with neither flag for the repo-wide report, which is a measurement, not a merge gate. | |
| # | |
| # No setup-python: the checker and the parse_items module it imports are stdlib-only, so the | |
| # runner's preinstalled python3 is enough and the job stays a checkout plus two scripts. | |
| - name: Every backlog citation this PR adds must name the file its item lives in | |
| env: | |
| # Hoisted, never interpolated into the script body (zizmor: template injection). | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| python3 scripts/docs/backlog_citation_check.py --base "$BASE_SHA" --head "$HEAD_SHA" |