diff --git a/docs/INGESTION.md b/docs/INGESTION.md
index 4659856..1d71355 100644
--- a/docs/INGESTION.md
+++ b/docs/INGESTION.md
@@ -101,13 +101,15 @@ All live in `src/ingest/dates.ts`, each returning null rather than inferring any
| `parseAdjacentFullRange` | `July 30, 2026 August 13, 2026` (halves split by an `
`) | Persona 5: The Phantom X |
| `parseYearFirstSlashRange` | `2026/07/30 – 2026/08/20` (year first, so field order is not inferred) | Arknights |
| `parseOrdinalDateTimeRange` | `November 9th, 05:00 - December 4th, 2023, 04:59 (UTC-5)` (ordinal days, stated offset) | Reverse: 1999 |
+| `parseIsoDay` | `2026-08-04` (one boundary per column, so nothing to split) | Blue Archive |
| `parseOpenRange` | `Jul. 24, 2026 - End of 4.6`, `July 10, 2026 - Permanent` | Star Rail, Wuthering Waves |
`parseOpenRange` is tried last because it is the most permissive — it accepts any leading full date
and reports no end.
-The last two are anchored at both ends and require a year on each half, which is what keeps them from
-eating prose. `August 12, 2026 Day 3 rewards are doubled` would otherwise read "Day 3" as an end, and
+`parseAdjacentFullRange` and `parseYearFirstSlashRange` are anchored at both ends and require a year
+on each half, which is what keeps them from eating prose. `August 12, 2026 Day 3 rewards are doubled`
+would otherwise read "Day 3" as an end, and
`June 25, 2026 July 16/30, 2026` names *two* candidate ends — so it takes neither, and the leftover is
not shown as a summary either (a date the parser refused to trust must not reappear dressed as
information).
diff --git a/src/ingest/dates.ts b/src/ingest/dates.ts
index 2de84eb..11115ff 100644
--- a/src/ingest/dates.ts
+++ b/src/ingest/dates.ts
@@ -52,6 +52,25 @@ export function parseMonthDayYear(input: string): ParsedInstant | null {
return value === null ? null : { iso: value, precision: "day" };
}
+/**
+ * "2026-08-04" → 2026-08-04T00:00:00.000Z, day precision.
+ *
+ * A whole cell, anchored at both ends, because this is the least distinctive
+ * shape here: unanchored it would find a date inside a version string or an
+ * article slug. Blue Archive's wiki gives each boundary its own column and
+ * writes it as a bare ISO date, so unlike every range above there is nothing to
+ * split and no field order to infer.
+ *
+ * Rejects an impossible calendar date (`2026-02-30`) through `iso`, and states
+ * `day` rather than `exact` because the source publishes no time of day.
+ */
+export function parseIsoDay(input: string): ParsedInstant | null {
+ const m = /^\s*(\d{4})-(\d{1,2})-(\d{1,2})\s*$/.exec(input);
+ if (!m) return null;
+ const value = iso(Number(m[1]), Number(m[2]), Number(m[3]));
+ return value === null ? null : { iso: value, precision: "day" };
+}
+
/**
* "August 12 - September 21, 2026" → both instants, year taken from the end.
* A range whose end carries no year is unresolvable and returns null.
diff --git a/test/dates.test.ts b/test/dates.test.ts
index be03c4c..dc13005 100644
--- a/test/dates.test.ts
+++ b/test/dates.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
parseAdjacentFullRange,
parseFullRange,
+ parseIsoDay,
parseLabelledStartEnd,
parseMonthDayRange,
parseMonthDayYear,
@@ -330,3 +331,39 @@ describe("parseOrdinalDateTimeRange", () => {
expect(range?.start.iso).toBe("2026-08-13T10:00:00.000Z");
});
});
+
+describe("parseIsoDay", () => {
+ test("parses a bare ISO date at day precision", () => {
+ expect(parseIsoDay("2026-08-04")).toEqual({
+ iso: "2026-08-04T00:00:00.000Z",
+ precision: "day",
+ });
+ });
+
+ test("tolerates the whitespace a table cell carries", () => {
+ expect(parseIsoDay(" 2026-09-15 ")?.iso).toBe("2026-09-15T00:00:00.000Z");
+ });
+
+ test("rejects an impossible calendar date", () => {
+ expect(parseIsoDay("2026-02-30")).toBeNull();
+ expect(parseIsoDay("2026-13-01")).toBeNull();
+ });
+
+ test("refuses a date embedded in anything else", () => {
+ // Anchored at both ends because this is the least distinctive shape in this
+ // module. Unanchored it finds dates in article slugs and version strings.
+ expect(parseIsoDay("Rerun 2026-08-04")).toBeNull();
+ expect(parseIsoDay("2026-08-04 - 2026-08-18")).toBeNull();
+ // The page's other schedule tables write their boundaries this way, with a
+ // wall clock and no timezone anywhere. Reading one as UTC would invent the
+ // fact that matters most, so this reader takes none of them.
+ expect(parseIsoDay("08/12/2026 11:00")).toBeNull();
+ });
+
+ test("does not accept a partial date", () => {
+ expect(parseIsoDay("2026-08")).toBeNull();
+ expect(parseIsoDay("2026")).toBeNull();
+ expect(parseIsoDay("TBA")).toBeNull();
+ expect(parseIsoDay("")).toBeNull();
+ });
+});