From 8f3e8512744b85d1f8b4ed24f230c5ddd9a360fb Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Sun, 6 Sep 2026 18:03:46 +0200 Subject: [PATCH] report a source that is quiet rather than broken, instead of a bare 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build log prints a count per source, and CI now declines to fail on a page that states its own emptiness — which leaves that source showing an unexplained 0, reading as exactly the fault the gate just decided it was not. The zero we are content with is the one that has to say why. It is also the only line that would ever prompt anyone to ask whether a lane quiet for a month is a game between patches or a wiki that reworded the sentence a statesNoEvents check is still matching. Nothing else in the pipeline can tell those apart. The test count in the layout block was stale at 898 and moves with these two commits, so it is corrected here. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 7 ++++++- AGENTS.md | 8 ++++++-- src/shared/feed.ts | 19 +++++++++++++++++++ test/feed.test.ts | 19 ++++++++++++++++++- test/refresh.test.ts | 11 +++++++++++ 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 397fa3f..de4557a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,12 +81,17 @@ jobs: // and it was not: it read eventCount, which is counted after // expiry, so a page whose events had all simply ended reddened the // build. Behaviour belongs where behaviour can be exercised. - const { brokenSources, staleSources } = await import("./src/shared/feed.ts"); + const { brokenSources, quietSources, staleSources } = await import("./src/shared/feed.ts"); for (const s of staleSources(feed.sources)) { console.log( ` note: ${s.sourceId} parsed ${s.parsedCount} events, all of them ended — stale page, not a fault`, ); } + for (const s of quietSources(feed.sources)) { + console.log( + ` note: ${s.sourceId} parsed nothing and the page says so itself — between versions, not a fault`, + ); + } const broken = brokenSources(feed.sources); if (broken.length > 0) { throw new Error( diff --git a/AGENTS.md b/AGENTS.md index 2724bd5..71a162e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,7 +145,7 @@ src/client/ React app, service worker, manifest theme.ts — dark or light, and what a game hue reads as on each scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches) serve.ts static server + /api/health -test/ 898 tests +test/ 1,071 tests fixtures// raw HTML + .expected.json per source — pinned, kept forever snapshots/ current page per source, rewritten by refresh — see its README ``` @@ -658,7 +658,11 @@ and both generalise past this wiki: that file for a string, which proved the check existed and never that it was right. An empty Nikki lane is therefore now the truth rather than a gap, exactly as GFL2's thin weeks are. -The lane refills on its own when 2.8 is listed, with no parser change. +The lane refills on its own when 2.8 is listed, with no parser change. CI says so out loud rather +than silently — `quietSources` prints a note beside the count, because an unexplained `0` in the log +reads as exactly the fault the gate just declined to call it, and it is the only line that would ever +prompt somebody to ask whether a month-long quiet lane is a quiet game or a wording change under a +`statesNoEvents` that still matches. **The second Fandom source's page is chosen, not obvious.** `fategrandorder.fandom.com` publishes two schedules: `Event_List` opens "This page lists all Events diff --git a/src/shared/feed.ts b/src/shared/feed.ts index 5286b4d..c1c7089 100644 --- a/src/shared/feed.ts +++ b/src/shared/feed.ts @@ -165,3 +165,22 @@ export function staleSources(sources: readonly SourceHealth[]): SourceHealth[] { (s) => s.parsedCount !== null && s.parsedCount > 0 && s.eventCount === 0, ); } + +/** + * Sources whose page says it currently lists no events. + * + * The third empty, and the only one that is neither a fault nor a gap: a gacha + * calendar goes quiet between versions and this page said so in words. Not + * thrown, for the reason `statesNoEvents` exists at all. + * + * Reported, though, and that is the half worth defending. Excusing this zero + * from the build is not the same as saying nothing about it — the build log + * prints a count per source, and an unexplained `0` reads as exactly the fault + * this distinction denies. It is also the only thing that would ever prompt + * somebody to check whether a lane has been quiet for a month because the game + * is between patches or because the page's wording changed under a + * `statesNoEvents` that still matches. + */ +export function quietSources(sources: readonly SourceHealth[]): SourceHealth[] { + return sources.filter((s) => s.statesNoEvents); +} diff --git a/test/feed.test.ts b/test/feed.test.ts index 4db9a6c..c7c61f6 100644 --- a/test/feed.test.ts +++ b/test/feed.test.ts @@ -3,6 +3,7 @@ import { brokenSources, freshness, staleSources, + quietSources, STALE_AFTER_MS, SourceHealth, } from "../src/shared/feed.ts"; @@ -175,10 +176,26 @@ describe("telling a broken source from a stale one", () => { expect(brokenSources([health({})])).toEqual([]); }); - test("a healthy source is neither", () => { + test("a healthy source is none of the three", () => { const ok = health({ eventCount: 5, parsedCount: 5 }); expect(brokenSources([ok])).toEqual([]); expect(staleSources([ok])).toEqual([]); + expect(quietSources([ok])).toEqual([]); + }); + + test("a source whose page states it lists none is reported as quiet", () => { + // Excusing it from the build is not the same as saying nothing about it. + // The build log prints a count per source, and an unexplained 0 reads as + // the fault this whole distinction exists to deny — so the one empty we + // are content with is the one that has to say why. + expect( + quietSources([health({ parsedCount: 0, statesNoEvents: true })]).map( + (s) => s.sourceId, + ), + ).toEqual(["nikki-fandom-events"]); + expect(staleSources([health({ parsedCount: 0, statesNoEvents: true })])).toEqual( + [], + ); }); test("a source with nothing current left is reported as stale", () => { diff --git a/test/refresh.test.ts b/test/refresh.test.ts index a17f72c..c4d4552 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -1096,6 +1096,17 @@ describe("the workflows that drive the refresh", () => { expect(ci).toContain("brokenSources"); expect(ci).not.toContain("eventCount === 0"); }); + + test("ci.yml says which empty a source's zero was", async () => { + // Three sources can read zero and only one is a fault, so the two that are + // not have to say so where the count is printed — an unexplained 0 in the + // log reads as the failure the check just declined to call it. Whether the + // rules are right is exercised in test/feed.test.ts; this only pins that + // both are wired up. + const ci = await read("ci.yml"); + expect(ci).toContain("staleSources"); + expect(ci).toContain("quietSources"); + }); }); describe("flags", () => {