d3f470fda00731dcb3abce5cdfd755c48b814ee7
796
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d3f470fda0 |
fix(786): reconcile the guard-inventory counts after rebasing onto #871
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 8s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 21s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
Review verdict / Set review-verdict status (pull_request_target) Successful in 11s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 7m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
review-verdict/h10 Review-verdict: MERGEABLE @ d3f470f (base: main)
Rebase-only conflict, caught by re-running the gate on the NEW base rather than trusting the green from the old one. #871 (`docs(796)`) added `scripts/tests/test_mcp_smoke.py` as a MUTATION-graded guard row while this branch added `scripts/tests/test_workflow_job_guards.py` as another. Each bumped the hand-maintained summary sentence by one and each was green against its own base; merged, the table holds 44/22 and the sentence claimed 43/21. This is `ci.whole-tree-gate-is-only-as-green-as-its-base` — the shape #780 and #784 hit. The summary is asserted by `test_the_summary_counts_match_the_table`, so it failed loudly rather than drifting, which is the whole reason that assertion exists. Refs #786 |
||
|
|
9a2c4588d7 |
docs(786): drop a count the heading three lines above already carries
Confirmation review NIT. The paragraph said "That leaves the **four** jobs below" under a heading reading "The four jobs with no dropped-step guard" — a second hand-maintained copy of the same count, six lines apart, in the file that argues a hand-maintained summary of a table is a second copy of it. Nothing checks either. The heading keeps it; the prose does not repeat it. The review's blocking finding — that the scope-gap paragraph claimed both publish jobs carry `if: github.event_name != 'pull_request'` — was already corrected in 4a2c76027, one commit after the tree it reviewed. `ci-image.yml` has no `if:` and no `pull_request` trigger; the docstring says so. Refs #786 |
||
|
|
1ab50236c3 |
fix(786): share the last three detectors with their proofs, and close the round-4 wording items
Round four returned MERGEABLE with seven LOW/NIT items. All seven are fixed rather than shipped,
because three of them are the same defect class this branch has been closing all along.
THREE MORE PROOFS RE-IMPLEMENTED THE COMPARISON THEY NAMED. The previous commit extracted
`duplicate_row_faults` and `env_shadow_faults` for exactly this reason and left the set-equality and
Kind comparisons inline, so `test_MUTATION_a_guard_job_with_its_row_DELETED_is_reported`,
`…_a_PHANTOM_row_is_reported` and `…_a_row_whose_KIND_disagrees_is_reported` still rebuilt
`declared - listed` and the Kind check themselves. Review demonstrated it by flattening the live
assertion and watching the file stay green at 50 passed.
`coverage_faults(declared, listed) -> (missing, phantom)` and `kind_faults(roles, listed)` are now
named detectors that the live checks and all three proofs call, and each proof gained a negative
control. Witnessed after the change:
coverage_faults disarmed -> deleted-row proof REDDENED, phantom-row proof REDDENED
kind_faults disarmed -> kind-disagreement proof REDDENED
Worth recording that the system was never fail-open here: the manifest harness already caught the
same disarm ("the named test still PASSED with the clause mutated"). The in-file proofs were weaker
than their names, not absent.
THE ENV-MERGE EVIDENCE WAS STEP-SCOPE USED FOR A JOB-SCOPE CLAIM. `build`'s Smoke step shows the
runner LAYERS step-level `env:` over workflow-level, and what this branch newly relies on is
job-level layering. Measured: on origin/main no job anywhere declared both a job-level `env:` and a
consumed workflow-level var, so the job scope had no in-repo witness. The gap is now named, with
the exposure bounded — the two publish jobs are `if: github.event_name != 'pull_request'`, so PR CI
cannot exercise it and a non-layering runner would fail loudly on the first push to `main` trying to
publish `":<sha>"`, not silently.
Four wording fixes, each measured:
* the record still enumerated "exactly three jobs are `none`" while three other sites say a list
in prose is a second copy that rots — the fourth site now agrees with them.
* "the remaining `docker-build.yml` jobs" over-scoped: six remain after `test`/`migrations`, and
the table has four, because `toolchain-preflight` and `scan` carry the mark/assert protocol
voluntarily without being in the enforced scope. Said so.
* "on every push to `main`" ignored the Smoke step's own `docs_only` gate.
* `docs/ci-cd.md`'s excluded-token list omitted `tar`, which the source comment includes; and the
`scan` Proof cell disclosed the two pytest files' `NONE` but not the cited script row's, two
cells from a sibling that discloses it — so that row now states it claims no proof at all.
Suite: 1178 passed, 2 skipped (the production-log guard deselected; it is the shared-$HOME flake,
green in isolation — #809/#822). ruff 0.12.11 clean over all 47 tracked files; decisions-validate
OK; doc-narrative 0 warnings.
Refs #786
Refs #789
|
||
|
|
f45b0593ca |
docs(786): reconcile the three remaining statements of the Proof: NONE rule
The rule was hedged in `docs/guard-inventory.md` when review found two inline rows citing a pytest despite it. The record's `rule:` frontmatter, the record body, and the checker's module docstring kept the universal wording — the same fix-one-site-leave-the-paraphrase pattern this branch has now hit four times, which is why the sweep was by the RETRACTED words rather than the new ones. All three now say the same thing: an inline row carries `Proof: NONE` unless a pytest genuinely covers part of it, which `ci-image-pin` and `set-verdict-status` do, and each names which part — because a reference covering a fraction must not read as covering the whole. Refs #786 |
||
|
|
3fc3de0556 |
docs(786): name the assertion, not the file, where a cited file's Kind would mislead
Self-review sweep over every script the new table cites, cross-checked against that script's own Kind in the file-guard table above. Two rows credited a script with coverage it does not provide: * `functional-e2e` named `scripts/e2e-ui.sh` as an assertion site. It is `TOOLING` — it DRIVES the Playwright specs, and the UI assertions live in those specs. `scripts/e2e-functional.sh` is the only `GUARD` of the three; `e2e-local.sh` only boots the instance. Now says so. * `scan` cited `test_ci_dropped_step_guard.py` and `test_ci_release_path_scan_job.py` and pointed its Proof at "those files' rows above". The first is graded `PROOF` (for its `ci-step-ran.sh` role) with Proof `NONE`, so the reference read as coverage that row does not carry. The cell now names the assertion itself — `test_the_delimiter_banned_jobs_have_NO_expression_delimiter_in_any_run_body`, which does live in that PROOF-graded file — says which of the three is the GUARD, and states that the two pytest files carry `NONE`. The rule this applies: a cited file's Kind is a property of the file, not of the claim being made about it, so citing a `TOOLING` or `PROOF` file as an assertion site needs the assertion named or the reference reads as stronger than it is. Refs #786 |
||
|
|
e855a79999 |
fix(786,789): make the new proofs exercise the shipped detectors, and rename advisory -> report-only
Round three, reviewed twice in parallel (Codex gpt-5.6 and a cold Claude brief). Both converged
independently on the same two MEDIUM findings, which are the branch's own defect class reproduced
by its most recent commits: a test asserting a guarantee it does not provide.
1. THE PROOFS DID NOT EXERCISE THE CODE THEY NAMED. `test_MUTATION_a_DUPLICATE_row_is_reported`
rebuilt the comparison with its own `len(set(...))`, and `test_MUTATION_a_SHADOWING_env_key_is_
reported` built a synthetic dict and intersected it by hand. Both stayed GREEN with the live
check disarmed — a behavioural test wearing a MUTATION name, which is exactly what
`testing.guard-ships-with-mutation-proof` excludes.
`duplicate_row_faults(rows)` and `env_shadow_faults(docs)` are now named detectors; the live
test and its proof both call them. Verified by reproducing the reviewers' own demonstrations
against the fix:
duplicate detector neutered -> REDDENED "the DETECTOR did not report a second row for ..."
shadow detector neutered -> REDDENED "the DETECTOR did not report a shadowing job-level key"
2. `test_a_commented_out_marker_does_NOT_count` CLAIMED TO GUARD A TEXT-SCAN REGRESSION AND DID
NOT. It re-implemented the read inline and never called `declared_roles`. One reviewer built a
comment-blind grep reader reproducing all 17 current values byte-identically and ran every
non-parametrised test in the file: nothing went red.
`declared_roles(paths=None)` now takes an injectable population, so the test drives the REAL
reader over a fixture, asserts the fault function reports it, and pins that a live marker in the
same shape is still read (so the assertion cannot be satisfied by a reader returning None for
everything). Verified: installing the grep reader now reddens it.
RENAMED `advisory` -> `report-only`. docs/ci-cd.md already calls `functional-e2e` an advisory job
in a DIFFERENT sense — not a required context, though it can certainly fail — while this branch
declares that job `guard`. A reader would infer the wrong marker for exactly the job whose marker
is `guard`. The value is now unambiguous, its Kind is `REPORT-ONLY`, and the manifest's declared
`expect` string moved in lockstep with the message it matches.
The rename also caught its own adjacent defect: `REPORT-ONLY` contains a hyphen, which the row
regex's `[A-Z]+` Kind group did not match, so the docs-reminder row stopped parsing. That surfaced
as a loud red naming the job — the fail-closed property documented one commit earlier, working.
Widened to `[A-Z-]+`, matching the `BEHAVIOUR-ONLY` convention in the sibling table.
Nine further accuracy findings, each measured before fixing:
* "would therefore flag every job" — false. git/jq/python3/curl flag 4 of 8. And the REASON was
wrong too: `small` is git-only and provisions Python via actions/setup-python, while `build`
runs on stock ubuntu-latest, so "the bare runner has them" conflated two lanes. They are
excluded because they do not DISCRIMINATE.
* "four lines from the container: block" / "in one diff hunk" — false. Measured 6, 58, 6, 7, 7;
`migrations` has the whole `services:` block in between. The property the guard rests on is
INDEPENDENCE, not proximity, and the docs now say so.
* "the merge semantics never have to be relied upon" — too strong. The check proves no NAME
COLLISION; it does not prove the runner merges. That evidence is in-repo and live and is now
cited: `build`'s Smoke step declares its own step-level `env:` and consumes workflow-level
`${IMAGE}` on the same line, on every push to main.
* "every malformed row fails closed" — over-broad. A malformed DUPLICATE is invisible: its job is
already satisfied by the well-formed row. Stated.
* The `scan` Blocks cell said "a marked job's run: body"; the ban's scope is DELIMITER_BAN_JOBS
(the required contexts PLUS `build`), which is wider, and `scan` carries no `if:` so it runs on
PRs too.
* The functional-e2e row credited `e2e-local.sh` (TOOLING, only boots the instance) for curl
assertions that live in `e2e-functional.sh` (GUARD).
* `report-only` was defined nowhere; now defined as a verdict the job cannot enforce, with the
limit stated (nothing detects a `guard` job whose checks are all continue-on-error).
* "inline rows carry Proof: NONE" was stated universally while two rows cite a pytest; hedged,
and each such row names which PART it covers.
* "Exactly three jobs are `none`" was an unchecked prose enumeration in three sites, in a branch
that deleted one for that reason. Removed; the markers are the source.
* Plus: the ci-cd tool list was short by `dotnet-ef` and `playwright`; docs/README said only a
`guard` job needs a row; "all three checks fail together" read as the opposite of "go blind";
and #774's unrealised consequence is now subjunctive rather than past tense.
Suite: 1178 passed, 2 skipped, 1 known flake (test_hook_fire_log's production-log guard, which
compares mtimes under the SHARED ~/.cache/ersatztv/hook-fire/ — four session logs were written
during the window; 78 passed in isolation. ersatztv#809/#822, evidence posted there).
Refs #786
Refs #789
|
||
|
|
ae4bf8b662 |
fix(786,789): guard against env shadowing, and close the weaker proof shapes
Second cold re-review (Claude, cold brief, isolated worktree) of the tree at 0944877bf. Its BLOCKER (the retracted rule surviving in four sites) and its duplicate-row finding were already fixed in d8a507a3b, which it did not see. Six further findings remain and are fixed here. JOB-LEVEL env COULD SHADOW A MEMORY-CRITICAL WORKFLOW-LEVEL ONE. Raised as an unmeasured assumption, and it is the one worth acting on: before this branch NO job in docker-build.yml declared `env:` at all, and that workflow's workflow-level `env:` carries UseSharedCompilation, DOTNET_CLI_USE_MSBUILD_SERVER and MSBUILDDISABLENODEREUSE — what keeps a 7.8 GB VBCSCompiler off a RAM-oversubscribed runner (#406). Actions merges the scopes with the more specific winning, so a same-named job-level key replaces the workflow value and the loss is INVISIBLE: the job still runs, just with the shared compiler back on. Rather than measure the merge once and write the answer in prose, the reliance is removed: test_no_job_env_key_SHADOWS_a_workflow_level_one asserts no job-level key collides with a workflow-level one, in any tracked workflow. Measured clean today across all six. Witnessed red by planting `UseSharedCompilation: true` on `test`: docker-build.yml::test declares job-level ['UseSharedCompilation'], which also exists at workflow level. The job value WINS and the workflow value is silently lost. TWO PROOFS WERE WEAKER THAN THEIR TWINS. * test_MUTATION_a_guard_job_with_its_row_DELETED_is_reported had no negative control, so a PRE-EXISTING missing row satisfied every iteration. Its phantom twin got one in c00126b82 and this one did not — the same asymmetry, one commit apart. It now pins `declared - listed` to empty first and asserts the deletion is reported as EXACTLY that job. * test_a_commented_out_marker_does_NOT_count asserted that PyYAML ignores comments — a fact this suite does not own and that no change here could break. It now writes a workflow to tmp_path, drives the real `role_faults`, and asserts the commented text IS present in the file, which is the actual guard: a reader that ever became a text scan would find it. A PROSE COUNT SHIPPED FIVE LINES ABOVE THE ONE 0944877bf DELETED. "Only three jobs are `none` —" enumerated the workflow by hand, unasserted, in the same file where the previous commit replaced a count with an invariant for exactly that reason. Removed: which jobs are `none` is read off the markers, and the table is the only enumeration a check keeps honest. Three accuracy fixes, each measured: * docs-reminder: "all three check steps carry continue-on-error" is off by one — there are TWO check steps plus a `Set up Python` between them, all three with the flag. The substantive claim (no finding can fail the job) was right. * The two REQUIRED-context strings dropped the ` (pull_request)` suffix that .gitea/required-status-contexts.json carries, so a reader copying one into branch protection would get a context that never reports. Both now match the file exactly, verified against it. * The `_FULL_LINE_COMMENT` comment read as though stripping cured a live red. It did not: measured, ZERO toolchain tokens appear in the raw `run:` bodies of all three bare-runner jobs, comment lines included. It is preventive, and now says so — along with why it cannot manufacture a match (`.sub` on a `^…$` match leaves the newline, so no lines are joined). The re-review independently re-derived the verdict/artifact classification across all 17 jobs and would not reclassify any, and confirmed the three earlier de-vacuuming fixes hold under execution. Suite: 1179 passed, 2 skipped. ruff 0.12.11 clean; decisions-validate OK. Refs #786 Refs #789 |
||
|
|
402032db98 |
fix(786,789): sweep the retracted rule out of three sites, and reject duplicate table rows
Cross-family cold re-review (Codex/gpt-5.6) returned BLOCKED with four findings. All verified independently before fixing. 1. HIGH — THE RETRACTED RULE WAS STILL STATED AS ACTIVE IN THREE PLACES. The previous commit replaced the guard/none rule in docs/guard-inventory.md and flipped `test`/`migrations` to `guard`, but left the superseded wording in test_workflow_job_guards.py's module docstring, docs/ci-cd.md, and the decision record's `rule:` — from which the generated catalog (and MemPalace) copied it faithfully. Those three explicitly called `docker-build.yml::test` `none`, which is the opposite of what the workflow now declares. A maintainer following the ACTIVE decision record could downgrade both required jobs, delete their rows, and every structural check would stay green, because the checkers read the markers and the markers would agree with the stale prose. This is `process.enumerate-clauses-to-close-a-sweep` failing on my own retraction: I fixed the site I was looking at and left the paraphrases. All three now state the verdict/artifact rule, and each carries the rejected reading explicitly so it does not come back. Swept by grepping the RETRACTED words, not the new ones — the only hits left are the deliberate rejected-alternative notes. Same commit: docs/ci-cd.md claimed "every job in every workflow declares two things", which its own table contradicts — `CI_EXECUTION_CLASS` is docker-build.yml only. 2. MEDIUM — A DUPLICATE ROW WAS SILENTLY COLLAPSED. `inventory_jobs()` built a dict, so two rows for one job overwrote each other, last-one-wins, and set equality still held both ways. The realistic arrival is a merge conflict leaving an ADVISORY and a GUARD row for the same job: the registry contradicts itself and its checker reports green. Rows are now kept as a list and counted against distinct keys by `test_no_job_has_TWO_rows`, with a mutation proof carrying its own negative control. Witnessed: planting a contradictory duplicate row for `pr-checks.yml::ci-image-pin` in the real file reddens two named tests; restoring it returns 48 passed. 3. LOW — the toolchain-preflight row said it blocks "every downstream job". Measured: NOTHING `needs:` it (only `build` has a `needs:`, on test/migrations/scan), and it is not a required context. Corrected to what it actually blocks — a merge, through the combined commit status the consent gate reads (#598) — and why it must not consume the image it checks (#772). 4. LOW — the predicate's documented residue was partial. Now enumerated in both directions: false positives (trailing comment, echo string, heredoc body — stripping is line-based, not shell-aware) and false negatives (absolute path, script indirection, a `#` heredoc line that a later command strips and executes). One of those claims was WRONG as first written and is corrected to what execution shows: variable indirection is invisible ONLY when the assignment is not in the same run body. `$SDK_CMD build` with `SDK_CMD` from `env:` is missed; `SDK_CMD=dotnet` written in the same body IS caught, because the assignment line carries the bare token. Re-review confirmed the four earlier findings are genuinely fixed: the classification is consistent across all 17 jobs under the verdict/artifact rule with no job it would reclassify; both repaired positive controls redden on an emptied population; the corrected functional-e2e claim is true and the old wording has no matches; the adjacency residual is accurately disclosed and not overstated; the summary counts assertion holds at (43, 6, 21, 21, 6, 16). ruff 0.12.11 clean; decisions-validate OK; targeted suites 145 passed. Refs #786 Refs #789 |
||
|
|
2d1a9ff531 |
fix(786): state the dropped-step invariant instead of a step count that was wrong
Self-review of the rows added in the previous commit. They said `test` had "13 of 19 steps marked" and `migrations` "7 of 9". Measured: 12 of 19 and 6 of 9 carry `ci-step-ran.sh mark`. The 13th and 7th steps carry `assert`, not `mark` — an aggregate of two different things, copied without re-deriving it. Replaced with the invariant rather than a corrected number: every consequential step marks and the final step asserts, which is what test_ci_dropped_step_guard.py actually enforces. A count in prose is a second copy of the workflow that rots on the next step added; the invariant does not. Refs #786 |
||
|
|
0c3625d2e2 |
fix(786,789): reclassify test/migrations, de-vacuum two controls, correct a refuted claim
Cold adversarial review of b7e5c35fb returned BLOCKED with four substantive findings. All four are real and all four are fixed here; each fix is demonstrated rather than asserted. 1. THE guard/none LINE EXCLUDED BOTH REQUIRED CONTEXTS. The rule was "a guard enforces an invariant about the REPOSITORY; a job exercising the PRODUCT is `none`". It reads as principled and put `test` and `migrations` outside the table — the two required Actions contexts on `main` per .gitea/required-status-contexts.json, i.e. exactly the jobs where a failure to fire is fail-OPEN against branch protection, and exactly the gap #786 exists to close. It also could not survive its own examples: `test` runs ToolCatalogTests.cs and web/src/api/*.guard.test.ts, the structural guards this document's own scope-limit item 2 names. Replaced with a line that can be restated: a `guard` job's output is a VERDICT; a `none` job's output is an ARTIFACT. A `none` job can still go red — that is an error in producing the thing, not a finding about the repo. `test`, `migrations` and `functional-e2e` flip to `guard` and gain rows; `none` is now exactly the two image publishers plus renovate. The rejected rule is recorded so it is not re-adopted. 2. TWO POSITIVE CONTROLS WERE VACUOUS — both of them ones this commit series added, while the pre-existing sibling it copied was not. `len(_MUTANTS) == 2 * len(population)` is `2N == 2N`, a tautology at N=0, so an emptied population satisfied it while the parametrised proofs collected zero cases. Both now assert non-emptiness FIRST. Demonstrated after the fix, by degrading each population to empty: role mutation set -> REDDENED: "declared_roles() is EMPTY ..." declaration mutation set -> REDDENED: "the workflow parsed to ZERO jobs ..." phantom-row proof -> REDDENED: "the table parsed to nothing ..." 3. A MECHANISM CLAIM IN A DECISION RECORD WAS REFUTED BY EXECUTION. Four sites (the test docstring, docs/ci-cd.md, the record's `rule:`, and the generated catalog) said `functional-e2e` reaches dotnet only through scripts/e2e-local.sh and so is invisible to the step scan. It is not: that job runs `dotnet restore`, `npm ci`, `npm run build` and `dotnet build` in its own YAML, and the derivation detects it. Measured: the blind-spot set (declared toolchain minus detected-from-own-steps) is EMPTY today, all five jobs detected. Reworded to say that, and the converse is still not asserted because a future job could be written behind a script. 4. THE ADJACENCY RESIDUAL WAS UNDERSTATED. All three checks fail together under one edit that drops the container block, flips the marker AND moves the invocation into a script — the review verified it. Now disclosed in the code and in docs/ci-cd.md, with what actually bounds it: the failure MODE, not its likelihood. The job then dies on a missing binary, loudly, where #774's original defect sent a REQUIRED check green on the bare runner. A silent pass traded for a noisy crash, not a hole closed. Also from the review: the docs-reminder row said the job "never fails" (its Checkout step can); the phantom-row proof could not fail (it supplied its own phantom, so it now pins the unmutated set to zero phantoms first); and the scope-limit paragraph was reworded from draft history into a rejected-alternative note. Review findings 6 and 7 (regex false positives on comments, false negative on `dotnet-ef`) were already fixed in 75747fbca, which the reviewer did not see. Suite: 1161 passed, 2 skipped. ruff 0.12.11 clean; decisions-validate OK. NOTE for whoever sees a red `test_hook_fire_log.py::test_the_suite_does_not_write_to_the_ PRODUCTION_log` locally: it snapshots mtimes under ~/.cache/ersatztv/hook-fire/, which is shared across worktrees and sessions, so a second Claude session running concurrently reddens it. Seen once here with three session logs written during the window; green in isolation, both before and after this change. That is ersatztv#809 / #822, not this diff. Refs #786 Refs #789 |
||
|
|
f420649fa2 |
fix(786,789): harden the step-text predicate and sharpen the guard/none rule
Self-review findings on the previous commit, all measured before fixing.
THE PREDICATE HAD BOTH FAILURE DIRECTIONS. Measured against constructed inputs:
full-line comment -> ['dotnet'] false POSITIVE
echo string / heredoc -> ['dotnet'] false POSITIVE
dotnet-ef migrations -> [] false NEGATIVE
playwright install -> [] false NEGATIVE
The false positive is the dangerous one: a bare-runner job whose COMMENT merely mentions dotnet
would redden the suite on a correct tree, and a correct guard going red on a correct tree is the
fastest route to that guard being deleted. Full-line comments are now stripped before matching.
`dotnet-ef` and `playwright` join the tool list, and the alternation is sorted longest-first so the
match does not depend on backtracking out of the `dotnet` prefix.
The residue is stated in the code rather than papered over: a TRAILING `# ...` comment, an
`echo "run dotnet build"` and a heredoc body all still match. Parsing those means parsing shell,
which `testing.fixing-a-parser-bug-introduces-the-next-one` prices at three or more rounds. It is
tolerable only because of its DIRECTION — a loud red naming the job and the token, with an obvious
remedy — never a silent pass.
Verified the real workflow's classification is unchanged and exact after the change:
needs-toolchain derives to {test, migrations, functional-e2e, api-docs, format}, equal to the
declared set, zero violations.
THE GUARD/NONE RULE WAS UNDER-SPECIFIED. "Purpose, not whether a step can fail" does not decide
`docker-build.yml::test`, which blocks the merge as a required context. Sharpened everywhere it is
stated: a `guard` job enforces an invariant about the REPOSITORY or its ARTIFACTS; a job that builds
or exercises the PRODUCT is `none` even when it is a required check. No marker value changed — the
rule now explains the classifications that were already there.
Also: a row for test_workflow_job_guards.py in the file-populations table, and the scope-limit
preamble de-narrated (it recounted an earlier draft; the operative warning — an unlisted class reads
as covered — is kept, the autobiography dropped).
Refs #786
Refs #789
|
||
|
|
20a6fed165 |
fix(786,789): workflows declare their own per-job metadata; guard populations derive from it
together. #786 asked for inline workflow-job guards to be enumerated using "the same
reviewed-registry-cross-checked-both-ways shape TOOLCHAIN_JOBS uses"; #789 said that
exact shape is the defect and must be deleted. Building #786 as written would have
constructed the artifact #789 removes.
Both now derive from a marker the workflow declares about itself:
env.CI_EXECUTION_CLASS toolchain | bare-runner (docker-build.yml)
env.CI_JOB_ROLE guard | advisory | none (every tracked workflow)
Missing or unknown values are hard failures in both checkers — a marker scheme whose
absent value defaults to a safe class stops applying the moment a job is added and
nobody notices.
it: set equality between two DERIVED sets is blind to a member leaving both at once, so
a job losing its container: block leaves the declared and pinned sets balanced. The
population needs an anchor that does not move with the block. The marker is that anchor
and is better placed — claim and reality in one diff hunk.
Its cost is adjacency, and is paid rather than argued away: a marker four lines from the
container: block can be deleted with it, where a distant literal could not. So the file
derives the requirement a THIRD time, from each job's own step bodies, independent of
both marker and block. That is also the only check that sees the failure #789 filed —
a .NET step MOVED into a bare-runner job, where no set changes at all. It is a
necessary condition only (functional-e2e reaches dotnet via scripts/e2e-local.sh), and
says so; the converse is deliberately not asserted.
test_workflow_job_guards.py asserts set equality both ways against a new
"Workflow-job guards" table in docs/guard-inventory.md. The marker records the job's
PURPOSE, not whether a step can fail: docker-build.yml::build runs a smoke test that
fails the job and is still `none`. Read the other way, every job is a guard and the
table distinguishes nothing.
The four jobs #786 named as having no dropped-step guard each get a recorded decision in
that section. None is a required context, so a drop there cannot send a required check
green; build's residual was already self-documented and is now indexed. ci-step-ran.sh
is deliberately not widened — its scope is derived from the required contexts (#787) and
test_ci_dropped_step_guard.py argues the ban belongs where a drop is consequential.
Two corrections to the issues, both measured rather than assumed:
* #789's "editing docker-build.yml re-points ci-image-pin's expected" is false.
expected="$(git log -1 --format=%H -- docker/ci)" — the pathspec is docker/ci only.
Verified: docker-build.yml was last touched by HEAD and expected still resolves to
|
||
|
|
e11d577193 |
docs(796): verification code is code under test, and the proof it was claiming an exemption from (#871)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 8s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m9s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m50s
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) Successful in 4m18s
Records `testing.verification-code-needs-its-own-proof`: the proof obligation follows the VERDICT rather than the file, so it binds harnesses, wrappers, timeouts and checkers — not only the files the guard population derives. The issue asked for a stated position on whether non-guard checker scripts get mutation proofs. The position as first written claimed `scripts/mcp_smoke.py` "cannot participate" because driving it needs the gitignored `.mcp.json` and a cold-built language server. Cold review refuted that by execution: it takes its config path and server name as positional arguments. The record had failed its own headline rule on the one claim its decision rested on, so this ships the proof instead of the exemption. - `scripts/tests/test_mcp_smoke.py` — a hermetic stub JSON-RPC responder and six cases pinning the defects the checker has already had, with the positive control as a fixture the refusal tests depend on, so a node-id or `-k` selection cannot skip it. - A declared clause in `mutation_manifest.py` targeting the unguessable request id, using the `guard=test / target=script` shape that already exists for `mutation_harness_lib.py`. Witnessed red: `id_init = 1` makes the pre-answer accepted at `initialize` (rc 9 -> 10), and only that test moves. `mcp_smoke.py` still gets no inventory row — one is rejected as a phantom (measured). The row goes to the test file, which joins the derived population automatically. Five cold-review rounds, four BLOCKED. Round 2 caught a `ruff format` red that would have failed `script-tests`. Rounds 3-5 found only hand-maintained counts and uniqueness claims in prose, three of them created by the previous round's fix; that class was deleted rather than corrected again, per this record's own stop-and-subtract rule. Docs updated in the same PR: `docs/README.md` task-signal map and `docs/guard-inventory.md` (row, summary counts, scope-limit item 6). fixes #796 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
609fd852c2 |
fix(763): page both /statuses/{sha} reads to a validated terminator (#868)
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 / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m10s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m2s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m9s
|
||
|
|
5fb9c8537a |
docs(747): re-verify the Gitea 1.25.4-pinned CI claims on 1.27.1, and measure the merge-gate semantics that were source-attested (#867)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 17s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m25s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m48s
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) Successful in 4m10s
Population derived from `git ls-files`, not the issue's 9-key list (~21 claim sites). Re-confirmed unchanged on 1.27.1: the distinct `skipped` commit-status state; `compare` serving no `files`; no agent-side cancel route (REST route + swagger only); `branches: [main]` suppressing the run off a non-main base. Newly measured on four throwaway scratch bases, `main`'s rule never PATCHed: an absent required context blocks an ORDINARY merge without needing `block_admin_merge_override` (that field governs the FORCE path only), and `enable_bypass_allowlist` with an empty list is NOT a substitute for it. Trap recorded: the PR API reports `mergeable: true` while such a merge is refused. Left explicitly dated with reasons: push-supersession auto-cancel, `pull_request_target` overlap, `--depth=1` no-merge-base, and the scope-enum/`reqRepoWriter`/403 items. Not a corpus sweep, and `ci.actions-credential-scoping` now says so. `review-verdict.yml` untouched — #763 holds that file. Five adversarial review rounds (21/12/9/6/2). Caveat: all same-model-family; Codex was rate-limited. fixes #747 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
462118d98d |
fix(763): round 9 — assert the repair's reason positively, not by absence
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 17s
PR Gates / Docs update reminder (pull_request) Successful in 21s
review-verdict/h10 Review-verdict: MERGEABLE @ 462118d (base: main)
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Failing after 14s
Review verdict / Set review-verdict status (pull_request_target) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Canceled after 1m31s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
A sixth cold review found everything in round 8 clean except one line, and it
is the rule this branch keeps rediscovering: the test pinned the new
`raced_why` only by asserting the ABSENCE of the borrowed wording. Measured —
replacing the string with `zzz` left the suite green while an operator would
get `::error::… — zzz.` beside a sticky sentinel. The sibling test 330 lines
away states the rule and follows it; this one did not.
Now asserted positively, with the em-dash and full stop discriminating the
`::error::` reason from the `::warning::` text that continues ", which cannot
be true". The `zzz` mutation reddens it.
Three nits from the same review, all verified by execution rather than reading:
- the earlier fixture's row was excluded by the strict `> $since` because the
mark became its OWN id, not because it sat below the mark.
- the predecessor comment said `main` "warned only on `null`". True of the two
EMPTY shapes being contrasted; an empty body and a non-array object warned
as well. Scoped.
- `docs/ci-cd.md` and the record described the `::error::` as a two-way split
(found vs unverifiable). Round 8's whole argument is that a complete read
returning an IMPOSSIBLE answer is a third case, not a variety of the second
— which is the operator-facing point, since it decides whether to go looking
for an API failure that never happened. Both now say three.
The review re-verified, by comment-stripped diff, that round 8 changed no
executable line beyond the `raced_why` string and the if/elif restructure, and
independently reproduced both inertness measurements and the `origin/main`
predecessor behaviour.
Verification: `scripts/tests` 1097 passed, 2 skipped; decisions_validate and
build_decisions_catalog --check exit 0.
refs #763
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e6f04cc739 |
fix(763): round 7 — reject an empty post-write history, and the fourth overclaim
A fourth cold review returned NOT-MERGEABLE on two Mediums. Both fixed, plus
its three Lows.
MEDIUM, and a defect this branch introduced. Tolerating a `null`/`[]` page 1 as
"complete, zero rows" is correct for the PRE-write caller — a head nothing has
posted to genuinely has no statuses — and impossible for the POST-write one,
which has just written a row to that sha. The body is well-formed, so nothing
retries it, and the walk reports success: `raced=0` concluded from a list that
cannot be real, on the one path whose failure direction is toward SUCCESS.
Worse than the code it replaced, which at least emitted a `::warning::` — a
logged fail-open had become an unlogged one. Reviewer measured both directions.
The post-write caller now rejects an empty result itself; the walk stays
caller-agnostic because the pre-write caller genuinely needs the empty answer.
This is NOT the withdrawn currency witness: that asked whether ANY row sat above
the mark, which an unrelated newer row satisfied while the rejection stayed
hidden, and it fired on schema-valid staleness. This asks only whether the list
is EMPTY — a state no unrelated row can produce and no ordering can disguise.
It carries neither defect. Proved by fixture; disarming it reddens the named
test, and the previously-uncovered `null`-at-page-1 clause is now covered too.
MEDIUM — the fourth overclaim of the same class, in the decision record body:
"Uncertainty must fail closed at both ends … Both repair now." The page-2 probe
was DELETED, not converted; it repairs nothing. It also contradicted the
record's own `rule:` ("the two directions are NOT symmetric") and the bullet
directly beneath it. Round 5 retracted this wording in `docs/ci-cd.md` only —
the sweep was by subject, not by the retracted words.
Also fixed:
- the record presented "an empty FIRST page is legitimate" as a property of
the walk; it is a property of the pre-write caller.
- `docs/ci-cd.md` called the numeric-only id comparisons a fix for mark
inflation; they are a TYPE guard, closing the string half. A corrupt but
genuinely numeric id still inflates the mark — not attacker-controllable,
since ids are server-assigned, and now stated rather than implied.
- `test_a_partial_mark_is_SAFE...`'s self-guard promised to detect that the
fallback ran; it keys on a warning emitted by a different condition, so
deleting the fallback left it green. Its sibling is what reddens; the
message now says what it actually pins.
- the order-faithful fixture appended the job's own POST after the reversal,
serving the NEWEST row on the OLDEST page — the opposite of DESC, in the one
fixture that exists to be ordering-faithful.
- "twice per walk" for the wasted sleep; it is once per walk, twice per run.
- a dead counter read in the DESC mode.
Rebased onto
|
||
|
|
89db6022d9 |
fix(763): round 6 — measure the partial-mark fallback under the REAL ordering
A fourth cold review (Opus, isolated worktree, tests/double/docs focus)
reported no correctness bugs in shipped behaviour but two coverage defects on
exactly the two things this change advertises. Both are closed.
The partial-mark fallback's safety is a claim ABOUT THE ORDERING — page 1 holds
the newest rows, so a walk that fails later still saw the true maximum. The
fixture pinning it served ASCENDING ids, i.e. the arrangement the design calls
unsafe, and passed anyway because the raced row's id sat above even the partial
mark. It could not distinguish safe from unsafe.
The stub now HONOURS the sort parameter: order-faithful modes serve DESC by
default and ASC when the request asks. The new fixture holds a PRE-EXISTING
base-mismatched verdict at id 7055 among 60 rows. Under DESC the salvaged mark
is 7059 and that row is below it — the exemption correctly stands. Under ASC
the mark would be 7049 and that untouched row tests as NEWER, a sticky repair
on a head nothing raced. So re-adding `sort=highestindex` now reddens by
BEHAVIOUR, not only by the structural assertion added in round 5. Measured:
re-adding it reds both tests.
Most modes stay ordering-blind on purpose and now say so: they test walk
COMPLETENESS, which is order-independent, and insertion order is what lets a
fixture place a row beyond page 1.
Also fixed:
- `null` is accepted as an empty page. An array-only gate is the exact shape
of #751 — `count_retargets` had one, the timeline really did return `null`
past the end, and the fence withheld EVERY exemption from the day it
shipped. The same narrowing here is worse, because this walk's failure is
the STICKY sentinel: every exempt PR would need a hand-posted verdict, per
head. Tolerating `null` cannot misread `[]`. Proved by fixture.
- the fail-closed comment said "past the 1000-row page cap"; the bound is 950,
as the walk's own comment and both docs already said.
- the docs claimed "only a read returning no rows at all abandons the mark".
False: a VALIDATED empty history yields a mark of 0 and is not abandoned —
that is the normal first run. What abandons it is a read that both FAILED
and returned nothing. Corrected in ci-cd.md and the record `rule:`.
- a comment pointed at the page-2 probe "a few lines further down"; it was
deleted, so the deixis pointed at nothing.
- the stub claimed its logical-read counter "is only reached on a SUCCESSFUL
page-1 serve" — measured false; it counts page-1 requests, retries included.
- five `(round N)` markers removed. A round number is session chronology and
does not parse for a reader who never saw it (`docs.no-session-narrative`);
an issue number does. The four that remain predate this change.
Verification: `scripts/tests` 1096 passed, 2 skipped. Thirteen executed
mutations across rounds 2-6. The reviewer independently re-ran the earlier
matrix and confirmed it, with one correction carried here: two of those
mutations redden MORE than their named test, so "each reddening exactly its
named test" was wrong — they redden at least it.
refs #763
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
53ee44cbc1 |
fix(763): round 5 — correct the overstated motivation, and fix the string-id twin
A third cold review (Opus, isolated worktree) returned NOT MERGEABLE with one High and three Medium. All are addressed. HIGH — the stated motivation was wrong, and self-contradictory once round 4 landed. Under the server default (`created_unix DESC`) page 1 holds the NEWEST rows and ids are monotonic with `created_at`, so page 1 already carried the true maximum id AND every row newer than the mark — the only rows the post-write check selects on. A single-page read therefore missed a raced verdict only if more than 50 rows were created INSIDE the write window, not merely on "a head with more than 50 rows", which the issue, the comments and the docs all asserted. Reviewer executed an order-faithful DESC stub: a page-1-only reader repairs identically to the full walk. What actually removed #761's stall is retiring #751's page-2 probe, not the paging. The walk still earns its place, for a reason now stated instead of the false one: it stops the gate's one fail-toward-SUCCESS path depending on an undocumented ordering the server honours only coarsely (page 1 came back `114,112,113,111,110`). That measurement was deleted in commit 1 and is restored, since round 4's safety argument rests on exactly it. MEDIUM/real defect — the string-id TWIN, live on `main` and one expression away from the fix already made: `select((.id? // 0) > $since)`. jq orders strings above every number, so a PRE-EXISTING row with `"id": "3"` reads as newer than any mark, is counted as having raced the write, and gets the sticky sentinel plus a false "was overwritten" on EVERY later run — a permanent per-sha stall no re-trigger clears. Now numeric-only, with a test. Also fixed: a non-empty history carrying no numeric id was collapsed to a mark of 0 (making every pre-existing row look newer); it is now reported unusable and the check is skipped. `sleep` no longer fires after the final attempt. Three unpinned clauses now have tests, each proved by an executed mutation: - the page cap is a refusal, not a terminator (1050-row fixture) - the `::error::` found-vs-unverifiable distinction (forcing `raced_why=human` reddened nothing before) - the walk requests no sort order — a structural guard on round 4's withdrawal, which nothing mechanical protected. It reads request LINES, not comments, since the withdrawal note names the parameter to explain it. Honest scoping, not new code: the test double is ordering-blind, so the paging tests prove WALK COMPLETENESS, not that a real raced verdict would otherwise be missed — under DESC it would not be. The stub comment and the docstrings now say so rather than implying the stronger claim. Docs: `ci-cd.md` and the record's `rule:` carry the corrected reachability, the DESC dependency of the partial-mark fallback, and both rejected alternatives stated as rejected alternatives rather than as draft chronology (`docs.no-session-narrative`). Verification: `scripts/tests` 1094 passed, 2 skipped; eleven executed mutations across rounds 2-5, each reddening exactly its named test; decisions_validate and build_decisions_catalog --check exit 0. refs #763 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1eb7b41fc1 |
fix(763): round 4 — withdraw the ASC sort, which inverted the partial-mark fallback
Round 3 added `sort=highestindex` to close a mid-walk-insert gap: under the server default (`created_unix DESC`) a row inserted while the walk is running lands at position 0, on a page already read, so the walk never sees it. That fix and the round-2 partial-mark fallback are incompatible. ASC puts the OLDEST rows on page 1, so an incomplete walk takes its high-water mark over the oldest rows — leaving every pre-existing row above the mark and read as "raced". That is a spurious STICKY repair on a head nothing raced, which is precisely the #761 failure this whole issue exists to remove. Under the default DESC the newest row is on page 1 by construction and ids are monotonic with `created_at` (measured), so a partial mark is at or very near the true maximum and "lower is safe" actually holds. Two defects from one mechanism again, so the mechanism goes rather than getting patched: the sort is withdrawn and the mid-walk-insert residual is ACCEPTED and documented. It is bounded — a row arriving after this job's POST is not one this job overwrote, and being newest it wins on the combined endpoint branch protection reads. Both the code comment and the docs record the withdrawal and the reason, so the next reader does not re-adopt it. Verification: `scripts/tests` 1090 passed, 2 skipped; the partial-mark mutation still reddens `test_a_PRE_WRITE_paging_failure_still_yields_a_usable_high_water_mark`; decisions_validate and build_decisions_catalog --check both exit 0. refs #763 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
007d2fd3df |
fix(763): round 2+3 — close the fail-opens the paging change introduced
Two independent cold reviews (Codex GPT-5.6 cross-family, and an isolated Opus agent) converged on the same blocker, which is fixed here along with everything else they found. BLOCKER — the mark walk turned a fail-closed case into a fail-open. The high-water mark gates the post-write race check entirely: `max_id_before=-1` skips it. Before paging, only a failure of the single page-1 request could reach that. Requiring a COMPLETE walk newly routed a page-2 hiccup, an over-cap history, or one malformed id on a later page into the same hole, so a human rejection racing the write was left green where `main` repaired. A partial list now still yields a mark: it can only be LOWER than the true maximum, which makes the check more eager, never blinder. Only a read returning no rows at all abandons it — the pre-existing #849 gap, unchanged and now asserted by a test so it stays visible. WITHDRAWN — the "currency witness". It produced two defects from one mechanism, which is the signal to remove rather than patch twice: counting ANY row above the mark does not witness this job's write, so a stale-but-valid snapshot carrying an unrelated newer row passed while hiding a rejection; and a schema-valid stale read is not retried, so one such response turned a transient anomaly into a permanent sentinel. The hazard has no mechanism here either — Gitea is a single instance with no read replicas. Removing it restores the pre-change exposure on that path, a non-regression. Also fixed, each a fail-open with a fixture and an executed mutation: - `.creator` is type-tested before indexing. `.creator.login` on a non-object exits jq 5 and `set -e` took the step down after the green was posted and before the repair. Reproduced by both reviewers. - the mark is the max over NUMERIC ids only. jq orders strings above every number, so one `"id": "99999"` passed the numeric gate and inflated the mark until nothing looked newer. - an unusable `raced` count now repairs instead of "not acting on it". - `sort=highestindex` (ASC, measured) so a row inserted mid-walk appends at the end rather than at position 0 on a page already read. An unknown sort value silently falls back to DESC, so this is insurance, not load-bearing, and the comment says so. - `ph_ok`/`ph_rows` renamed off `read_existing_verdict`'s `st_ok`. No live bug, but a name collision in a 1400-line step. Tests the reviews showed were missing, each proved by an executed mutation: - verdict beyond a SHORT page (a deliberately unfaithful truncated response — against a faithful double a short page is always the last, so the rule "terminate only on an EMPTY page" was unobservable) - pre-write paging failure still yields a usable mark - pre-write read returning nothing abandons the mark and says so - a TRANSIENT page failure is retried (the retry was unproven code: every other error mode fails on every attempt, so disarming it reddened nothing) - a string id cannot inflate the mark - a malformed `creator` row does not kill the job Stub corrections, both the same class as the earlier `[]`-vs-`null` gap: it served one flat list (so paging was unobservable) and computed its own-post id with `max()` over mixed str/int, which raised TypeError and made the string-id test pass because the DOUBLE crashed rather than because the mark was right. Mutation matrix, all executed, each reddening exactly its named test: retry disarmed; numeric-max reverted; partial-mark fallback removed; short-page terminates; page-1-only walk; post-write fail-closed flipped open; jq type-guard reverted. The unusable-count arm is unreachable by any fixture and is annotated as such rather than claimed as proved. Verification: `scripts/tests` 1090 passed, 2 skipped; decisions_validate and build_decisions_catalog --check both exit 0; terminator, clamp, sort order and id monotonicity all re-measured live on Gitea 1.27.1. refs #763 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4368cc8cbe |
fix(763): page both /statuses/{sha} reads to a validated terminator
`review-verdict.yml` read the per-POST status history twice with a single `?limit=100` request. `limit` clamps to the server-wide `MAX_RESPONSE_ITEMS` (measured 50), so on a head carrying more rows than the clamp both reads saw a partial list. The high-water mark was only page 1's maximum, and — the direction that matters — a raced human verdict beyond page 1 was invisible to the post-write race check, leaving a forged green over a rejection. Both reads now walk to a validated empty page (`[]` on this endpoint, measured 2026-08-28 against PR #761's 114-row head: pages 1-2 return 50, page 3 returns 14, page 4 is `[]`), never terminating on a short page, under a 20-page cap and retrying each page once. Correctness does not depend on the cap value. This retires #751's page-2 "assume raced" probe, which repaired every head that outgrew one page. It fired on Renovate PR #761: an `::error::` claimed a human verdict had been overwritten on a head carrying none, and the sticky sentinel then refused re-exemption on every later run. Two properties replace it. Uncertainty now fails closed at both ends — the unreadable-history branch warned and left the exemption green while the page-2 probe repaired on the same uncertainty, one check disagreeing with itself; this is affordable only because paging removed the common trigger. And the post-write read must witness the job's own write: reaching a validated empty page proves the walk finished, not that it saw a current list, so at least one row above the pre-write mark must exist because the job just posted one. The `::error::` now distinguishes a verdict actually found from an unverifiable read. The sentinel description stays generic — the classification recognises it as a fixed point, so its wording is load-bearing. The stub gained faithful paging (50-row slices, `[]` past the end, one snapshot per logical read so a counter mode cannot describe two different histories across pages) and, separately, modelling of the job's own POST appearing in the history — which it had never done, so in its world every ordinary run looked like a head nothing had been posted to. `own-write-invisible` withholds exactly that detail as the negative control for the currency witness. Mutation-proved by execution, one clause at a time: - walk reads page 1 only -> RUNNING_PAST_PAGE_1_is_PAGED_and_the_exemption_ STANDS, raced_verdict_on_PAGE_2_is_detected_and_repaired and both UNREADABLE history tests go red - currency-witness zero branch deleted -> CANNOT_SEE_OUR_OWN_WRITE red - fail-closed flipped to fail-open -> both UNREADABLE history tests red Verification: `scripts/tests` 1085 passed, 2 skipped; decisions_validate and build_decisions_catalog --check both exit 0. fixes #763 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b16ec15d6c |
docs(781,799): re-measure the tooling audit from a derived population, and adopt serena (#862)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m10s
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) Successful in 4m13s
§5.3's verdicts rested on a single surface, which manufactured four false zeros: codex is driven through `codex exec` inside Bash, security-guidance and ralph-loop expose no tool at all and run as hooks (1,086 executions each), and feature-dev is used through its agents. The audit also compared current enablement against historical usage — six of the eight plugins it called "genuinely unused" were disabled for 16 of the 30 corpus days. The retirement half of #781 is answered *no* on evidence: the zeros split six ways and only one is grounds for removal. Eight plugins are kept by operator decision. #799's observation was correct and its cause is now established. serena was `false` in settings.json until 2026-08-14T12:31Z, when a concurrent session enabled it; its tools appear in no transcript before 12:42:54Z. #799's session started at 12:01Z and never reloaded, so its probe correctly found nothing while the settings file already said `true`. serena is adopted and documented as the third code-intelligence surface. Four review rounds, two independent cold reviewers (one cross-family); rounds 1-3 BLOCKED. fixes #781 fixes #799 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
761e575836 |
fix(787): derive the dropped-step guard's scope, and reconcile its snapshot against the server (#861)
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 / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m22s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m33s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
8aebba4d89 |
fix(748): declare permissions: on all six workflows, and prove the declaration binds (#860)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m4s
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) Successful in 4m20s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
45b17e58e1 |
fix(744,835): ci-image.yml publishes from main only; guard persist-credentials with no exemption (#857)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m23s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m15s
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) Successful in 4m27s
Closes the push route into ci-image.yml (#744) and ships the persist-credentials guard that was waiting on it (#835). ci-image.yml's push trigger had no branches: filter and was path-scoped to docker/ci/** AND to the workflow file itself. Gitea resolves a push workflow's definition from the pushed ref, so any branch push touching those paths ran that branch's own YAML on a docker-capable runner holding the credential that writes ersatztv:prod and the ersatztv-ci:<sha> five container: jobs execute. Be precise about what the filter buys: it is loaded from the pushed ref like the rest of the file, so a branch that deletes it re-enables the route. This closes the DRIVE-BY case - publication as a side effect of an ordinary push - and is not a boundary against a writer who intends to run their own YAML. The wider class is #853. The self-reference left both paths: and ci-image-pin's expected in the same change - a decided tradeoff with both prices stated, not a necessity. Branch publishing moves to workflow_dispatch, probed live: run 2340 on this branch published ersatztv-ci:43b1e45 and left :latest unchanged. With both mechanical blockers gone, ci-image.yml's checkout takes persist-credentials: false (16 of 16) and scripts/tests/test_workflow_persist_credentials.py holds the convention with NO exemption list - git-index population, declared clause mutation re-run every suite, guard-inventory rows. fixes #744 fixes #835 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
d68ce42fcb |
fix(742): inherit an h10 verdict only from an allow-listed reviewer (#850)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m55s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m0s
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) Successful in 4m25s
`review-verdict.yml` decided whether an existing `review-verdict/h10` was worth INHERITING by testing `.creator.login != null` — satisfied by any account's credential, including the `renovate` bot's `RENOVATE_TOKEN`, a `write:repository` PAT that cannot be scoped down the way #697 scoped the registry credential. The test is now membership in `H10_REVIEWERS="timothy"`, a literal in the base-resolved definition. The design that survived 11 cold review rounds: * `read_existing_verdict` carries TWO flags. `ex_human` (attributable AND allow-listed) gates INHERITANCE; `ex_attributable` gates the last-moment re-read, which asks the opposite question and must stay broad. Narrowing both — the first draft — makes the job post its exemption over a mid-run rejection, and the post-write repair does not cover that. * The two calls no longer compute an identical predicate, so "changed" is made explicit: the state/creator/description triple from the first read is snapshotted and compared. * The allow-list governs an inherited `success` ONLY. An existing `failure` inherits on attributability alone, because inheriting a rejection can only withhold an exemption while re-deriving one can turn it green on an exempt PR. A symmetric rule was a measured fail-open. * The post-write raced check stays broad — not because narrowing would let a rejection go green (a real reviewer is on the list by construction), but for the misconfiguration case. Two mechanisms were WITHDRAWN rather than patched a third time, and both withdrawals are recorded in `ci.exemption-provenance` so they are not re-attempted: a `::warning::` annotation that produced three defects in three rounds, and a post-write fix whose generic `pending` would have been re-derived anyway and which had no retry trigger. Verified: the inheritance predicate driven against the LIVE Gitea API on a probe-named context, both allow-list directions; every clause mutation-proven against the shipped file; `scripts/tests` 1012 passed, 2 skipped. Follow-ups filed: #845 (post-review-verdict.sh does not check its own account is allow-listed) and #849 (post-write verification: three routes leaving an exemption `success` over a human `failure`, plus the retarget fence's post-POST gap, plus the prose sweep that lands with the behaviour). fixes #742 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
ed8b602445 |
feat(735): bound the numeric FFmpeg profile fields with a 422, and expose readrate pacing (#847)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m23s
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) Successful in 6m23s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
469d19852c |
fix(788): one declarative H10 verdict vocabulary, derived by both sides (#846)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m17s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m50s
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) Successful in 4m22s
The verdict words lived in two hand-written shell copies — the `case` arms of post-review-verdict.sh (write) and the POS_RE/NEG_RE regexes of check-review-verdict.sh (read) — held together by nothing but a comment that had already gone stale. scripts/lib/review-verdict-vocabulary.sh now declares them once and both sides derive; neither script enumerates a verdict word any more. Only the WORD SET moved. The grammar stays in check-review-verdict.sh, where every #629 false-open actually lived. No parity test: #774 shipped one and withdrew it after six rounds, because a regex over shell source is not a shell parser. The proof is behavioural and graded MUTATION — the harness restores the pre-#788 hardcoded POS_RE each run and requires it to redden. Enforcement is a DATA dependency, not a control-flow gate. Review round 1 found a real fail-open in the first commit: `${#arr[@]}` is nounset-safe only for a declared-empty array, and under `set -u` that error inside a function called as `if ! validate` skips BOTH branches — so on the reader (deliberately no `set -e`) an explicit BLOCKED @ head classified `positive`, exit 0. Validation now sets a sentinel on its last line and the derived views refuse without it. Six cold review rounds; rounds 2-6 found no fail-open across differential fuzzing (4788 / 2612 / 7560 payloads, zero divergences from origin/main's grammar), sentinel forgery, environment poisoning, declare -p evasion on bash 5.3 and 3.2, path/symlink resolution and probe TOCTOU. Every malformation fails closed: reader exit 2, writer exit 1 with nothing posted. Also corrected: CLAUDE.md and release.review-verdict-gate both enumerated the vocabulary without LGTM, a word the code has accepted since #629. fixes #788 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
ba6a4b08aa |
feat(732): On Now / Next gets a background box, and is on by default (#843)
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 / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m18s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
probe742/combined-newest SECOND
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
f2551b778e |
fix(746): drop the persisted checkout credential; unmask the base-ref fetches (#842)
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 / CI toolchain image resolves (push) Successful in 24s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 30s
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 1m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m14s
Build ErsatzTV Image / Build & push image (amd64) (push) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m1s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
40a3232d9e |
feat(734): field-level progressive disclosure — shared FieldHelp trigger + panel (#841)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 15s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m57s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m32s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m18s
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) Successful in 5m27s
Implements the three-level field-help pattern from #734 as a shared component: field name + optional one-sentence summary → a one-short-paragraph panel behind a consistent Info-icon trigger → a future external-docs deep link (`docsHref`, built and typed; no screen passes one yet). Adopted on FFmpegProfilesScreen (9 fields), documented as docs/spa-conventions.md §15 with decision record `spa.field-progressive-disclosure`, and mirrored into the design-system prototype. The panel is portalled to document.body: `.ctv-card` sets `overflow: hidden`, which clips a positioned descendant whatever its z-index, and one field's explainer rendered 12px of a 92px paragraph in every state of the Audio card. A `::before` hover bridge was added and then WITHDRAWN — it held for a vertical descent onto the panel and failed for a diagonal one, leaving a safe sideways exit of 1.25px on an 18px icon. Hover reads the paragraph in place; the panel's interactive content is reached by pinning. Four cold adversarial review rounds; the first three returned BLOCKED. They found five wrong copy claims across nine paragraphs and two vacuous tests in a row for the same mechanism. Deferred with owners: #839 (placement verified by hand, not by a test) and #840 (the portal puts a docsHref link at the end of the tab order). fixes #734 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
35affecd29 |
docs(755): ersatztv owns the fork code, media-management owns channel operations (#838)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 8s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 1m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m7s
Build ErsatzTV Image / Build & push image (amd64) (push) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m5s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
e8e17f3844 |
docs(708): record the live route-2 reproduction against PR #761 (#834)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m57s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m5s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m53s
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) Successful in 4m24s
|
||
|
|
08cd3a002d |
fix(690,758): count the same query a paged handler pages (#833)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m18s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m40s
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) Successful in 5m46s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
7453dd3a82 |
fix(721,740): align Auto-Tune proposal rows on a grid; guard AddItemsDialog's async searches (#831)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 8s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m35s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m38s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m58s
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 1m17s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
3e2c07b42f |
fix(807): SPA full-replace bodies are built as Complete<T>, with a derived guard over droppable request members (#827)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 14s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 30s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m9s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m49s
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 15s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
7966e08164 |
fix(701): guard SongMetadata's nullable primitive collections at the read site (#826)
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 / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 16s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m59s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m55s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m15s
|
||
|
|
b5dee26202 |
fix(701): guard SongMetadata's nullable primitive collections at the read site
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 8s
PR Gates / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 19s
review-verdict/h10 Review-verdict: MERGEABLE @ b5dee26 (base: main)
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 15s
Review verdict / Set review-verdict status (pull_request_target) Successful in 6s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 4m32s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m53s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Both search indexers opened UpdateSong with
metadata.AlbumArtists ??= [];
metadata.Artists ??= [];
Artists/AlbumArtists hold the whole list in ONE COLUMN rather than
being navigations. So unlike the same `??= []` idiom on
Genres/Tags/Artwork all around them, the property IS the column value:
assigning it on a TRACKED entity flips the entry to Modified and the
next SaveChanges writes [] over a NULL column. This is the mechanism an
adversarial review demonstrated in #691, which is why that issue's
entity-level guard was reverted in favour of guarding at the read site.
Measured rather than reasoned about, per the issue's first done-when
box. Restoring ONLY the `??= []` clause (the real predecessor lines,
not a hand-written mutant) reddens the new fixture on
`metadata.Artists should be null but was []`; a probe variant with the
first two assertions replaced by prints reports STATE=Modified and the
raw column moving from NULL to "[]". Today's two feeds are both
AsNoTracking (SearchRepository.GetItemToIndex and GetAllSongs), so no
shipped caller loses data -- but that is a property of two callers, not
of the indexer, and #691 already recorded it as a loaded gun. The
fixture pins the indexer's own contract instead.
Removing the assignment is not sufficient alone: it was load-bearing
for the four reads below it, and deleting it by itself converts a
silent write into a live throw on every untagged song. Measured by
deleting only those two lines from the real predecessor file:
NullReferenceException, thrown at the foreach (cited by symbol: a line
number in a mutant that exists in no committed tree is unreproducible
by construction). The
exception type follows the read FORM, not the field -- foreach yields
NRE, string.Join/ToList yield ArgumentNullException -- and this PR
contains two of each, which is why no single exception-name grep
characterises the class. So each site moves together with its reads:
- LuceneSearchIndex.UpdateSong / ElasticSearchIndex.UpdateSong: hoist
Optional(...).Flatten().ToList() locals and read those.
- RefreshChannelDataHandler: the Scriban context took the raw nullable
lists (the issue's second item). The shipped _song.sbntxt only does
array.join, but a custom template is free to do anything.
The population was derived from the MODEL rather than from the issue's
file list, and the obvious derivation is wrong: "the IList<string>
properties under ErsatzTV.Core/Domain" returns two of eight. It misses
the six value-converted collections (ProgramScheduleAlternate and
PlayoutTemplate each carrying DaysOfMonth, MonthsOfYear, DaysOfWeek),
declared as plain ICollection<T> and made single columns only in
Data/Configurations -- and their storage differs (comma-separated text
for the int converter, JSON for the enum one), so the shared property
is "one scalar column", not the serialization. No site applies `??=`
to any of the six, so this defect has no instance there; whether a null
can REACH one at runtime is unverified and is filed as #823 rather than
asserted either way. Only the SongMetadata pair is left NULL in
practice, by FallbackMetadataProvider. Every site touching either field
was then swept; the remaining readers were already guarded by #691.
The fixture carries two anti-vacuity guards, both witnessed:
- A POSITIVE CONTROL (`writer.NumDocs.ShouldBe(1)`). Every other
assertion says something did NOT happen, so all of them hold
vacuously if UpdateSong never runs -- and it silently stops running
if a future refactor gates UpdateItems on `_initialized`, which this
fixture bypasses by injecting the writer. Verified BOTH directions:
with that gate added the control fails `NumDocs should be 1 but was
0`, and with the control removed the whole test PASSES while the code
under test is unreachable.
- A capturing logger, because UpdateSong wraps its body in a catch that
assigns metadata.Song = null -- severing a required relationship and
cascading the metadata to Deleted. Without it the probe silently
measures the error path; on the first run it did exactly that (a bare
ILanguageCodeService substitute NPEs inside AddLanguages). The raw
column helper also fails loudly on a missing row, since ExecuteScalar
returns CLR null for both "NULL column" and "no such row".
ElasticSearchIndex has no equivalent fixture -- it needs a stubbed
transport -- so its change is by inspection against the Lucene one, and
the gap is filed as #824 rather than covered by a source-text guard.
The whitespace-only churn in ElasticSearchIndex.cs is the #311
fix-as-you-touch format gate: it scopes to whole changed FILES.
`git diff -w` over that file shows only the two hunks above.
Local gate: ErsatzTV.Tests 2006 passed / 4 pre-existing skips,
Core.Tests 685/1 skip, Infrastructure.Tests 114, Architecture.Tests 7,
Scanner.Tests 1504 -- 0 failures in each. scripts/tests 874 passed / 2
skipped. dotnet format whitespace --verify-no-changes clean on the four
touched files, no BOM on any. decisions_validate OK.
Fixes #701
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
78fc283627 |
docs(772): the merge gate decides on the COMBINED state, and skipped counts as green
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 8s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 19s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 34s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
Review verdict / Set review-verdict status (pull_request_target) Successful in 11s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 5m0s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m59s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
review-verdict/h10 Review-verdict: MERGEABLE @ 78fc283 (base: main)
Graded a nit and not blocked on, but it is a claim about a neighbouring subsystem that is one notch too strong: the hook decides on the combined `.state` and its filters exclude `skipped` (#593). Left as-is it would have taught the next reader that any non-success context denies, which is what #593 exists to correct. refs #772 |
||
|
|
8c72a5de9e |
docs(772): correct the twins of two claims this branch already fixed
Both remaining review findings were the same shape as the one before them, and it is the
shape this repo keeps recording: a claim corrected in one place, its copy left standing
somewhere else in the tree.
* `docs/ci-cd.md` said "gates nothing" in the small-lane paragraph while the section 1441
lines below said the opposite. A red preflight lands in the PR's combined status, which
the merge gate reads (#598) — what it does not do is SKIP the jobs it diagnoses, and
that is now the sentence in both places.
* Two docstrings in the preflight's test file still described the disarmed script as
warning and exiting 0. Built the mutant and ran it: it emits an error and exits 1. The
exit code separates nothing now that an unverifiable answer fails too — the DIAGNOSTIC
is what the mutation destroys, which is what `mutation_manifest.py` already said and
the prose next to it contradicted.
Nits from the same pass: the admin-cron URL is quoted (`?` globs in zsh, the operator's
shell); the retry assertion's message quoted a threshold it does not use; the arm table
omitted the malformed-credential shape the code and tests both have; `buildx inspect` no
longer `--bootstrap`s a builder just to read its name, and an empty capture no longer
produces a noisy `buildx use ""`.
Swept the tree for the shape rather than the two reported lines: the surviving "exits 0"
and "could-not-tell" hits are other subsystems, or the concept named as a concept.
refs #772
|
||
|
|
d304c8573e |
docs(772): state the trap, not the draft history
The advisory narrative check was right about both new passages: 'an earlier draft warned and exited 0' and 'both cold reviews found it independently' only parse to someone who saw the session. What a cold reader needs is that warning-and-exiting-0 is the natural way to write this check and is wrong, and what it costs — which is now what the doc says. refs #772 |
||
|
|
30640bb780 |
fix(772,792): close the fix-round review — the doc still described the semantics the fix reversed
The re-review's one HIGH was mine and was the obvious one to miss: the previous commit changed the preflight so an unverifiable answer FAILS, and left a `docs/ci-cd.md` paragraph two screens away still saying "anything else is reported as could-not-tell". That paragraph is the one an operator reads when the job goes red, and it would have talked them into reinstating the defect. Replaced with the full arm table, including the two rows the first draft got wrong and why. * "gates nothing" was false in the way this repo has recorded before (#598): the merge-consent hook reads the COMBINED status, so a red preflight blocks the merge like any other red job. It does not SKIP the jobs it diagnoses; that is the accurate claim, in ci-cd.md and in the remote-state row. * The production retry defaults were evaluated by nothing — every test overrode both knobs. A test now drops the overrides and measures three attempts and a real pause, so editing the default to 1/0 (which would falsify the "a blip does not redden a PR" argument) goes red. * `journalctl -u gitea | grep ExecuteCleanupRules` is not a reproduction: that identifier reaches the log only through slow-query warnings, so an empty grep on a healthy host reads as "the rule never ran" — the inverse. Replaced with the admin cron API, which answers deterministically. * The recovery recipe's `docker buildx use default` needs the containerd image store to `--push` (both named hosts have it, checked today) and mutated the operator's builder selection without restoring it. * The stub's comment claimed both halves of real curl's transport failure mattered; only the exit status is observable, because `|| resp=""` discards what curl printed. * The empty-half credential refusal echoed the username; it needs no value at all. The 401/403 arm aborts the remaining pins while 404 continues — deliberate, now stated. * `curl -u "$VAR"` puts a credential in argv, and this job runs container-free on a shared host. NOT fixed here: it is the shape all five `scripts/` callers already use, so fixing one site leaves the class and splits the codebase. Filed as #821 and named at the site. refs #772 refs #792 |
||
|
|
0cf355e494 |
fix(772,792): address both cold reviews — an unverified pin is no longer a green job
Two independent reviewers (one cross-family) converged on the same defect, and it was the
important one: the preflight WARNED and exited 0 on every answer that was not 200 or 404,
so a missing `curl`, a moved registry or a DNS change would have left it green forever —
"the check could not run" presenting as "the pin is fine", in a script whose own header
disclaimed exactly that. Unknown answers are now retried (3x, 5s) and then FAIL, with
wording kept distinct from the deleted case because the two send an operator to different
places.
Also from the reviews:
* An absent secret does not arrive as an unset variable. `${{ secrets.X }}:${{ secrets.Y }}`
interpolates to ":", a perfectly non-empty and perfectly useless credential, and the
tests covered only the unset shape. Both halves are now required, and the parametrised
test drives the production shape.
* HTTP 200 is not a manifest. A proxy or a login page answers 200 too, so the body is
fetched and matched for `schemaVersion` (a shell `case`, so no jq dependency and no
pipeline that can inject).
* The curl stub ignored `-u` and answered 200 regardless, so deleting the real `-u` would
have left the suite green while the live registry rejected every request. It now 401s an
unauthenticated read, as the registry does.
* The mutation's declared diagnostic changed with the script: now that unknown fails too,
the exit code no longer separates "deleted" from "could not check", so the proof turns on
the message and `expect` says so.
* docs/ci-cd.md: `scan` is no longer the only `docker-build.yml` job on the small lane, so
the tag-push exclusivity claim and the lane membership were both false. Fixed.
* "Immutable" was overstated: `ci-image.yml` tags `rev-parse --short HEAD`, so a dispatch or
a weekly no-cache run at the same HEAD republishes that tag from a rebuilt image. Stated,
along with what the rebuild recovery does NOT restore (mutable bases and apt, so equivalent
rather than bit-identical).
* The recovery recipe left you in a worktree checked out at the pin commit — where the
verify script does not exist, and where the workflow carries the pre-bump pin. It now
keeps `$repo`, returns, and removes the worktree. It also needed BuildKit's `http = true`
caveat: the container driver does not inherit the daemon's insecure-registries.
* The root cause carries its evidentiary limit and its reproduction commands, and says what
to conclude if a pin vanishes after server-management#842 lands (refuted, not re-applied).
* The `ci.required-job-step-execution-markers` carve-out named one container-free job; there
are two now, and the membership is what rots.
* The decision record's `''` YAML escapes leaked into rendered prose; "status, no comment ->
ask" is qualified (a prior positive verdict for the SAME head still satisfies condition
(c)); "exits 1" is "exits non-zero" (usage exits 2, jq its own status, signals 128+n).
refs #772
refs #792
Decisions-Edit: yes
|
||
|
|
5ba442c11c |
fix(772,792): name the missing toolchain image, and stop a refusal leaving a verdict comment
#772 — the pinned CI toolchain image can be deleted out from under us, and when it was (2026-08-11..13) all five `container:` jobs died at image pull, both required contexts included, with the cause buried in each job's log. Root cause is registry-side and is now established rather than guessed: an owner-level Gitea package cleanup rule (keep_count 15, remove_days 1, remove_pattern `.*`, keep_pattern no 7-hex sha can match) deletes a sha tag once 15 newer versions exist, and `ExecuteCleanupRules` ran nightly through the window. The `ersatztv` package carries the same rule's fingerprint exactly — every sha tag older than the 15-slot window is gone, every keep_pattern tag back to 26.3.1 survives. Version deletes leave no audit row, so the specific run cannot be replayed; that limit is stated where the claim is made. The durable fix belongs to the registry's repo: server-management#842. What lands here is what a consumer of someone else's registry can do: * `toolchain-preflight`, a container-free job (a job consuming the image could not run to report it missing) resolving every pin against the registry and failing with a message that names the tag and the recovery. Not a `needs:` of the jobs it diagnoses — gating five jobs behind a checkout and one curl taxes every green run to speed up a rare red one, and they already fail fast. * Only HTTP 404 means gone. Everything else is could-not-tell, and rejected credentials fail rather than pass as unknown — "the check could not run" must never present as "the pin is fine". * A recovery path that does not need CI: rebuild the SAME tag from the commit it names and push it. The push half was verified against this registry on 2026-08-22 with a throwaway package (created, resolved 200, deleted). #792 — the reported defect was the exit code, and re-measuring says that premise is false: every no-status path already exits 1, and eight refusal modes now assert it against the real predecessor, where they pass. The observed 0 came from the invocation, not the script. What WAS broken is the half-state the issue describes second: the comment was written before the status, so every refusal left `Review-verdict: MERGEABLE @ <head>` on a PR with no gating status behind it. The two writes are now ordered status-then-comment, which makes the only reachable half-state the safe one — a status with no comment leaves the merge hook's condition (c) with nothing to classify, which is an `ask`. The refusals themselves are untouched. Ordering rather than compensating deletion: an orphaned-comment cleanup needs a Gitea call, and these refusals are usually caused by Gitea being unreachable. Proof for the ordering is the split against origin/main's script: the 8 orphan/ordering tests go red there, the 8 exit-code tests stay green. fixes #772 fixes #792 Refs: server-management#842 Decisions-Edit: yes |
||
|
|
29d7a06e65 |
fix(806): guard populations over FILES derive from the git index, not a filesystem walk (#818)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m57s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m56s
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) Successful in 4m20s
`testing.guard-derives-population-from-source` (#774) was silent on the commonest population in our own guards — files in a directory — and every one answered with a filesystem walk. A walk is not authoritative: it reports build output, generated shims and editor droppings, and differs per machine. #778 measured the cost by getting the same population wrong three times in one PR. CONVERTED (a completeness claim over tracked files): `test_guard_inventory.py`, `test_hook_fire_log.py`, `test_ci_image_pin_population.py` (which also gained `*.yaml`), `test_remote_state_inventory.py` (folded onto the shared derivation), and `test_pr_changed_files.py` (not on the issue's list — found by sweeping the whole repo). ASSESSED AND RECORDED, not silently skipped: `_repo_copy` takes its file list from the index for hermeticity though it makes no completeness claim; `test_ci_dropped_step_guard.py` has no filesystem population at all; the decisions corpus is recorded as unexamined rather than cleared; and the SPA page-size guard is deferred to #819 with its obstacle documented. This is not "replace every glob". `scripts/tests/tracked_files.py` is the single derivation. `test_guard_populations_derive_from_git.py` proves it in two measured complements: exhaustive removal catches a hardcoded `.exists()` admit and memoisation; the call log catches an append-only source that yields nothing on this machine — #778's shape — which removal cannot see because it has nothing to remove. Twelve rounds of independent cold review, alternating model families in isolated worktrees. The production derivations were confirmed sound every round; every blocking finding after the first was in the proofs or in prose claims about them. Counts over growing populations were removed rather than corrected, after three drifted. fixes #806 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
3a53c50c45 |
docs: a cancelled job reads as failure where a monitor actually looks (#817)
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 / Delimiter ban (release path) (push) Successful in 24s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m13s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m1s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
`ci.cancelled-is-not-a-verdict` documented the run/job API, where cancelled is distinguishable. The endpoint a CI monitor actually polls — `commits/{sha}/status`, the per-sha view the merge gate reads — has no `cancelled` state and reports one as `failure`. The record now says to resolve the job-level `conclusion` before reporting a red.
The kickoff's push HARD CONSTRAINT is tightened from "a review has run" to "a CLEAN verdict, zero outstanding findings on the current tree", since #790's rounds 7 and 8 each still found a real mechanism defect and every earlier push auto-cancelled a live run.
Refs #790
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
|
||
|
|
3b386649bf |
feat(790): execute every MUTATION grade instead of asserting it (#815)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m30s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m9s
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) Successful in 4m10s
Every `MUTATION` row of `docs/guard-inventory.md` now carries a DECLARED clause mutation that is applied to an isolated copy of the repository on every suite run, with the row's own named test required to go red carrying a declared diagnostic. Manifest and MUTATION rows are compared for set equality both directions; the other 22 guards each carry a stated reason, compared the same way. Measured rather than assumed: 12 of 13 guards admit a single-clause mutation; `instrumentation_faults` does not, and that entry carries the surviving finer mutation, re-run every suite. Nine cold cross-family review rounds. Rounds 1, 2, 7 and 8 each found real mechanism defects — two mutations that measured nothing, an incomplete git-environment sanitisation, a reset that restored its own mutant, and a proof of that fix which was not itself isolated. All fixed and witnessed red. fixes #790 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
d4c72697f2 |
feat(780): commit a ruff config and enforce it in CI (#813)
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 / Delimiter ban (release path) (push) Successful in 28s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m54s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m59s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 15s
Python lint here was a property of the operator's laptop: the global instructions
say to run ruff, no workflow ran it, and with no committed config ruff fell back
to whichever ~/.config/ruff/ruff.toml the machine happened to have.
- ruff.toml at the root, pinned ruff==0.12.11 in the script-tests job.
- Both lint steps pass an EXPLICIT population from `git ls-files` with
`--no-force-exclude`, never `ruff check .` — an `exclude` empties a
discovery-based run into a GREEN one (top level empties both commands, [lint]
empties check, [format] empties format --check), and `ruff check .` over zero
files exits 0 with only a stderr warning. Guarded by an empty-population arm.
- Tree clean: 74 findings at
|
||
|
|
6a4265d81d |
feat(784): a doc records the end state — generalize the no-session-narrative rule (#811)
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
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |