Files
ersatztv/docs/remote-state-inventory.md
T
timothyandtimothy 6a4265d81d
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 40s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m16s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m52s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 17s
feat(784): a doc records the end state — generalize the no-session-narrative rule (#811)
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
2026-08-22 00:23:22 +00:00

33 KiB

Remote-state inventory (ersatztv#778)

Every git-tracked file matching one of these, that reads live remote state and later acts on that read, and whether the read is bound to something that cannot change underneath it:

Directory Files
scripts/ (recursive, excluding scripts/tests/) *.sh, *.py
.claude/hooks/ *.sh
.husky/ all tracked files
.gitea/workflows/ *.yml, *.yaml

The per-directory extensions are stated because the prose once attached them to scripts/ alone while the guard applied them everywhere, so a .py hook would have joined the scope the doc described and acquired no row.

The scope qualifier is load-bearing, not throat-clearing: this file twice claimed to cover "every executable in this repo" while its own derivation missed real remote readers, so the heading now states exactly what the guard enforces — including the scripts/tests/ exclusion, so nobody adds a remote-reading test executable expecting a red guard that will stay green. C#/TypeScript guards, web/, and anything outside those directories are not covered. Scope is limit 3; the files-not-call-sites limit is limit 2.

scripts/tests/test_remote_state_inventory.py derives the population from git ls-files and asserts set equality against the Site column, so a new script that talks to a remote service cannot ship without acquiring a row.

Read docs/decisions/records/process/check-and-use-pins-a-version.md before adding a row or changing a classification.

Why this file exists rather than a linter

docs/defect-shapes-773.md §4 detector D is a fix pattern, not a mechanical detector: there is no general lint for "this code should have pinned a sha." What makes the class actionable is that the population is small and enumerable, so the detector is detector A — derive the population from an authoritative source and assert set equality — applied to this inventory. The inventory is the artifact; the test keeps it from rotting.

Columns

  • Class
    • PINNED — the read is bound to an immutable version identifier (a full commit sha, an image digest, a monotonic event count), and that binding still holds when the action runs. Two shapes qualify, and the second was missing from the first wording: either the action re-validates against the identifier immediately before committing (a snapshot nobody re-checks is not pinned — binding alone is never enough), or the check and the use are a single step over a value that cannot move, such as a workflow reading a full sha straight out of its own fixed event payload. What never qualifies is a value captured early and trusted later. The second shape is distinguished from an N/A row that says "resolution and use are one step" by the IDENTIFIER, not by the step count: PINNED requires the value itself to be immutable (a full sha, a digest), while a one-step read of a MUTABLE identifier — a registry tag, a branch name — is not pinned and is graded on what it authorizes.
    • CAS — the write itself carries a compare-and-set condition the server enforces.
    • UNSAFE-KNOWN — read-then-act with nothing pinning it, accepted with the reason stated in the Note. Every row here must say why the residual is tolerable, not merely that it exists.
    • N/A — reads no live remote state, or draws no authorization from what it reads.
  • Note — the window, and what closes or bounds it.

A row is about a site, not a file: a file with two independent reads gets two rows only where the classifications differ; otherwise the strictest applies and the Note names the exception.

The inventory

Hooks

Site Class Note
.claude/hooks/pretooluse-merge-consent.sh — head-sha reads (CI status, H10 status, verdict comments) UNSAFE-KNOWN Every comparison uses the full 40-char .head.sha (${sha:0:7} appears only in human-readable strings), which is the right identifier — but it is captured ONCE from the PR snapshot at the top of the hook, and is stale in two distinct ways. FIRST, within the run: every later check (CI status, H10 status, verdict comments) is evaluated against that captured sha, so a push landing mid-run is checked against the commit it replaced. This is the same defect that WAS live for $base_ref until it was re-read before the branch-protection lookup; the sha is not re-read, and closing it symmetrically is tracked in #803. SECOND, after the decision: the hook returns allow and a separate call performs the merge. "No async window" was the second overclaim cold review removed from this file. Both are bounded the same way — a head the verdict does not cover cannot inherit the sha-bound required status, so the server refuses it — and neither is bounded by anything in this hook. The merge API accepts an optional head_commit_id, which would make the call a true CAS; a PreToolUse hook cannot ADD that argument, only refuse without it, and requiring it changes every merge call's shape — tracked as follow-up rather than smuggled in here. Accepted meanwhile because the window is one tool call wide with no scheduler in it, and the server-side per-sha required check still refuses an unreviewed head.
.claude/hooks/pretooluse-merge-consent.sh — scheduled auto-merge (merge_when_checks_succeed) UNSAFE-KNOWN Preflight, not a pin — graded down by cold review, which was right: the hook's own comment concedes the branch-protection read pins nothing, so calling it PINNED contradicted this file's definition. What the hook proves is a snapshot; Gitea merges later; and since #778 it also verifies that review-verdict/h10 is a required check, reading the repo's full rule list (never the by-name endpoint, which does no matching and knows nothing about precedence) — nothing can govern the base → deny, unreadable → ask, and a glob rule that could govern it → ask, because the hook deliberately does not reimplement Gitea's glob dialect. It also asks when two rule names fold equal, or when either name is non-ASCII, since Gitea's EqualFold is Unicode-aware and its rule precedence is not derivable here. That converts an unobserved assumption into an observed precondition and detects drift, but an admin can still weaken the protection after the read. Accepted, and the earlier wording here was circular — it said the residual was "closed one layer down" by the very branch protection an admin may have removed. It is not closed; it is BOUNDED, and the bound is a trust assumption that should be stated rather than dressed as a mechanism: everything on this path assumes repo-admin branch-protection config is not hostile. If protection is present at preflight and removed afterwards, an unreviewed head can merge, and nothing in this repo would detect it. What the check does buy is that the far commoner case — protection already weakened when the merge is attempted — stops being silent. A PreToolUse hook cannot add head_commit_id to the merge call, so it can never convert its own grant into a CAS — it can only refuse, which is what it now does.
.claude/hooks/pretooluse-merge-consent.sh — base-retarget detection UNSAFE-KNOWN recorded_base parsed from the H10 status description is compared against the PR's live .base.ref (#632), and it deliberately compares the base ref rather than base.sha — the tip moves on every unrelated merge, so comparing it would deadlock every open PR. The residual is the same ABA the enumerator has: a name can be retargeted away and back, and the comparison cannot see it. Accepted because the alternative that CAN see it is the monotonic event count, which lives in the workflow that writes the enforced status rather than in this advisory hook.
.claude/hooks/pretooluse-merge-consent.sh## Done-when issue-body read UNSAFE-KNOWN The issue body carries an updated_at that is not used, so a box unticked between the read and the merge is invisible. Accepted: the only actor who can edit the issue is the one requesting the merge, so this is a self-inflicted race with no adversary and no silent-failure mode.
.claude/hooks/prepush-donewhen.sh## Done-when issue-body read UNSAFE-KNOWN No pin, but the hook's exit code gates the push synchronously — git blocks on this process. Blast radius is near zero regardless: main carries enable_push: false and block_admin_merge_override: true, so the direct push this hook exists to block is refused server-side for every account (#743). This is belt-and-braces over a path the server already refuses.
.claude/hooks/pretooluse-nav-guard.sh N/A Reads only the proposed tool call's own parameters and decides synchronously; the curl mentions in the file are prose, not executed lines.

| .claude/hooks/prepush-rebase-check.shgit fetch origin main, then git merge-base --is-ancestor origin/main HEAD to decide whether to block the push as behind | UNSAFE-KNOWN | Fetch-then-decide with no re-validation before the verdict; origin/main can advance inside that window. Accepted because the decision is self-correcting and cannot reach main: a push allowed on a now-stale read still lands on a feature branch, since direct pushes to main are refused server-side (#743), so the worst case is a rebase nag arriving one push later rather than a bad merge. This row exists because the previous token filter did not list git fetch and so could not see it at all. | | .claude/hooks/prepush-clean-worktree-check.shgit fetch origin main, then diffs origin/main...HEAD to scope which dirty files are in the pushed diff | UNSAFE-KNOWN | Same fetch-then-decide shape and the same bound: the verdict advises a feature-branch push only, main refuses direct pushes server-side (#743), and a stale origin/main read at worst lets a dirty-file push through, which the downstream review gate still catches before any merge. | | .claude/hooks/decisions-guard.sh | N/A | Reads no live remote state — runs scripts/decisions_validate.py over the local working tree; no fetch, no HTTP call. | | .claude/hooks/design-sync-reminder.sh | N/A | Reads no live remote state — compares local git diff/ls-files output against the session's own edits, never contacts origin. | | .claude/hooks/posttooluse-worktree-marker.sh | N/A | Reads no live remote state — parses the tool call's own JSON payload and writes a local ownership marker. | | .claude/hooks/pretooluse-agent-model.sh | N/A | Reads no live remote state — inspects only the proposed Agent call's own model/subagent_type fields. | | .claude/hooks/pretooluse-agent-ram.sh | N/A | Reads no live remote state — samples local memory_pressure -Q output. | | .claude/hooks/pretooluse-bash-guard.sh | N/A | Reads no live remote state — pattern-matches the proposed Bash command string for ETV_UPDATE_GOLDENS=. | | .claude/hooks/pretooluse-bom-guard.sh | N/A | Reads no live remote state — inspects local git diff output and reads local .cs bytes for a BOM. | | .claude/hooks/pretooluse-worktree-guard.sh | N/A | Reads no live remote state — reads a local .claude-worktree-owner marker in the target worktree. |

Husky git hooks

Site Class Note
.husky/pre-push N/A Delegates every remote read to prepush-donewhen.sh, prepush-rebase-check.sh and prepush-clean-worktree-check.sh, each of which carries its own row. This file forwards stdin ref lines and runs local npm run check:api/lint/typecheck/build against the checked-out tree.
.husky/pre-commit N/A Reads no live remote state — local lint-staged, decisions-guard.sh, a git diff --cached scan, and local dotnet format --verify-no-changes.
.husky/commit-msg N/A Reads no live remote state — greps the local commit-message file for a trailer.

Scripts

Site Class Note
scripts/post-review-verdict.sh — commit-status write PINNED Re-reads the PR and compares both .head.sha and .base.ref immediately before the POST, and dies (exit 1, no status written) on a mismatch or on a field it cannot read. That last clause is new: both comparisons were guarded by [ -n "$x" ] &&, so a well-formed 2xx body that merely omitted the field made the check a no-op and the status was posted having confirmed nothing — found by cold review on #778 and regression-tested against the real predecessor, since the redundant -z arm alone mutates green. Closes #706 and #632 for this path by read-compare-refuse, not by CAS: Gitea's status API offers no conditional write. Residual: the comment is posted before the re-read, so a head that moves in between leaves a verdict comment with no status — the comment is not the gate, but the mismatch is confusing and is tracked in #792.
scripts/pr-changed-files.sh — paged file enumeration UNSAFE-KNOWN #707's fix, graded honestly after cold review: .base.ref, .base.sha and .head.sha are captured before paging and re-checked after, and any observed movement fails the whole enumeration closed rather than emitting a short list. But before-and-after equality is ABA-vulnerable — a main → scratch → main retarget during paging can return the same ref and, if nothing merged meanwhile, the same base sha, while the pages in between were diffed against the scratch base. The script's own comment says it narrows rather than erases; this row previously said "any movement fails", which was stronger than the code. Accepted here because the enumerator cannot close it alone, but be exact about what the caller-side fence does and does not cover: ci.verdict-write-retarget-fence counts change_target_branch events, so it catches the BASE alias and nothing else. A HEAD alias is not covered by anything — a force-push H1 -> H2 -> H1 during pagination leaves the final .head.sha comparison equal while the middle pages were enumerated against H2, and no counter moves. That residual is real, unfenced, and stated here rather than papered over; closing it needs a monotonic head-mutation fence or enumeration bound to an immutable tree, neither of which exists today — tracked in #803, which also carries the three older contracts that still assert more than this row does.
scripts/select-queue.sh — issue list, then per-issue /dependencies UNSAFE-KNOWN The open-issue list (labels, milestone, priority) is snapshotted once; per-candidate dependency reads happen seconds later and never re-read the issue's own labels, so an issue claimed in-progress in that gap still appears on the shortlist. Accepted: the script authorizes no write. The real gate is the four-way claim check in process.parallel-session-claim, which runs after selection and re-reads live state by construction. Tightening this would move a check that must be adversarial into a tool that is advisory.
scripts/ci-detect-already-validated.sh — prior-head combined status UNSAFE-KNOWN Reads the PR head's status and emits skip=true, with nothing re-checking before the consuming job runs. Accepted and narrow: the skip elides only re-running test/migrations on a tree already validated; the build job still builds and pushes unconditionally, so no image ever ships from unvalidated source.
scripts/issue-qualification-audit.sh — paged issue list, then a report UNSAFE-KNOWN Pages the open-issue list and reports which issues lack a priority: label, so like every paged read here its pages can straddle a change and the report can name a state no single instant held. Graded to match select-queue.sh rather than N/A: the two run the same shape, and the reason offered for accepting select-queue.sh — it authorizes no write — cannot simultaneously be the reason this one is out of the class. Accepted on the same terms: it is advisory, session-end, human-read, and the labels it prompts for are applied by hand afterwards.
scripts/security-scan.shdocker pull, then docker run the same tag UNSAFE-KNOWN Pull and run are two steps over a MUTABLE tag, which is the same shape the registry rows below were graded down for; "resolution and use are one step" overstated it. In practice the second step resolves against the local daemon, which holds the image the pull just placed, so a mid-window retag does not change what runs. Accepted on that, plus the scope: this boots a throwaway container and scans it, authorizing nothing.
scripts/migration-smoke.shdocker pull, then docker run the same tag UNSAFE-KNOWN The same pull-then-run over a mutable tag as security-scan.sh above, and graded with it rather than left behind: its row previously said "Same shape" as a note that has since been rewritten to the opposite conclusion, so the backreference had quietly inverted. This one deserves the grade MORE, not less — security-scan.sh boots a throwaway container and authorizes nothing, while this is the pre-deploy migration smoke that gates a production stack recreation. Accepted on the same bound (the run resolves against the local daemon holding the image the pull just placed) plus its own stated operator-trust gap: the resolved image id is reported for a human rather than compared against a prior read.
scripts/hook-fire-log.sh N/A Entirely local: reads stdin and writes JSONL under the cache dir; the only curl in the file is in a comment.
scripts/e2e-local.sh N/A No outbound call at all; readiness is a local log grep and a local port probe against a subprocess it started.
scripts/e2e-ui.sh N/A Launches a local Chromium and runs specs against http://localhost:$PORT.
scripts/e2e-functional.sh N/A Every call targets $BASE_URL, defaulting to http://localhost:8409. The one non-local-looking address, 192.0.2.1, is TEST-NET-1 (RFC 5737) — written into the DB as a connection row precisely so it is unroutable, never dialed by the script.

| scripts/ci-detect-docs-only.shgit fetch origin "$base", then diffs the fetched tip against HEAD to emit docs_only, which gates whether the required test/migrations jobs run their real steps | UNSAFE-KNOWN | Read-then-act with no re-check between the fetch and the emitted value, and the decision genuinely gates required CI work. Accepted because the script is deliberately asymmetric: every ambiguous, undeterminable or shallow-checkout case resolves to docs_only=false (run everything), and only an exact unanimous all-docs diff yields true — so a stale or racing base read can at worst cause an unnecessary full run, never a skipped one (#416). | | scripts/refresh-shared-checkout.shgit fetch origin main, then git merge --ff-only origin/main and a conditional npm ci | UNSAFE-KNOWN | Fetch-then-act with no re-check between the fetch and the merge. Accepted because every action is self-refusing or reversible: --ff-only fails harmlessly rather than diverging if the ref moved on, the script refuses outright when the tree is not clean main or is ahead or mid-rebase, and this is a developer-convenience checkout rather than a release or merge-authorization path — a stale read costs one extra fetch next run, never lost work. | | scripts/check-review-verdict.sh | N/A | Reads no live remote state itself — classifies a comments JSON payload supplied on stdin; the fetch belongs to the caller's row. | | scripts/decisions_validate.py | N/A | Reads no live remote state — its git log/show/ls-tree/merge-base calls operate on refs the caller already checked out or passed via --base/--head, never a fetch. | | scripts/prove-fix.sh | N/A | Reads no live remote state — git worktree add/rev-parse/diff-tree operate on the local repository's own objects. | | scripts/add-migration.sh | N/A | Reads no live remote state — runs dotnet ef migrations add against local project files; implicit NuGet resolution is dependency supply-chain, out of this class per limit 1. | | scripts/update-openapi.sh | N/A | Reads no live remote state — a local dotnet build/GenerateOpenApiDocuments then a local python script. | | scripts/cleanup-code.sh | N/A | dotnet tool restore resolves and uses tooling in one step (limit 1, not a check-and-use split); the rest is a local git status --porcelain scan. | | scripts/cleanup-all-code.sh | N/A | Same shape as cleanup-code.sh — restore-and-use in one step, no check-then-act over remote state. | | scripts/build_decisions_catalog.py | N/A | Reads no live remote state — parses local decision records and writes the local catalog. | | scripts/decisions_lib.py | N/A | Reads no live remote state — pure parser over local decision-record files. | | scripts/migrate_decisions_split.py | N/A | Reads no live remote state — one-shot local file migration over docs/decisions/. | | scripts/generate-endpoint-index.py | N/A | Reads no live remote state — reads local v1.json and writes a local markdown index. | | scripts/check-doc-narrative.py | N/A | Reads no live remote state. Both modes are local: --diff reads git diff against a ref the CALLER fetched, --all reads git ls-files. The docs-reminder fetch that supplies the ref carries its own row below. | | scripts/check-kickoff-guard.sh | N/A | Reads no live remote state — scans a fixed local file list for forbidden phrasing. | | scripts/check-local-lsp.sh | N/A | Reads no live remote state — probes local PATH binaries and spawns a local MCP server over stdio. | | scripts/mcp_smoke.py | N/A | Reads no live remote state — spawns a local subprocess and speaks JSON-RPC over stdio pipes, no network socket. | | scripts/jq-preflight.sh | N/A | Reads no live remote state — runs local jq --version. | | scripts/ci-peak-anon.sh | N/A | Reads no live remote state — samples the runner's local cgroup memory.stat/memory.peak. | | scripts/ci-prove-ban-detects.sh | N/A | Reads no live remote state — mutates a local workflow copy and runs pytest against the local checkout. | | scripts/ci-step-ran.sh | N/A | Reads no live remote state — reads runner-supplied env vars and local marker files it wrote itself. | | scripts/set-provider.sh | N/A | Reads no live remote state — sets local dotnet user-secrets values. | | scripts/__init__.py | N/A | Empty package marker — executes nothing. | | scripts/scripted-schedules/entrypoint.pyScriptedScheduleApi.get_context(build_id), then define_content / reset_playout / build_playout against the same live server | UNSAFE-KNOWN | A genuine read-then-act over live ErsatzTV state, and the row cold review found missing when the population was still non-recursive. The context is fetched, handed to user-supplied script functions that mutate the playout, and re-fetched after a reset with nothing pinning either read — a concurrent build or edit between them is invisible. Accepted because it runs inside a single scripted-schedule build the server itself serialises per playout, and because the API exposes no version or ETag on the context to compare against; the honest bound is that the blast radius is one playout's content, reversible by rebuilding. | | scripts/macOS/bundle.sh | N/A | Reads no live remote state — moves files and creates symlinks in a local app bundle. | | scripts/macOS/sign.sh | N/A | codesign --timestamp contacts Apple's timestamp server, but resolution and use are one step with no earlier check whose answer is later trusted — the same boundary as limit 1. | | scripts/macOS/sign-dmg.sh | N/A | Same shape as sign.sh — a timestamped codesign over a local DMG, no check-then-act over remote state. |

Workflows

Site Class Note
.gitea/workflows/review-verdict.yml — status read → status POST UNSAFE-KNOWN The residual this whole class reduces to. Gitea's status API has no ETag, no If-Match and no expected-previous-state, so read and write cannot be made one operation. Narrowed twice rather than claimed closed: a monotonic change_target_branch event-count fence refuses to write if the count moved (ci.verdict-write-retarget-fence; the count is used because the branch name is ABA-vulnerable), and a high-water-mark re-read repairs a success posted over a human verdict back to pending. The file states the residual window explicitly rather than asserting safety.
.gitea/workflows/review-verdict.yml — base-ref checkout PINNED ref: ${{ github.event.pull_request.base.sha }} — a full sha from the fixed event payload, so the PR head cannot supply the workflow definition that judges it.
.gitea/workflows/review-verdict.yml — changed-file enumeration UNSAFE-KNOWN Delegates to scripts/pr-changed-files.sh with the head sha and expected base, and therefore inherits that row's residual, not a pin — this row said "inherits that row's pins" while the row it points at was being graded down, which is exactly the stale-cross-reference a multi-round edit produces. Accepted on better terms than the enumerator alone — this is the one caller that also runs the monotonic change_target_branch event-count fence (ci.verdict-write-retarget-fence) — but only for the BASE axis. The HEAD alias described in the enumerator's row (H1 -> H2 -> H1 during pagination) is unfenced here too, and this row previously implied the fence covered it.
.gitea/workflows/docker-build.yml — CI toolchain image UNSAFE-KNOWN Graded down by cold review, correctly: this file's own definition of PINNED names an image digest, and ersatztv-ci:<short-sha> is a mutable tag. A registry tag can be repointed after ci-image-pin verifies it and before a job pulls it, and a 7-hex short sha is additionally collision-prone. Accepted rather than fixed here because the exposure needs write access to our own LAN registry — i.e. an attacker already inside the trust boundary — and two controls bound it: jobs never consume :latest, and pr-checks.yml's ci-image-pin fails the build if the tag drifts from the last commit touching docker/ci/. Consuming image@sha256:… is the real fix and is the natural companion to #772, which already covers the availability half of this tag's weakness.
.gitea/workflows/docker-build.yml — release smoke pull UNSAFE-KNOWN Pulls ${IMAGE}:${SMOKE_SHORT_SHA}, the tag this same job pushed moments earlier. The first draft called that PINNED on the strength of the job's concurrency group; cold review showed the group is per-ref, so a branch build and a tag build of the same commit sit in DIFFERENT groups and can publish the same :<short-sha> — the smoke step can therefore pull the other run's image. Accepted rather than fixed here because the fix is the same one-line change as the row above (pull image@sha256:…, propagated from the push step) and belongs with it; until then no registry row in this file claims to be pinned.
.gitea/workflows/docker-build.ymlapi-docs / format base fetch UNSAFE-KNOWN Fetches the live base tip to diff generated artifacts, with nothing pinning it. Graded up from N/A by cold review, which was right that "advisory" was too quick: these are not branch-protection-required contexts, but the merge-consent hook reads Gitea's combined status, and a combined state that is not success blocks the auto-grant — so a wrong answer here does participate in merge consent. Accepted because the failure direction is benign: a base that advanced mid-job makes a generated artifact look stale and FAILS the job, costing a re-run, rather than passing something it should not.
.gitea/workflows/ci-image.yml — verify the pushed image UNSAFE-KNOWN Pulls back the :<sha> tag it pushed in the immediately preceding step. Same demotion and same reason as the two rows above: a registry tag is not a digest, and anything able to write to the registry can repoint it between the push and the verify, which would make the verification confirm an image other than the one built. Accepted on the same terms — the exposure requires registry write access, i.e. an actor already inside the trust boundary — and the same fix applies.
.gitea/workflows/pr-checks.ymlprove-fix PINNED base/head are full shas from the event payload, immune to later PR mutation.
.gitea/workflows/pr-checks.ymlci-image-pin N/A A purely local comparison — git log over the checked-out tree against a literal in docker-build.yml. It never queries the registry, which is exactly why it cannot bound the retagging the image rows above describe.
.gitea/workflows/pr-checks.ymldocs-reminder base fetch N/A Fetches the live base tip, but the result only selects the text of a non-blocking warning. The rationale has to be narrower than the first draft's "the job cannot fail and never reaches the combined status", which is false — any job's status joins the combined state, and runner or checkout failure can redden it. What is true, and is what earns the N/A: the fetch and diff are failure-swallowed, so the remote read cannot change this job's outcome, only the warning's wording. It draws no authorization from what it reads, this file's second N/A clause. Grouping it with decisions-guard over-demoted it, and the two have different classifications, so per the rule above they get separate rows. Since ersatztv#784 the job runs TWO advisory checks off this fetch (the parity-doc reminder and scripts/check-doc-narrative.py); the classification is unchanged because the second is advisory on the same terms — it exits 0 on every path, so a moved base still only changes warning text (its unproven arms are enumerated in docs.no-session-narrative).
.gitea/workflows/pr-checks.ymldecisions-guard base fetch UNSAFE-KNOWN Fetches the live base tip and diffs against it, unpinned, and unlike docs-reminder this job CAN fail — so its answer reaches the combined status the merge hook reads and therefore participates in consent. Accepted on the same grounds as the api-docs/format row: a base that moved mid-job produces a spurious FAILURE and a re-run, never a spurious pass, and it re-fetches fresh on every trigger with no state carried between runs.
.gitea/workflows/renovate.ymlrenovate/renovate:43 N/A Out of this class, in scope for a different one — see the limits below.
.gitea/workflows/dependency-scan.yml — NuGet advisory query N/A dotnet list package --vulnerable queries a live advisory database and fails the job on the report marker in the same step, so there is no check-then-act split. Worth one line anyway: a green here means "no advisories as of this run", which is a dated claim about mutable remote data rather than a property of the tree — which is why the scan runs on a weekly schedule instead of only on PRs.

Limits, stated rather than implied

  1. Unpinned dependencies are a different class and are not graded here. actions/checkout@v4, docker/build-push-action@v6, mysql:8.4 and renovate/renovate:43 are floating tags, and renovate.yml runs its one with a repo-writing token. But resolution and execution are the same step — there is no earlier check whose answer a later action trusts — so they are a supply-chain-pinning concern, not a check-and-use race. Listing them as N/A here records that they were examined and classified, not that they are safe.

  2. The population is files, not call sites. The test derives which files are in scope; it cannot tell that an existing file grew a second, unpinned read. That residue is the sites-in-code limit named in testing.guard-derives-population-from-source — it needs find-all-references tooling, tracked in #777 — and it is why the Note column is prose a reviewer reads rather than a field a script checks.

  3. Scope is hand-written; the population inside it is derived, with no content filter at all. Scope is every file under scripts/ (*.sh, *.py), .claude/hooks/, .husky/ and .gitea/workflows/ — a reviewed policy choice. Inside it, every file gets a row, and a file that reads no remote state earns an explicit N/A rather than silently staying out.

    The first version filtered that scope by an outbound-network token list and argued the filter was a scope choice rather than a population filter. Cold review rejected the distinction, and the evidence settled it: the list omitted git fetch, which is this repo's most common remote read, so prepush-rebase-check.sh — which fetches origin/main and derives a push decision from it — was structurally invisible to a guard claiming to cover "every executable that reads live remote state", along with three others. The defence offered was that over-inclusion is the safe direction; the filter also under-included, which is the direction that costs a blind spot. Enumerating the directories costs more rows and has none.

  4. Nothing here checks that a PINNED claim is true. The test asserts every in-scope file has a row and that the classifications come from a closed vocabulary. Whether a row is honest stays a review responsibility, and this table is what review reads — the same split, and the same admitted residue, as docs/guard-inventory.md.