Skip to content

review(#4213): every reader of a declaration fails closed, and the window is read off the purge - #4223

Merged
meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/4213-review-fixes
Sep 13, 2026
Merged

meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/4213-review-fixes

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Stacks on #4213, which was already in the merge queue when its review landed and therefore
could not be pushed to ("Branches that are queued for merging cannot be updated"). Dequeuing a
green, queued PR to amend it is a shared action with a steward attached, so the fixes come as
their own change instead. Until #4213 merges this diff shows both commits.

Six Copilot findings on #4213 and #4214. All six are real; each has its own falsified arm.

1–2. 🚨 Fail-open on a malformed switch — three guards disarmed by one quote mark

Every reader of pause and recordAheadOfRegistry asks inForce is True. That is correct for a
JSON boolean and catastrophic for "true", which is not True. A record written with a typed
quote mark would read as no declaration at all to:

  • the apply interlock — it would walk straight past a pause and enable the purge;
  • the record overwrite guard — it would recapture the live 7-day task without OVERWRITE;
  • the new coherence gate — it would pass an Enabled task sitting under an apparent pause.

…while reading, to a human reviewing the diff, exactly like an armed declaration.

The shape is now asserted on every pull request (check_in_force_is_boolean, over both blocks:
not-an-object, missing inForce, and a non-boolean are each RED), and both shell halves fail
closed
on the same condition rather than trusting that to have run. Controlled against a scratch
record:

$ cat tasks.json   # pause.inForce is the STRING "true", and a task is Enabled
$ acr-retention-tasks.sh apply
pause.inForce is 'true', not a JSON boolean — refusing to decide whether the purge is paused
exit 1
$ acr-retention-tasks.sh record     # recordAheadOfRegistry.inForce likewise
::error::cannot read …/tasks.json, so it is unknown whether the record is
  deliberately ahead of the registry. Refusing to decide either way.
exit 1

3. 🚨 The window was read off the shell LINE, not the command that deletes

cmd: echo --ago 30d; acr purge --filter 'x:.*' --untagged satisfied the 30-day floor with the
purge carrying no --ago at all. Each acr purge is now isolated at the first shell separator and
checked on its own — and every invocation on the line, so a second one carrying --ago 7d no
longer rides in behind a compliant first.

4. --keep=10 and a bare --keep escaped a whitespace-only match

Both are quotas; a bare one is a malformed step rather than a compliant one. All three spellings
count now, and a bare --ago with no duration is unchecked — which is a failure, never a pass.

5. 🚨 The declared-drift exemption was keyed only by task name

Once purge-old-images was listed under recordAheadOfRegistry, every difference in its live
YAML printed as the deliberate record-ahead drift and left verify green — including a live
definition that had gained --include-locked (which deletes every manifest the lock protects),
swapped its filters, or stopped being an acr purge. The record-side checks read the recorded
file, so they would never have seen it.

The LIVE steps of a declared task are now held to all three invariants. The declaration covers the
window moving, never the task becoming something else.

6. (#4214) "two AZURE_*" — there are three

Corrected in #4222, with the denominator counted rather than eyeballed: the preflight carries nine
missing+= lines, five already provisioned for main-cd and four COMBO_* missing.

The negative controls

Restoring the three pre-review behaviours — the window read off the whole line, the whitespace-only
--keep, and the inForce boolean check removed — reds seven arms:

::error::ARM 31c: a `--ago` sitting on the LINE but not in the `acr purge` INVOCATION satisfied the window…
::error::ARM 31c: `--keep=10` passed. It is the same quota as `--keep 10`…
::error::ARM 31c: a BARE `--keep` passed — a malformed step is not a compliant one
::error::ARM 31c: a SECOND `acr purge` on the same line escaped the window check…
::error::ARM 31d: `pause.inForce` as the STRING "true" was accepted…
::error::ARM 31d: `recordAheadOfRegistry.inForce` as the STRING "true" was accepted…
::error::ARM 31d: the coherence gate passed an `Enabled` task sitting under a pause whose `inForce`
         is a string — the exact record the apply interlock would also walk past

Restored: --self-test exits 0. The shell halves were driven against a scratch record (above), no
Azure.

Verification

  • lock-pinned-digests.py --self-test → 0 · --check-retention-record . → 0
  • check-workflow-shell.py --root . → 0 (shellcheck covers .github/scripts/*.sh)
  • dotnet build test/MeshWeaver.Documentation.Test -c Release -warnaserror → 0 Warning(s) 0 Error(s)
  • dotnet test … → 447 passed, 0 skipped

Refs #3438
Refs #3859

🤖 Generated with Claude Code

rbuergi and others added 2 commits September 13, 2026 20:45
… interlock re-enabling it

#3842's rule — retain unreferenced continuous artifacts for at least 30 days by age,
without a build-count quota — was landed in two of the three stores that implement it and
asserted in none. `PrebuiltBundleRetention` clamps `MinimumAge` up to 30 days (#3843) and
`AssemblyCacheRetention` does the same (#3846); #3843's own body records the third store
being left out in as many words: "The ACR task record and live cloud cleanup are unchanged."

So `.github/acr-retention/purge-old-images.yaml` kept `--ago 7d --keep 10` over five
continuously-republished repositories, `memex-portal-ai` among them — the image both
production portals run — and nothing compared it to the rule governing the other two.

THE WINDOW
`check_retention_record` now holds every recorded `acr purge` step to both halves: an
`--ago` of at least 30 days, and no `--keep`. Every step, enabled or not, because the
record is what `acr-retention-tasks.sh apply` pushes — a disabled task carrying a 7-day
window is a 7-day window one command away from running. An unparseable `--ago` is RED, not
a default. The check needs no credential and already runs on every pull request
(dotnet-test.yml's workflow-shell lane).

`--keep` is removed rather than raised: it counts NEWER BUILDS, so republishing frequency
is what destroys a long-lived reference — #3438's own root cause stated as a flag. A 30-day
`--ago` beside a `--keep 10` still collects a manifest ten builds old.

The record is now deliberately AHEAD of the (disabled) live task, declared in tasks.json
under `recordAheadOfRegistry`: `verify` prints that drift instead of reporting it as an
incident, and reds once it is closed so the declaration expires by being checked; `record`
— the one command that overwrites the record FROM live — refuses without an explicit
OVERWRITE, because re-capturing would silently restore the quota window.

THE RE-ENABLE INTERLOCK (#3859 acceptance criterion 1)
`apply` is the only thing in this repository that can turn a destructive schedule back on,
and it did so with reference to nothing: one word in tasks.json (Disabled -> Enabled) plus
`apply` restored the 03:00 purge with no protection decision consulted, while the `pause`
block two screens below still said protection was incomplete. It now REFUSES while
`pause.inForce` is true and any task is recorded Enabled, naming `pause.reEnableWhen`, and
refuses before the confirmation prompt so the prompt cannot be mistaken for the gate. The
record's own coherence is gated too: an in-force pause beside an Enabled task is RED, and
so is every task Disabled with nothing declaring why.

This does NOT close #3859. The lock is an Actions cron at 01:00 and the purge an ACR timer
task at 03:00; a nightly deletion is still not downstream of that night's protection
verdict. That needs a registry-side mechanism and an RBAC grant — the coordination choice
#3859 says to make with the maintainer.

NOTHING DESTRUCTIVE WAS RUN: no purge, no dry run, no registry mutation, no `apply`.

Refs #3438
Refs #3859

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ndow is read off the purge

Six Copilot findings, all real, each with its own falsified arm.

1-2. 🚨 FAIL-OPEN ON A MALFORMED SWITCH. Every reader of `pause` and `recordAheadOfRegistry`
     asks `inForce is True` — correct for a JSON boolean, catastrophic for `"true"`, which is
     not `True`. ONE typed quote mark would have disarmed the apply interlock, the `record`
     overwrite guard AND the new coherence gate at once, while reading to a human exactly like
     an armed declaration. The shape is now asserted on every pull request
     (`check_in_force_is_boolean`, both blocks: not-an-object, missing `inForce`, and a
     non-boolean are each RED), and BOTH shell halves fail closed on the same condition rather
     than trusting that to have run. Controlled: `pause.inForce: "true"` + an Enabled task now
     refuses `apply` (exit 1) instead of walking past the interlock.

3.   🚨 THE WINDOW WAS READ OFF THE SHELL LINE, NOT THE COMMAND THAT DELETES.
     `cmd: echo --ago 30d; acr purge --filter 'x:.*' --untagged` satisfied the floor with the
     purge carrying no `--ago` at all. Each `acr purge` is now isolated at the first shell
     separator and checked on its own, and EVERY invocation on the line is checked — a second
     one with `--ago 7d` no longer rides in behind a compliant first.

4.   `--keep=10` and a bare `--keep` both escaped a whitespace-only match, so a recorded step
     could keep a build-count quota and pass. All three spellings count; a bare `--ago` with no
     duration is now UNCHECKED (red), never compliant.

5.   🚨 THE DECLARED-DRIFT EXEMPTION WAS KEYED ONLY BY TASK NAME, so once `purge-old-images`
     was listed, EVERY difference in its live YAML printed as the deliberate record-ahead drift
     and left `verify` green — including a live definition that had gained `--include-locked`
     (which deletes every manifest the lock protects), swapped its filters, or stopped being an
     `acr purge`. The record-side checks read the RECORDED file and would never have seen it.
     The LIVE steps of a declared task are now held to all three invariants; the declaration
     covers the WINDOW moving, never the task becoming something else.

6.   (#4214) the provisioning table said "two AZURE_*"; there are three.

Every fix carries an arm (ARM 31c, ARM 31d) and every arm was falsified: restoring the three
pre-review behaviours reds seven lines, including the two `inForce`-as-a-string arms and the
coherence gate passing an Enabled task under an apparent pause.

Verification: --self-test 0, --check-retention-record 0, check-workflow-shell 0,
Documentation.Test 447 passed / 0 skipped, Release -warnaserror 0 warnings 0 errors.

Refs #3438
Refs #3859

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 13, 2026 19:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved critical and moderate findings affect declaration handling, shell command parsing, and retention reporting.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates ACR retention controls and documentation to enforce a 30-day, no-quota purge policy with fail-closed interlocks.

Changes:

  • Adds retention-window, pause, and coherence validation.
  • Guards recording, verification, and re-enabling operations.
  • Updates retention records, architecture docs, and release notes.
File summaries
File Summary
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-13-retention-keeps-continuous-artifacts-for-thirty-days.md Adds the user-facing retention change note.
src/MeshWeaver.Documentation/Data/Architecture/ReleasedArtifactRetention.md Updates retention status documentation.
src/MeshWeaver.Documentation/Data/Architecture/PinnedImageRetention.md Clarifies recorded and live retention policy.
src/MeshWeaver.Documentation/Data/Architecture/ArtifactRetentionInterlock.md Documents policy and interlock behavior.
.github/scripts/lock-pinned-digests.py Validates retention policies, declarations, and self-tests.
.github/scripts/acr-retention-tasks.sh Guards recording, verification, and re-enabling.
.github/acr-retention/tasks.json Records pause and registry-drift state.
.github/acr-retention/purge-old-images.yaml Updates the purge window to 30 days.
Review details

Suppressed comments (8)

.github/scripts/acr-retention-tasks.sh:294

  • The apply interlock has the same null/absent ambiguity for pause: with "pause": null, p is None selects the no-pause path and an enabled task can reach the mutation prompt. This should fail closed for a present null, while only an absent pause block should print no.
p = d.get('pause')
if p is None:
    print('no'); sys.exit(0)

.github/scripts/acr-retention-tasks.sh:178

  • The declared-drift path repeats the same text-matching flaw: grep -qv "acr purge" accepts a live cmd: echo "acr purge --filter ...", and the filter comparison can still match the recorded task. A live task can therefore become a no-op while verify reports the drift as declared and exits successfully. Validate the actual command tokens rather than the substring.
        if printf '%s\n' "$live_steps" | grep -qv "acr purge"; then
          echo "::error::the LIVE definition of '$task' has a \`cmd:\` step that is not an acr purge."
          drift=1

.github/scripts/acr-retention-tasks.sh:186

  • The declared-drift exemption compares only single-quoted --filter '…' text. A live task can retain all recorded filters and add an unquoted or double-quoted --filter for another repository, leaving live_filters == recorded_filters; because declared drift is otherwise accepted, verify would then greenlight an unreviewed purge target. Compare parsed filter arguments (including every spelling), rather than extracting only this quote form.
        local live_filters recorded_filters
        live_filters=$(printf '%s\n' "$live_steps" | grep -o -- "--filter '[^']*'" | sort | uniq)
        recorded_filters=$(grep -E "^[[:space:]]*-[[:space:]]+cmd:" "$recorded" \
                             | grep -o -- "--filter '[^']*'" | sort | uniq)
        if [ "$live_filters" != "$recorded_filters" ]; then

.github/scripts/lock-pinned-digests.py:1757

  • After recording a malformed pause, this function still calls .get on the raw value. A truthy non-dict such as "yes" makes check_pause_coherence raise AttributeError, so --check-retention-record crashes with a traceback instead of returning the collected validation error. Normalize non-dict pause values before reading their fields.
    pause = manifest.get("pause") or {}
    in_force = pause.get("inForce") is True

.github/scripts/lock-pinned-digests.py:1656

  • Stopping the regex at ; only isolates the purge for its window check; it does not reject other commands on the same cmd: line. A recorded step such as acr purge ... --ago 30d --untagged; az acr repository delete ... passes the substring, window, and lock checks while adding an unrelated destructive operation. The record gate must validate the complete shell command and reject non-allowed commands.
# command that deletes. Each `acr purge` is isolated at the first shell separator and checked on its
# own, and EVERY invocation on the line must pass.
PURGE_INVOCATION_RE = re.compile(r"acr\s+purge\b(?P<args>[^;&|\n]*)")

.github/scripts/lock-pinned-digests.py:1660

  • These matchers now accept --ago=30d, --keep=10, and a bare --keep, but retention_windows() still extracts only the spaced forms (--ago (\S+)/--keep (\S+)). An enabled record using an accepted equals spelling would therefore be reported as --ago ? or --keep none, contradicting the policy reader and misleading the protection report. Reuse the same parser for the report.
# 🚨 BOTH SPELLINGS, AND THE BARE FLAG. `--keep=10` is the same quota as `--keep 10`, and a bare
# `--keep` is a malformed step rather than a compliant one; a whitespace-only match passed all three.
KEEP_RE = re.compile(r"--keep(?:[=\s]+(?P<value>[^\s;&|]+))?")
AGO_RE = re.compile(r"--ago(?:[=\s]+(?P<value>[^\s;&|]+))?")

.github/scripts/lock-pinned-digests.py:2884

  • The success summary still says the self-test has “25 arms”, but this change adds the ARM 31/31b/31c/31d retention checks described later in the same message. That output now understates the coverage it claims to report; update the count or derive it instead of leaving a stale verification summary.
          "and counted as protecting NOTHING, and the two existing pin extractors still agree. AXIS 3: the set an installation is RUNNING is locked though no file pins it, its migration twin with it, the TAG is locked beside the manifest, an installation that did not answer is INCOMPLETE and refuses the unlock arm, silence is never retirement, a stale roster entry and an unknown running set are RED, the digest extractor is controlled against a fixture rather than inferred from the fleet, a tag lock that did not take is counted as protecting NOTHING, a locked INDEX is expanded to the platform manifests acr-cli would otherwise collect out from under it, and the harness provably drives the same path as run(). THE RECORD: every recorded purge step is held to #3842's decided window — at least 30 days by age, no `--keep` build-count quota — with the exact `--ago 7d --keep 10` step this repository carried until 2026-09-13 driven as a literal control, a bare or unreadable `--ago` RED, and the decided window itself proven to PASS; and the pause declaration cannot contradict the statuses it is recorded beside, in either direction. The window is read off each `acr purge` INVOCATION rather than the shell line, every invocation on it, `--keep=N` and a bare `--keep` count as quotas, a bare `--ago` is unchecked rather than compliant, and a declaration whose `inForce` is the STRING \"true\" — which every `is True` reader silently treats as absent — is RED in both blocks.")

src/MeshWeaver.Documentation/Data/Architecture/ArtifactRetentionInterlock.md:418

  • acr-retention-tasks.sh apply does not lift the pause in the current record: it writes the YAML and then applies status: Disabled for purge-old-images; the interlock only refuses when a paused record also has an Enabled task. Thus applying the decided window and lifting the pause are not the same act. Clarify that apply can update the disabled definition, while re-enabling still requires the reviewed pause/status change.
1. **The decided window has not been APPLIED to the registry.** The record states it; the live
   (disabled) task still carries `--ago 7d --keep 10`. Applying it is the same act as lifting the
   pause, and it belongs to whoever owns the registry. Its cost is storage: dropping `--keep 10`
   and moving 7d → 30d retains strictly more, over five repositories that republish many times a
   day.
  • Files reviewed: 8/8 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +169 to +173
if printf '%s\n' "$live_steps" | grep -q -- "--include-locked"; then
echo "::error::the LIVE definition of '$task' passes --include-locked."
echo " That flag DELETES LOCKED MANIFESTS — the entire protection the lock job provides."
echo " A recordAheadOfRegistry declaration is about the retention WINDOW; it does not"
echo " excuse this, and without this line the drift would have printed as declared."
Comment on lines +1652 to +1656
# searching the whole line lets `cmd: echo --ago 30d; acr purge --filter 'x:.*' --untagged` satisfy
# a window the purge itself does not carry — the flag is present on the line and absent from the
# command that deletes. Each `acr purge` is isolated at the first shell separator and checked on its
# own, and EVERY invocation on the line must pass.
PURGE_INVOCATION_RE = re.compile(r"acr\s+purge\b(?P<args>[^;&|\n]*)")
Comment on lines +80 to +82
a = d.get('recordAheadOfRegistry')
if a is None:
print(''); sys.exit(0)
Comment on lines +1727 to +1729
block_value = manifest.get(block)
if block_value is None:
return []
# Manufacturing and SocialMedia CI down simultaneously 2026-09-05T15:29Z on a purged tester digest).
steps:
- cmd: acr purge --filter 'mw-plugin-test:.*' --filter 'memex-migration:.*' --filter 'memex-portal-ai:.*' --filter 'memex-portal-next:.*' --filter 'memex-bake:.*' --ago 7d --keep 10 --untagged
- cmd: acr purge --filter 'mw-plugin-test:.*' --filter 'memex-migration:.*' --filter 'memex-portal-ai:.*' --filter 'memex-portal-next:.*' --filter 'memex-bake:.*' --ago 30d --untagged
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

262 tests   262 ✅  2m 15s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit 7d48482.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 425 tests   1 425 ✅  1m 28s ⏱️
    2 suites      0 💤
    2 files        0 ❌

Results for commit 7d48482.

rbuergi added a commit that referenced this pull request Sep 13, 2026
…are prose

Six Copilot findings, all real. The measurement that settled the design also found a
seventh — my own extractor was matching inside COMMENTS.

MEASURED across the fleet's TWELVE deployment overlays, 2026-09-13, before deciding
anything: exactly TWO foreign hosts. `cr.meshweaver.cloud` (4 references: build x2,
pearl x2) and `ghcr.io` (2 — one is `ghcr.io/distribution/distribution`, the registry
service's OWN image in memex-cloud's overlay; the other is A LINE OF PROSE in ci-runners
describing what the runner image is built from). Applying "every undeclared foreign host
reds" literally would therefore have reddened the lane over a third-party image AND over a
sentence. This issue already paid for that lesson: 11 of the 31 `sha256:` tokens in its
2026-09-08 hand count were comments.

WHAT CHANGED
1. 🚨 THE UNIT IS THE REGISTRY, NOT THE INSTANCE. `instances.json` gains a top-level
   `registries` table and the per-instance `registry` key is REFUSED. A per-instance field
   answers "is this one out of scope" and can never answer "is every registry the fleet
   pins in accounted for" — an installation pinning its declared registry AND a second
   undeclared one passed it with half its running set unnamed (finding 4). Two dispositions
   with a mandatory reason: `fleet-unlockable` (ours, this lane cannot lock it) and
   `third-party` (never ours to protect). Unknown disposition, missing reason and an
   UNDECLARED host are each RED.
2. 🚨 AN UNDECLARED HOST REDS WHEREVER IT APPEARS (finding 3) — including on an installation
   that ALSO pins here, which the `if not repositories` branch never reached. And half
   covered is not covered: pinning both here and in a fleet-unlockable registry is a
   BLOCKER, because locking the ACR half and reporting success is a partially-protected
   installation reading as a protected one.
3. 🚨 A `*.azurecr.io` THAT IS NOT THIS REGISTRY IS FOREIGN (finding 2). `--registry
   meshweaver` locks one ACR; extracting another's repository and looking it up here is this
   bug reintroduced one registry along.
4. Helm's split `repository:` + sibling `tag:` shape is handled on the foreign path too
   (finding 5) — without it a foreign-only overlay written that way extracted as NOTHING and
   fell back into `pins no image at all`: this bug in its second shape.
5. 🚨 THE REPORT NO LONGER CLAIMS PROTECTION NOBODY CHECKED (finding 6). The out-of-scope
   line read "protected by that registry's own retention"; it now reads "NOT locked here,
   and whether anything retains that registry is UNVERIFIED". A summary claiming otherwise
   would make a green run read as covered and support re-enabling cleanup on a false
   premise — this mechanism's own failure mode, committed by its own report.
6. Whole-line COMMENTS are stripped before extraction, on BOTH paths.
7. Finding 1 (the declared-drift exemption keyed only by task name) is #4223, already open.

CONTROLS. Twenty-three arms (ARM 32), falsified in two directions: an inert classifier reds
six, and removing comment-stripping plus the host comparison reds four more including "a
reference inside a COMMENT was extracted as a pin". Re-run over the ACTUAL committed bytes of
all twelve overlays: the comment match is gone (ghcr.io 2 → 1), memex-cloud's 3 ACR + 1
third-party is not a blocker, build and pearl are out of scope, memex is 6 ACR + 0 foreign,
and both hosts are accounted for by the committed table.

Refs #3438

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

883 tests   883 ✅  3m 31s ⏱️
  2 suites    0 💤
  2 files      0 ❌

Results for commit 7d48482.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files      6 suites   4m 46s ⏱️
2 093 tests 2 093 ✅ 0 💤 0 ❌
2 097 runs  2 097 ✅ 0 💤 0 ❌

Results for commit 7d48482.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 047 tests   1 045 ✅  6m 23s ⏱️
    3 suites      2 💤
    3 files        0 ❌

Results for commit 7d48482.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 829 tests   1 637 ✅  4m 27s ⏱️
    4 suites    192 💤
    4 files        0 ❌

Results for commit 7d48482.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   18 files     18 suites   22m 52s ⏱️
7 539 tests 7 345 ✅ 194 💤 0 ❌
7 543 runs  7 349 ✅ 194 💤 0 ❌

Results for commit 7d48482.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 8e08ddf Sep 13, 2026
37 checks passed
rbuergi added a commit that referenced this pull request Sep 13, 2026
Five more findings, all real.

1-2. 🚨 A TEXT SEARCH FOR `--include-locked` IS DEFEATED BY THE SHELL'S OWN QUOTING.
     `acr purge --include-"locked" …` EXECUTES with that option — the shell joins the
     fragment — and matches no grep for the literal string. That flag deletes every manifest
     the lock protects, so a text search was the wrong instrument for the one invariant this
     lane rests on, on BOTH sides: the recorded gate and the live check `verify` runs on a
     declared task. The same search fails the other way too: `echo "acr purge --filter 'a:.*'
     --ago 30d"` CONTAINS the text and deletes nothing, so it was reported as a compliant
     purge where no purge runs.
     One change answers both: a `cmd:` is tokenized the way a shell would (`shlex`,
     punctuation_chars), split on the operators, and only a command whose first two tokens
     are literally `acr` and `purge` is a purge — every such command on the line. A step with
     unbalanced quotes cannot be tokenized at all, which means nothing in it was checked,
     which is now a FAILURE rather than a pass.
3.   🚨 ONE PARSER, BOTH SIDES. `acr-retention-tasks.sh` kept its own greps for
     `--include-locked` and `--filter '…'` — the same defeated search one file along. It now
     asks `lock-pinned-digests.py --describe-purge-file`, which prints the tokenized steps as
     JSON. Controlled: `--include-"locked"` reads back `includeLocked: true`, and
     `echo "acr purge …"` reads back `notAPurge`.
4.   🚨 `pause: null` IS NOT THE SAME AS NO `pause`. `dict.get` answers None for an absent key
     and for a written null alike, so a present block declaring nothing read exactly like a
     record that never had one, and every reader — the apply interlock, the overwrite guard,
     the coherence gate — walked past it. Absent and null are now separated before anything
     reads the value, in both blocks.
5.   The `purge-old-images.yaml` header still called the first step "the 7d step" after its
     window moved. Corrected, with the previous window stated as history.

ARM 31e (6 assertions) and a null arm per block, all falsified: restoring a naive tokenizer
and `dict.get`-based null handling reds four, including the `--include-"locked"` bypass and
the unbalanced-quote pass.

Rebuilt onto main after #4223 merged: the branch was based on a pre-rebase copy of the same
commit, so git saw the change applied twice. Only the round-2 delta is re-applied here.

Verification on the rebuilt branch: --self-test 0, --check-retention-record 0,
check-workflow-shell 0, check-pinned-digests --self-test 0, Documentation.Test 459 passed /
0 skipped, Release -warnaserror 0 warnings 0 errors.

Refs #3438
Refs #3859

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request Sep 13, 2026
#4223 landed the #4213 review round in the same self-test region and the same summary line
this branch appends to, so the merge conflicted in two places. Both resolved by KEEPING BOTH:

  * the self-test arms — this branch's ARM 32 (another registry) followed by main's ARM 31c
    (the window read off the invocation) and ARM 31d (a non-boolean `inForce` fails closed);
  * the summary sentence — main's text, with this branch's ANOTHER REGISTRY clause re-appended
    and updated to describe what actually landed here (an undeclared registry reds wherever it
    appears, a declared fleet-unlockable one is counted saying protection there is UNVERIFIED,
    pinning in both is RED because half covered is not covered, a foreign `*.azurecr.io`,
    helm's split shape, and a comment is prose).

Nothing was dropped from either side. Verified after the merge: --self-test 0,
--check-retention-record 0, check-workflow-shell 0, Documentation.Test 459 passed / 0 skipped,
Release -warnaserror 0 warnings 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi deleted the fix/4213-review-fixes branch October 10, 2026 13:10
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.

2 participants