787e354b9b
The `build` job fails on main and on every branch merged from it: doc-site: context/code/pr-review.md is neither published nor a named omission NOT a merge loss -- scripts/build-doc-site.mjs in this branch is byte-identical to main's, and neither lists the file. PR #96 added context/code/pr-review.md without a manifest line, which is exactly the gate improvise-res9 added: a doc added without a line publishes nothing and says nothing, so the generator refuses. Omitted rather than published, and the line is cloud-agents.md's rather than tooling.md's. tooling.md explains checkers a reader of the code will meet; pr-review.md is about THIS REPOSITORY'S PROCESS -- where a Codex review appears, how to tell it from our own agents' comments, why absence is not approval. Nobody visiting a website is about to read a PR thread. Both homes updated, because an omission is restated beside the manifest AND in doc-site.md section 2, and the prose there counted them ("Neither is referenced...") so it went stale at three. This strays into scripts/, which is Wave N's ground. Flagged in the hand-back; it is a one-line data entry either way if the orchestrator would rather publish it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
80 lines
29 KiB
HTML
80 lines
29 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>The repository's own checkers — improvise docs</title>
|
||
<link rel="stylesheet" href="../style.css" />
|
||
</head>
|
||
<body>
|
||
<div class="shell">
|
||
<nav class="side"><a class="brand" href="../index.html">improvise docs</a><input type="checkbox" id="navtoggle" class="navtoggle" /><label class="navsummary" for="navtoggle">Pages</label><div class="navlinks"><ul><li><a href="../../index.html">← Project home</a></li><li><a href="../../app/">Browser demo</a></li><li><a href="https://github.com/fiddlerwoaroof/improvise">Source on GitHub</a></li></ul><h3>Start here</h3><ul><li><a href="../design-notes.html">Design notes</a></li><li><a href="../design-principles.html">Design principles</a></li><li><a href="../repo-map.html">Repository map</a></li></ul><h3>Decision records</h3><ul><li><a href="../adr/001-concurrent-edits.html">ADR 001 — Concurrent edits</a></li><li><a href="../adr/002-naming-a-cell.html">ADR 002 — Naming a cell</a></li><li><a href="../adr/003-live-backed-models.html">ADR 003 — Live-backed models</a></li></ul><h3>The arguments, by area</h3><ul><li><a href="../code/core.html">improvise-core</a></li><li><a href="../code/formula-and-io.html">improvise-formula and improvise-io</a></li><li><a href="../code/protocol.html">improvise-protocol</a></li><li><a href="../code/command.html">improvise-command</a></li><li><a href="../code/clients.html">The clients</a></li><li><a href="../code/tui.html">The TUI</a></li><li><a href="../code/gp.html">gp-tree and improvise-gp</a></li><li><a href="../code/kb-tool.html">The kb tool</a></li><li><a href="../code/tooling.html" class="here">The repository's own checkers</a></li></ul><h3>Test evidence</h3><ul><li><a href="../test-evidence/core.html">improvise-core</a></li><li><a href="../test-evidence/formula-and-io.html">improvise-formula and improvise-io</a></li><li><a href="../test-evidence/protocol.html">improvise-protocol</a></li><li><a href="../test-evidence/command.html">improvise-command</a></li><li><a href="../test-evidence/clients.html">The clients</a></li><li><a href="../test-evidence/tui.html">The TUI</a></li><li><a href="../test-evidence/gp.html">gp-tree and improvise-gp</a></li><li><a href="../test-evidence/tooling.html">The repository's own gates</a></li></ul><h3>About this site</h3><ul><li><a href="../doc-site.html">About this site</a></li></ul><h3>Reference</h3><ul><li><a href="../tickets.html">Ticket index</a></li></ul></div></nav>
|
||
<main>
|
||
<p class="src">Source: <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/context/code/tooling.md">context/code/tooling.md</a></p><details class="toc" open><summary>On this page</summary><ul><li class="l1"><a href="#the-repository-s-own-checkers">The repository's own checkers</a></li><li class="l2"><a href="#1-the-comment-cap">1. The comment cap</a></li><li class="l2"><a href="#2-the-evidence-check">2. The evidence check</a></li><li class="l3"><a href="#a-leaf-is-not-an-identity">A leaf is not an identity</a></li><li class="l3"><a href="#the-names-come-from-cargo-not-from-the-source">The names come from cargo, not from the source</a></li><li class="l3"><a href="#a-demotion-is-a-reviewer-s-job-not-a-gate-s">A demotion is a reviewer's job, not a gate's</a></li><li class="l3"><a href="#a-flag-adds-a-question-it-never-selects-one-away">A flag adds a question; it never selects one away</a></li><li class="l3"><a href="#two-headings-one-test">Two headings, one test</a></li><li class="l2"><a href="#3-the-others">3. The others</a></li><li class="l3"><a href="#why-the-pages-deploy-fails-on-found-0-artifact-s-improvise-864a">Why the Pages deploy fails on "Found 0 artifact(s)" (improvise-864a)</a></li></ul></details><h1 id="the-repository-s-own-checkers">The repository's own checkers</h1>
|
||
<p><a href="https://github.com/fiddlerwoaroof/improvise/tree/main/scripts"><code>scripts/</code></a> holds the gates this repo runs on itself. They are code, and they are held to the rule they exist to enforce: a check nobody has seen fail is indistinguishable from one that always passes, so each of them carries a self-test whose cases are injected faults (<code>doctrine(check_the_checker)</code>).</p>
|
||
<p>Four docstrings in <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/check-comment-runs.py"><code>scripts/check-comment-runs.py</code></a> point here. This is the file they mean.</p>
|
||
<hr />
|
||
<h2 id="1-the-comment-cap"><a class="anchor" href="#1-the-comment-cap">#</a>1. The comment cap</h2>
|
||
<p><code>check-comment-runs.py</code> fails a <code>.rs</code> file carrying more than two consecutive comment lines. The rule is <a href="../design-principles.html"><code>context/design-principles.md</code></a> §6: reasoning lives in <a href="https://github.com/fiddlerwoaroof/improvise/tree/main/context"><code>context/</code></a> prose, where it can be read, corrected and audited, not in a comment that nothing checks against the code.</p>
|
||
<p>The whole difficulty is deciding what <em>is</em> a comment line. Ten silent successes have been found in this one function, every one of them failing in the direction that looks like passing — a file that should have been flagged and was not:</p>
|
||
<ul>
|
||
<li><code>"test" in line</code> matched <code>#[cfg(test)]</code>, so every cfg-gated fixture cleared a gate written to reject exactly those. <strong>Anchor the match</strong>; an unanchored substring is a filter that over-matches, and a filter that over-matches fails in the direction that looks like success. (The same class as the bd ID audit's exclusion list, and as <code>_JS_TEST</code>'s exclusion of <code>.skip</code>/<code>.only</code>.)</li>
|
||
<li>A <code>proptest!</code> flag was set and never cleared, so the first <code>proptest!</code> in a file made every later <code>fn</code> — helpers included — count as a test.</li>
|
||
<li>An attribute holding a bracket inside a string (<code>#[cfg(feature = "[")]</code>) left the bracket depth unbalanced and exempted the rest of the file. Every question is therefore asked of the <strong>scanner</strong>, never of the raw line: the scanner knows Rust strings, raw strings, char literals and nested block comments, and the attribute depth is counted over code text only.</li>
|
||
<li>A fence that opens and never closes exempted everything after it.</li>
|
||
</ul>
|
||
<p>Two rules that are decisions rather than accidents. A <code> fence **suspends** the run rather than ending it, so two lines above a fence and two below is a run of four — a reader meets it as one uninterrupted block. And only a *runnable* fence earns the exemption: rustdoc executes a bare </code> or <code>rust and does not execute </code>text, <code>ignore or </code>no_run, so the rest is prose wearing a fence.</p>
|
||
<h2 id="2-the-evidence-check"><a class="anchor" href="#2-the-evidence-check">#</a>2. The evidence check</h2>
|
||
<p><code>--check-evidence</code> holds <a href="https://github.com/fiddlerwoaroof/improvise/tree/main/context/test-evidence"><code>context/test-evidence/</code></a> and the tests it describes to each other, in two directions:</p>
|
||
<ul>
|
||
<li><strong>Orphans.</strong> Every heading shaped like a test name must name a test that runs.</li>
|
||
<li><strong>Unstated.</strong> Every test whose doc comment cites <a href="https://github.com/fiddlerwoaroof/improvise/tree/main/context/test-evidence"><code>context/test-evidence/</code></a> must have an entry there.</li>
|
||
</ul>
|
||
<p>Neither direction is "every test has evidence". Most tests have none and should not be forced to have one; what is checkable is a <em>claim</em>, and both directions are claims. The failure they close is the same one a dead ticket ID is: a pointer that reads as a promise the thing exists.</p>
|
||
<h3 id="a-leaf-is-not-an-identity"><a class="anchor" href="#a-leaf-is-not-an-identity">#</a>A leaf is not an identity</h3>
|
||
<p>The names cargo lists are <strong>module paths</strong>, and the checker keeps them whole. Six leaf names collide in this workspace — two of them cited in evidence — so reducing a path to its last segment made a heading satisfiable by a test in another module answering another question. Renaming <code>ui::project::tests::a_load_that_fails_is_not_a_rebase</code>, the one its entry actually describes, left the checker at <em>579 entries, all name a test that runs</em>, exit 0: the surviving <code>ui::host::tests</code> namesake answered for it.</p>
|
||
<p>So a bare leaf resolves only when it is <strong>unique</strong>, and an ambiguous one is a <strong>refusal</strong> naming the candidates — not a warning, because a warning re-creates the problem for anyone who does not read it. The heading is then written with its module path, which is also what makes the entry say which test it is about.</p>
|
||
<p>Note the two halves are both needed and neither is sufficient. The refusal forces qualification but cannot survive a rename on its own: rename one of a colliding pair and the leaf becomes <em>unique</em>, silently resolving to the survivor. Qualification is what closes that, and the refusal is what makes somebody write it.</p>
|
||
<p>What stays open is collision <em>across binaries</em>: <code>--list</code> prints module paths with no crate and no binary, so two crates sharing one could not be told apart (<a class="tid" href="../tickets.html#improvise-sxsf" title="cargo test -- --list cannot name the binary, so two crates could share a full test path">improvise-sxsf</a>). Measured 2026-09-21 and still latent — 1444 names, <strong>zero</strong> duplicate full paths, and zero duplicate <code>(crate, path)</code> pairs.</p>
|
||
<p>The fix the ticket proposes — <code>--list</code> per package with the crate prefixed — was measured and is <strong>declined twice over</strong>. It costs 45.2s against 2.4s for the one workspace call (11 members, warm), 18× for an unreachable case, and <code>--check-evidence</code> needs the <code>--ignored</code> twin too, so the real figure is 22 invocations. More to the point <strong>it does not close the class it names</strong>: a package is not a binary. <code>improvise</code> alone carries five integration test binaries beside its lib, and an integration test's top-level <code>fn</code> lists with no module path at all — 60 of the 1444 names are bare — so <code>-p improvise</code> prefixes all six identically and <code>tests/a.rs::foo</code> still cannot be told from <code>tests/b.rs::foo</code>. A fix that ever becomes worth having is per-<em>target</em> (<code>--lib</code>, <code>--test <name></code>), not per-package, and the ticket's proposal would have bought 18× for a partial cure.</p>
|
||
<h3 id="the-names-come-from-cargo-not-from-the-source"><a class="anchor" href="#the-names-come-from-cargo-not-from-the-source">#</a>The names come from cargo, not from the source</h3>
|
||
<p>A heading is diffed against <code>cargo test --workspace -- --list</code>, minus the names that <code>--list --ignored</code> reports. It used to be scraped out of <code>.rs</code> text, and four shapes defeated that, all of them "evidence for a check that never runs" (<a class="tid" href="../tickets.html#improvise-rm4s" title="The evidence check accepts headings naming tests that never run">improvise-rm4s</a>):</p>
|
||
<table><thead><tr><th>shape</th><th>why the scrape accepted it</th></tr></thead><tbody><tr><td><code>#[test] fn x</code> inside a raw string literal</td><td>the scanner reads inside strings</td></tr><tr><td><code>#[cfg(feature = "…")] #[test]</code></td><td>the feature is never enabled</td></tr><tr><td><code>#[cfg(target_arch = "wasm32")] #[test]</code></td><td><code>cargo test --workspace</code> is native</td></tr><tr><td><code>#[test] #[ignore]</code></td><td>it compiles, and never runs</td></tr></tbody></table>
|
||
<p>The third is the realistic one here: this repo has real wasm32-only code and a <code>wasm</code> CI job that only runs <code>cargo check</code>. Adopting the stricter source cost nothing — all 539 headings resolved against a real listing on the commit that made the change — which is the moment to tighten a gate rather than the moment to argue about it.</p>
|
||
<p>Demonstrated before the fix, by injecting all four shapes into <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/draw.rs"><code>src/draw.rs</code></a> and <a href="../test-evidence/tui.html"><code>context/test-evidence/tui.md</code></a> at once: the previous checker printed <em>543 entries, all name a test that exists</em>, exit 0. The current one names all four.</p>
|
||
<p>What this costs: <code>--check-evidence</code> now needs a toolchain and a compiled workspace, about three seconds warm. A missing <code>cargo</code> says so rather than raising. The comment-cap check is unaffected and needs neither.</p>
|
||
<p><code>--rust-tests FILE</code> takes a saved listing for a caller that has one, and it <strong>requires <code>--rust-tests-ignored FILE</code></strong>: a first version took one file, called <code>cargo_test_names</code> with one argument, and so skipped the ignored rule entirely while printing the same success line as the honest path. A flag that is strictly more permissive and indistinguishable in the log is a bypass, not a convenience — so the pair is mandatory and the success line <strong>names its source</strong>. CI passes neither and runs cargo itself.</p>
|
||
<p>A <strong>missing</strong> <a href="https://github.com/fiddlerwoaroof/improvise/tree/main/context/test-evidence"><code>context/test-evidence/</code></a> is not an early return either. It used to be, and it exited 0 while 170 tests cited a directory that was not there; the walk now yields nothing and every citation is reported unstated, which is loud.</p>
|
||
<p>Note what the self-test can and cannot carry here. The listing <em>reader</em> is testable from fixtures — a leaf name taken off a module path, an <code>--ignored</code> name subtracted, cargo's own prose not mistaken for a test — and those cases are in <code>SELF_TEST_LISTING</code>. The four <em>source shapes</em> are not: once <code>defined</code> comes from cargo, a fixture asserting "a heading with an empty listing is an orphan" is the same case however the Rust beside it is spelled, and writing four of them would be four copies of one assertion dressed as coverage. The evidence for those is the injection above, against the real tree.</p>
|
||
<p>What it does <strong>not</strong> close: <code>citing</code> is still scraped, so a citation inside a string literal would be reported as a test owing an entry. That is a false positive in the safe direction — loud, not silent — and it has no instance.</p>
|
||
<h3 id="a-demotion-is-a-reviewer-s-job-not-a-gate-s"><a class="anchor" href="#a-demotion-is-a-reviewer-s-job-not-a-gate-s">#</a>A demotion is a reviewer's job, not a gate's</h3>
|
||
<p>A heading is classified by <strong>shape</strong>: a bare identifier is a Rust test, a quoted string a JS test, anything else is prose about the section (<code>Helper — x</code>, <code>Note — y</code>, a file's own scope). So any orphaned heading can be retired by rewriting it with a space and a dash, and nothing can check whether the prose is honest — no mechanism could, because the question is whether a sentence is true (<a class="tid" href="../tickets.html#improvise-9za9" title="Demoting an evidence heading to prose is an unguarded escape hatch">improvise-9za9</a>).</p>
|
||
<p>Two things make that tolerable. The unsafe direction is the one that fails loudly: prose that <em>looks</em> like an identifier is classified as a Rust claim and reported as an orphan, so the error you can make by accident is the noisy one. And <strong>the mechanism is <code>git diff context/test-evidence/</code></strong> — a demotion shows up there as a <code>### identifier</code> line becoming prose, in the diff every reviewer already reads. The live instance, PR #69 demoting a heading while PR #72 restored its coverage under another name, is what says a reviewer rather than a gate has to catch it.</p>
|
||
<p>The printed count of prose headings is a <strong>convenience, not a guard</strong>: nothing stores the previous count and nobody diffs two CI logs, so a retirement moves a number no one is watching. Saying otherwise would claim a detectability it cannot support, which is the class of defect §7's adjacent-check section is about.</p>
|
||
<h3 id="a-flag-adds-a-question-it-never-selects-one-away"><a class="anchor" href="#a-flag-adds-a-question-it-never-selects-one-away">#</a>A flag adds a question; it never selects one away</h3>
|
||
<p><code>--check-evidence</code> used to <strong>early-return</strong>, so the flagged run answered the evidence question and said nothing at all about the comment-run cap. Three agents hit that independently in one campaign, and in one case a local gate went green while CI went red on a three-line doc comment (<a class="tid" href="../tickets.html#improvise-ijql" title="check-comment-runs.py --check-evidence silently skips the comment-run cap it is named for">improvise-ijql</a>). It is now additive: <code>main</code> runs <code>check_cap</code> first, then <code>check_evidence</code>, and returns the worse of the two.</p>
|
||
<p>The ticket offered a cheaper shape — leave the early return and print one line saying the cap did not run — on the precedent of PR #83 making <code>--rust-tests</code> name its source. That precedent does not reach this case, and the distinction is worth keeping. <strong>A provenance line and a disclaimer are not the same object.</strong> Naming which listing the names came from qualifies an answer the run actually gave; saying "the cap did not run" qualifies an answer it did not give, while still <strong>exiting 0</strong>. Exit 0 is what CI branches on and what a human reads as clean, so a disclaimer leaves the gate exactly as skippable and merely documents the skip. The rule the fix is written to is therefore stated over the exit code: <strong>a tree with a long comment run must not pass any invocation that can exit 0.</strong></p>
|
||
<p>Which invocations those are is a decision, not a sweep. <code>--self-test</code> and <code>--list-doctests</code> are excluded and neither is an exception to the rule: the first scans no tree, and the second is a <em>listing</em> that renders no verdict. Only the two that answer "is this tree clean" are in <code>SELF_TEST_DISPATCH</code>.</p>
|
||
<p>That table is checked by running the script as a <strong>subprocess</strong> against a temporary tree, which is the only level at which this bug is visible — every unit in this file was correct, and the defect lived in <code>main</code>'s dispatch. Both arms are needed and both were demonstrated: restoring the early return fails <code>DISPATCH: --check-evidence: exited 0 over a four-line comment run</code>, and forcing the cap to always report a violation fails the clean arm for both invocations, which is what stops an always-fail from satisfying the first. The temporary tree also has no <a href="https://github.com/fiddlerwoaroof/improvise/tree/main/context/test-evidence"><code>context/test-evidence/</code></a> and no JS roots, so the evidence half is vacuously clean there and the cap is the only thing that can decide the exit code — the isolation is what makes the assertion name one question.</p>
|
||
<h3 id="two-headings-one-test"><a class="anchor" href="#two-headings-one-test">#</a>Two headings, one test</h3>
|
||
<p>An orphan is a heading whose test nobody can find; the mirror image is a test that <strong>two</strong> headings answer for, and it is invisible to every rule above — each copy names a test that runs, so each copy passes. PR #92 shipped two entries under one heading where the older described a fixture the test no longer had and repeated a claim an adversarial review had measured false. A reader meeting the older one first has no way to know a successor exists two thousand lines down, so the stale copy reads as current. That is worse than an orphan, which at least announces itself as broken.</p>
|
||
<p>It is a <strong>refusal and not a warning</strong>, on the precedent <code>improvise-rm4s</code> set for the ambiguous leaf: the two are the same defect seen from opposite ends — one name answered by two tests, and one test answered by two names — so they are answered the same way. The refusal names the file and every line, in file order.</p>
|
||
<p>The key is the <strong>resolved test, not the spelling</strong>. <code>### f</code> beside <code>### m::tests::f</code> is two headings for one test, and grouping by the string would miss it precisely because qualification is the repair <code>rm4s</code> asks for; a heading qualified in one file and bare in another would have been the first way this came back. Resolution reuses the same leaf table the ambiguity rule uses, so a leaf that is genuinely ambiguous canonicalises to itself and is reported as ambiguous rather than as a duplicate — two <em>different</em> tests sharing a leaf are not two entries for one test, and conflating them would file the wrong repair. The check is over the whole directory rather than per file, because a heading keys a test name and a test name is unique workspace-wide once qualified.</p>
|
||
<p>What it still cannot see, and neither can any of the others: an entry that exists, names a live test, is the only one that does — and describes it backwards.</p>
|
||
<h2 id="3-the-others"><a class="anchor" href="#3-the-others">#</a>3. The others</h2>
|
||
<p><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/kb"><code>scripts/kb</code></a> is the only writer of <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/prolog-sessions/kb.pl"><code>prolog-sessions/kb.pl</code></a>; its own reasoning is in <a href="../code/kb-tool.html"><code>context/code/kb-tool.md</code></a>. <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/node-test.sh"><code>scripts/node-test.sh</code></a>, <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/kb-test.sh"><code>scripts/kb-test.sh</code></a>, <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/pr-watch-test.sh"><code>scripts/pr-watch-test.sh</code></a> and <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/assemble-stamp-test.sh"><code>scripts/assemble-stamp-test.sh</code></a> are the same shape as this file's subject: a guard, plus a suite that makes the guard refuse what it claims to refuse. Each is wired into <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/.github/workflows/ci.yml"><code>.github/workflows/ci.yml</code></a> in the same job as the thing it guards, because a check that only runs locally is a convention.</p>
|
||
<p><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/scripts/check-ci-graph.py"><code>scripts/check-ci-graph.py</code></a> is the workflow's own gate: <code>pages-deploy</code> must <code>needs</code> every other job in <code>ci.yml</code>. The rule "every check job" was written down (<a class="tid" href="../tickets.html#improvise-x7cq" title="A kb.pl inconsistency can still reach the published site">improvise-x7cq</a>) and a job added during review of PR #83 was still ungated with nothing noticing, which is what moved it from a sentence to a check. It reads the job keys and the <code>needs:</code> list off indentation, tracking which job a <code>needs:</code> belongs to — taking the first one seen would read another job's list as the deploy's and pass. <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/README.md"><code>README.md</code></a> therefore states the rule and deliberately <strong>does not list the jobs</strong>: a second copy with nothing holding it to the first is what went stale, twice.</p>
|
||
<h3 id="why-the-pages-deploy-fails-on-found-0-artifact-s-improvise-864a"><a class="anchor" href="#why-the-pages-deploy-fails-on-found-0-artifact-s-improvise-864a">#</a>Why the Pages deploy fails on "Found 0 artifact(s)" (<a class="tid" href="../tickets.html#improvise-864a" title="Concurrent merges to main race on one Pages environment and the superseded deploys fail">improvise-864a</a>)</h3>
|
||
<p><code>actions/deploy-pages@v5</code> intermittently fails a push to <code>main</code> with <em>No artifacts named "github-pages" were found for this workflow run</em>, while the upload step was green in that same run. The ticket framed this as concurrent merges racing on one Pages environment, and PR #46 proposed skipping the deploy unless the commit is the tip of <code>main</code>. <strong>Both the framing and the guard are wrong, and measurement says so.</strong></p>
|
||
<p><strong>What was measured.</strong> All 100 push-to-<code>main</code> CI runs from 2026-09-19T22:02Z to 2026-09-21T05:02Z, from the run, job, step and log APIs (job metadata and logs outlive artifacts, which expire in a day and are all 404 now — so this is reproducible where the ticket's own artifact-count table is not):</p>
|
||
<table><thead><tr><th>deploys</th><th>with another run's SUCCESSFUL deploy completing inside this run's own upload→deploy window</th></tr></thead><tbody><tr><td>12 artifact-not-found failures</td><td><strong>12 (100%)</strong></td></tr><tr><td>56 successes</td><td>2 (3.6%)</td></tr><tr><td>4 other failures</td><td>0 — all four are the unrelated <em>Create Pages site failed: Resource not accessible by integration</em>, from before <code>enablement: false</code> (<a class="tid" href="../tickets.html#improvise-3gy" title="4.2 Enable GitHub Pages">improvise-3gy</a>)</td></tr></tbody></table>
|
||
<p><strong>The direction is settled, and it is neither of the two the ticket offered.</strong> The oldest run of the 17:47Z batch failed at 17:47:23Z — <em>before</em> the next deploy in the batch had started (17:47:31Z) and before any Pages deployment existed at all, since <code>deploy-pages</code> fetches the artifact metadata <em>before</em> it creates a deployment. So the failure cannot have been caused by this run's own deploy, by supersession, or by a concurrent deploy. The artifact was already gone. <strong>The reaper is another run's <em>successful</em> deployment.</strong></p>
|
||
<p><strong>It is scoped by artifact NAME, not by run.</strong> In run 35560726456 the build job finalized three artifacts seconds apart — <code>harness-serve</code>, <code>wasm-bundles</code> and <code>github-pages</code>, each with an id — and 2m21s later that run's own lookup reported <em>Found 2 artifact(s)</em> and no <code>github-pages</code>. Same run, same token, same call, same retention: the two siblings survived and only <code>github-pages</code> was missing. That one observation rules out the whole family of general explanations — service flakiness, eventual consistency, retention, a token scope, a v3/v4 backend mismatch — because any of them would have taken the siblings too.</p>
|
||
<p><strong>Four things this kills.</strong> Queue depth is not the mechanism: the three deploys of the 17:47Z batch never queued, running 9s and 17s apart with each failure taking one second, so the <code>pages</code> group serialized nothing. Retention is not it either — all three shared <code>retention-days: 1</code>, and since the <em>newest</em>-finalized expires last, expiry can produce 0/1/1 or 0/0/1 but never the ticket's 0/1/0. Nothing in <code>deploy-pages</code> deletes an artifact (an upstream feature request asks for exactly that capability, which is only open because nothing does). And the action-version theory dies on the interleaving: successes and failures alternate minute by minute on identically-resolved action SHAs.</p>
|
||
<p><strong>One clause of the ticket's own correction is falsified, and the conclusion still holds.</strong> The correction argued the tip-of-<code>main</code> guard would have skipped the one deploy that published "and let 52507a6 deploy and fail anyway" — which assumed 52507a6's artifact loss was independent of its neighbour. It was not: 5f871f7's deploy completed at 17:47:40Z, four seconds before 52507a6's began. Fewer successful deploys really is the right lever, so PR #46 aimed at the right quantity for the wrong reason. <strong>It should still not be shipped</strong>, because the association is not a mechanism and the mechanism is not deterministic: 5f871f7 had a foreign deploy land inside its own window too and survived. A guard measured at nothing, against a probabilistic effect, is <code>doctrine(measure_the_obvious_fix_before_reporting_it_as_the_fix)</code> exactly.</p>
|
||
<p><strong>What the evidence does license.</strong> The risk lives entirely in the upload→deploy window — every victim had a foreign deploy land inside its own — so shortening that window is the one lever with evidence behind it, and consolidating the build already did some of that. Closing it properly means no two runs holding a <code>github-pages</code> artifact across a deployment, which is a real change to the job graph with real costs and is not attempted here. A retry is <strong>not</strong> a fix: the artifact is gone rather than transiently unlistable, so a retry cannot find it.</p>
|
||
</main>
|
||
</div>
|
||
</body>
|
||
</html>
|