Convention: for a predicate over shell or config TEXT, pin the artifact whole — "match a shape" is the exception that must argue for itself #901
Open
opened 2026-08-30 18:44:42 +02:00 by timothy
·
1 comment
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#901
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.
Two sessions reached the same answer today from unrelated subjects, each about two rounds later than this repo's own recorded standard. That is the signal for a convention rather than two area-specific records.
The measurements
#887 (image build). A guard parsed shell text to decide whether a command runs the SPA suite. Nine defects, one mechanism, across three cold-review rounds: executed heredocs treated as inert data (BuildKit does run
RUN <<EOF) while an opener matched inside quotes and blinded 303 lines of a real workflow;shlex.shlexnot clearingcommentersthe wayshlex.splitdoes, so#truncated a command mid-word — including the live${#reports[@]}idiom; compound punctuation welding two commands;npm t,./node_modules/.bin/vitest,pnpm vitest,timeout …,su -c …all invisible;true || npm testcounted as a run it never performs. Withdrawn; command TEXT pinned instead.Then the replacement was attacked. A partial match of
web/vite.config.tswas defeated seven further ways across three more rounds —test: {,test: {,test : {,"test": {, the same forplugins:, plus two that never touched the marker at all ([…].concat([evil]), and a trailing...moreTestspread —defineConfigis the identity function, so a later spread replaces what the pin matched). Withdrawn again; the file is pinned whole.#891 (hook fire-log sink). A lexical rule over the
ETV_HOOK_FIRE_LIBpreamble was defeated by${VAR:-<self-location>}, then backticks and$((…)), then$(printenv VAR)/$1/$?, then an indented orexported reassignment a column-anchored regex cannot see, then$'…'quoting making the required token literal. Five spellings, one mechanism. Withdrawn; byte-identity of the preamble's two lines pinned instead.The proposal
The boundary is the grammar, not the file type (main-3-bb, and it is a correction to this issue's first framing). One of these cases was shell source and the other was config blocks, so "shell or config TEXT" looked like the common factor. It is not: the common factor is that the artifact has a grammar and the predicate approximates it. TOML, YAML,
jqexpressions and JSON5 all qualify on that test and none of them would have been caught by the narrower wording.A pin cannot be defeated by a new spelling, because it does not have to recognise one in order to reject it. Its failure direction is a false RED — a human reading a diff they should have read anyway. A shape-matcher's failure direction is a false GREEN.
Stated cost, so it is not discovered later: any edit to a pinned artifact reddens, and the pin must be updated deliberately in the same commit. That is the same review trigger
KNOWN_GATE_CONDITIONSandPINNED_STAGE_COMMANDSalready carry.Two riders, both measured rather than reasoned
A pin assumes it is pinning the artifact that still DECIDES. Every hole found in #887 after the first withdrawal was authority moving where the pin was not looking: to another FILE (
vitest.config.*andvite.config.js/.mjsboth outrankvite.config.ts— read from vite's ownDEFAULT_CONFIG_FILES), another OCCURRENCE in the same file (a decoy firsttest: {), another WORKFLOW (aneeds:edge naming a job calledtestthat is not this one), or a HOOK the pinned command invokes (a vite plugin'sbuildStart(), an npmprebuild/preinstalllifecycle script). Ask of any new pin: what else could decide this, and would the pin still match?Widening a clause can retire its own evidence. From #891, and the sharper of the two. A
survived_clauseasserts a finer mutation still SURVIVES — its precondition is that the guard is deliberately coarse there. Widen the main clause and that precondition disappears; the canary then does not fail, it becomes a tautology, which reads exactly like a passing proof. So: when a clause is widened, re-derive every proof calibrated against the narrow one.Why this is worth a record rather than two comments
docs/guard-inventory.mdalready carries a precedent —test_no_redirection_in_the_sink_PRECEDES_its_stderr_redirectwent four iterations and was deleted rather than patched a fifth time. Both sessions today had that precedent available and neither applied it until a reviewer named it back: #887 took nine rounds then seven more, #891 took five. The existing lesson is recorded per-incident, so it is only found by someone already in the same incident. A convention record resolvable by topic throughdocs/decisions/README.mdis what makes it reachable before the third round rather than after.The threshold is not a count of spellings
This issue's first draft implied one ("about two rounds late"), and that is the wrong instrument — main-3-bb's correction, and it is better than what it replaces:
It generalises past this class, it needs no number, and both sessions had it available and misread it. Rounds 2 and 3 of #891 both had that property and were read as "one more fix" both times; #887 had it for most of nine rounds. Notably, neither session withdrew because the count got high — both withdrew when a reviewer quoted the existing precedent back, which is evidence for the "findable only from inside the incident" problem rather than against it.
Scope note
Proposal, not a decision. The evidence above is measured; the rule is not. What still wants a judgement call: whether the whole-artifact pin should be the DEFAULT for new guards or only the remedy after a shape-matcher has been defeated once, and what a shape-matcher's exception argument has to contain to be accepted.
Refs #887, #891.
Body updated with two corrections from main-3-bb (#891), both better than the framing they replace, both handed over rather than edited in — recording that here so the attribution is not lost when the body is read on its own:
The boundary is the grammar, not the file type. "Shell or config TEXT" was pattern-matching on the two incidents we happened to have. The real test is whether the artifact has a grammar the predicate does not implement — which pulls in TOML, YAML and
jqexpressions that the narrower wording would have let through.The threshold is not a count of spellings. It is noticing that the NEXT spelling was found by the reviewer rather than by you. My original "about two rounds late" framing invited exactly the wrong reading: a session at round two would conclude it had headroom.
Both were volunteered by the session that was still mid-PR and stood to gain nothing from widening this one.