Skip to content

backlog: file #1242 -- asvs-apply-cells.py is lossy in four ways and its guard sees one #904

backlog: file #1242 -- asvs-apply-cells.py is lossy in four ways and its guard sees one

backlog: file #1242 -- asvs-apply-cells.py is lossy in four ways and its guard sees one #904

Workflow file for this run

# 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"