Extend the machine-checked guard inventory to inline workflow-job guards #786
Closed
opened 2026-08-13 21:59:11 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#786
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Spawned by #774.
scripts/tests/test_guard_inventory.pyderives its population from guard files — a glob of.claude/hooks/and.husky/plusscripts/*references scraped from workflows and hooks. Guards implemented inline in workflow YAML are outside that population.docs/guard-inventory.mdstates the limit explicitly rather than implying coverage, but the limit is real.The one that motivated #774 lives exactly there:
pr-checks.yml:ci-image-pinstated an invariant it did not check. It was fixed by adding a structural test, not by bringing it into the inventory.The blocker is that "which jobs are guards" needs a judgement call per job that the filesystem cannot supply — so it needs the same reviewed-registry-cross-checked-both-ways shape
TOOLCHAIN_JOBSuses intest_ci_image_pin_population.py. 15 jobs across 6 workflows.Also worth folding in: several jobs have no dropped-step guard at all (
functional-e2e,build,api-docs,format).build's absence is self-documented as a known residual; the other three are not. Decide per job and record the decision, rather than extending the marker protocol reflexively —test_ci_dropped_step_guard.pyalready argues why the ban is scoped to where a drop is consequential.Done-when
docs/guard-inventory.mdscope limit updated to match the new coverageClaiming for this session (Claude Code / Opus 5, orchestrator).
Bundling #786 + #789 — they are one mechanism, and working #786 alone would build the artifact #789 removes:
TOOLCHAIN_JOBSuses".TOOLCHAIN_JOBSis itself the defect and must be deleted in favour of a machine-readable per-job marker declared in the workflow, with both sets derived from the parsed YAML.So the bundle resolves the tension in one direction: one per-job execution-class declaration owned by the workflow, both populations derived from it, unknown/missing marker a hard failure. Registry-shaped hand lists go away rather than multiply.
Progress and any scope cuts will be recorded here.
PR #872 is open, covering this and its sibling — the two were bundled because working either alone
would have built the artifact the other removes.
Review: four cold adversarial rounds, alternating model families (Claude / Codex gpt-5.6 /
both in parallel / Claude), each from a review-only brief in an isolated worktree. Severity
converged BLOCKER/HIGH → MEDIUM → LOW/NIT; the final confirmation pass verified the substance clean
(detector-extraction equivalence over 4000 randomized cases per detector, zero mismatches) and its
one blocking finding had already been fixed a commit before it reported.
The recurring defect class was prose drifting from code — over half of all findings, including a
mechanism claim in the decision record that execution refuted. Every factual claim in the changed
docs and comments has since been re-verified by execution.
Two corrections to the issues themselves, both measured rather than assumed, are recorded in the
closing records.
Awaiting CI on
d3f470f, then the H10 verdict and the merge gate.Closing record
Outcome: Closed by PR #872. Every job in all six tracked workflows now declares
env.CI_JOB_ROLE(guard/advisory/none), andscripts/tests/test_workflow_job_guards.pyasserts set equality both ways between the guard-declaring jobs and a new Workflow-job guards
table in
docs/guard-inventory.md. A missing or unrecognised marker is a hard failure. Thescope-limit entry that said this class was uncovered is closed in place, with its two residuals
stated. The four jobs named here as lacking a dropped-step guard each carry a recorded decision.
Root cause: the inventory derived its population from guard FILES, so a guard implemented inline
in workflow YAML belonged to no population at all — which is where
pr-checks.yml:ci-image-pinwassitting when #774 found it stating an invariant it did not check.
Decisions/conventions changed: added
testing.workflow-declares-its-own-job-metadata. Bundledwith #789, whose
TOOLCHAIN_JOBSliteral this issue proposed to COPY — the record resolves thattension in #789's direction (derive from a workflow-declared marker; never a literal in the checker).
Reusable knowledge:
guardjob's output is averdict; a
nonejob's output is an artifact, and its red means the build did not work ratherthan that an invariant was violated. The more natural-sounding rule — "a guard enforces an
invariant about the REPOSITORY, so a job exercising the PRODUCT is
none" — was tried, reads asprincipled, and is WRONG: it puts
testandmigrationsoutside the table, and those are the twoREQUIRED status contexts on
main, i.e. exactly where a failure to fire is fail-open againstbranch protection. Recorded in the doc as a rejected alternative so it is not re-adopted.
both-ways shape
TOOLCHAIN_JOBSuses" while #789 was open to DELETE that shape. Working it aswritten would have built the artifact the sibling issue removes. Check an issue's proposed
mechanism against its open siblings before implementing it.
Verification:
scripts/tests1185 passed / 2 skipped on the final rebased tree (baseline onmainwas 1083). Thenew checker's declared mutation runs as its own harness case. Two cold adversarial reviews; the first
returned BLOCKED with four findings, all fixed and each demonstrated.
Deferred:
test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_logreddenswhen a second Claude session runs concurrently — it snapshots mtimes under the shared
~/.cache/ersatztv/hook-fire/. Observed once here with three session logs written during the window;green in isolation before and after. That is #809 / #822, and evidence is posted there.
Docs updated:
docs/guard-inventory.md(new section, scope limit, file-populations row, counts),docs/ci-cd.md("Per-job declarations"),docs/README.md(task-signal row),docs/decisions/records/testing/workflow-declares-its-own-job-metadata.md+ regenerated catalog.