Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions data/outstanding-issues-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"version": "outstanding-issues-snapshot-v1",
"version": "outstanding-issues-snapshot-v2",
"ledger_revision": {
"sha": "4fe1131ebb326209fd5d7d379253de9ffb061ae2",
"committed_at": "2026-09-07T04:17:48+00:00"
"committed_at": "2026-09-07"
},
"counts": {
"open": 121,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "52d30987-38d0-4831-803d-6cb4c785aab7",
"createdOn": "2026-09-07",
"action": "update",
"payload": {
"id": "#RA0QAH",
"detail": "GENERIC HALF CLOSED BY MEASUREMENT 2026-09-07; the row stays open only for three unexplained specs. This row's fixed member left a standing worry in its own text: that the Next 16 prefetch-header trap is generic to any Playwright assertion trying to tell a prefetch from a navigation, and was worth grepping for. GREPPED, on origin/main 0177bed: grep -rn 'next-router-prefetch' over tests/, src/ and scripts/ returns two hits, both inside the already-fixed tests/ui-smoke.spec.ts:1354 block (its explanatory comment and the corrected guard). There is no second occurrence anywhere in the repository, so that specific trap is not lurking elsewhere and no further code fix follows from it. WHAT REMAINS UNEXPLAINED: the other three single-spec Production UI failures of 2026-09-02. This row never named them, and the only pointers are CI runs 33610490607 and 33613433031, which are past the useful retention window for reading logs. With no spec identities and no reproduction, any fix would be a guess, and guessing at flakiness is how a real failure gets quarantined by accident. RECOMMENDATION: leave open at P3 as a watch item, not a work item. If a fourth single-spec Production UI failure appears, capture the spec identity and the run URL AT THAT MOMENT - that is the missing input - then reproduce with --repeat-each on the same SHA before touching anything. Do not quarantine; tests/flake-ledger.json still holds one entry and it is not one of these.",
"source": "Assessment session 2026-09-07: repository-wide grep on origin/main 0177bed; no provider access",
"baseRowFingerprint": "11306c58889703ac51bed66d1499c6228814fc4e21c4905cd4f2b9bd437d2062"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "9e8f3e9a-0542-4c9b-b4ef-0263e73f5d27",
"createdOn": "2026-09-07",
"action": "update",
"payload": {
"id": "#KHTTW4",
"detail": "ASSESSED AND LARGELY OVERTAKEN 2026-09-07, on a fresh worktree at origin/main 0177bed. FIRST HALF IS FIXED, NOT BY THIS ROW: npm run test:e2e:ward-journeys now reports 71 passed, 3 skipped, 0 failed. The eight failures this row recorded are gone. Two of the three skips are the ui-ward-morning pair, and their skip is a documented owner-approved decision rather than a silencing: MERGE 02 (2026-09-05) folded the morning board into CapacityScreen, /mockups/ward-flow/morning is now a redirect stub, MorningPage is unmounted, and morning-page.tsx's own doc comment forbids retargeting the spec at CapacityScreen or re-mounting the component pending the owner's ruling on spec D9. Component-level coverage continues in tests/ward-morning-page.dom.test.tsx (20 cases) and tests/ward-morning-print.test.ts. SECOND HALF IS BUILT BUT INERT, AND THAT IS THE ONLY REMAINING ACTION. The blocking lane ui-ward-journeys in .github/workflows/ci.yml no longer scopes on ward paths - it now gates on needs.changes.outputs.ui_changed - so the path-scoped blind spot this row named is designed out. But the job also requires vars.WARD_JOURNEYS_BLOCKING == 'true', which is unset by default, so it is skipped and pr-required records it skipped-and-fine. That inertness is deliberate and documented in the workflow: a lane that is already red, made blocking, stops every UI pull request in the repository, so the author required one deliberate act in repository settings at the moment somebody has a green run of the journeys in front of them. THAT PRECONDITION IS NOW MET and the green run is quoted above. NEXT ACTION, owner only: set repository variable WARD_JOURNEYS_BLOCKING to true. It is a GitHub settings change, not a code change, so no PR can do it. Close this row once it is set and one PR has shown the lane reporting.",
"source": "Assessment session 2026-09-07: fresh origin/main worktree, local chromium-mockups run, and a read of ci.yml plus ui-ward-morning.spec.ts",
"baseRowFingerprint": "119605ea523b039ea176fbbc0860e20492b68c77669a199d3c341b9c7046dc3f"
}
}
51 changes: 45 additions & 6 deletions scripts/generate-outstanding-issues-snapshot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { pathToFileURL } from "node:url";
const LEDGER_PATH = "docs/outstanding-issues.md";
const INBOX_DIR = "docs/outstanding-issues-inbox";
const OUTPUT_PATH = "data/outstanding-issues-snapshot.json";
export const SNAPSHOT_VERSION = "outstanding-issues-snapshot-v1";
export const SNAPSHOT_VERSION = "outstanding-issues-snapshot-v2";

// Reuse the repo's escape-aware splitter. The ledger contains 8 escaped pipes
// (`\|`); a naive `line.split("|")` turns each into a column boundary and the
Expand Down Expand Up @@ -167,12 +167,45 @@ export function readInboxRecords(dir = INBOX_DIR) {
.map((entry) => JSON.parse(readFileSync(join(dir, entry.name), "utf8")));
}

/**
* A DATE, and no sha. This field is the only part of this snapshot measured
* changing as a side effect of another branch's work: PR #2690 ("calculators:
* remove directive clinical copy"), which touches no ledger file, rewrote
* exactly these two lines and nothing else here.
*
* The churn is structural rather than occasional. A reconciliation commit
* regenerates the snapshot and then becomes the ledger's newest commit, so the
* value it just wrote is stale the instant it lands — as the gate's own comment
* explains, which is why `ledger_revision` is excluded from comparison. Every
* later branch that regenerates (pre-commit doc sync, `docs:update`, `next
* build`) therefore rewrites it, stamping whichever ledger commit ITS base
* carries. Two branches cut either side of a reconciliation write different
* values into adjacent lines at the top of the file: a merge conflict in a file
* neither branch was editing. Excluding a field from the gate never stopped it
* conflicting in git, because the bytes still shipped.
*
* The sha is dropped because nothing reads it: `resolveFreshness` uses
* `committed_at` alone. Coarsening to a day makes two branches regenerating on
* the same day write identical bytes — and same-day is the measured case, the
* two commits behind #2690's conflict being 35 minutes apart. Branches a day
* apart still differ, which is deliberate residue: freshness is the one value
* here a reader cannot recompute.
*
* Same device and same reasoning as `captured_revision` in
* `repo-awareness-snapshot-v3`, which closed the identical defect in the
* sibling file.
*/
function toRevisionDate(committedAt) {
const match = /^(\d{4}-\d{2}-\d{2})/u.exec(committedAt);
return match ? match[1] : null;
}

export function readLedgerRevision(path = LEDGER_PATH) {
try {
const output = execFileSync("git", ["log", "-1", "--format=%H%x09%cI", "--", path], { encoding: "utf8" }).trim();
const output = execFileSync("git", ["log", "-1", "--format=%cI", "--", path], { encoding: "utf8" }).trim();
if (!output) return null;
const [sha, committed_at] = output.split("\t");
return { sha, committed_at };
const committed_at = toRevisionDate(output);
return committed_at ? { committed_at } : null;
Comment thread
BigSimmo marked this conversation as resolved.
} catch {
return null;
}
Expand All @@ -188,8 +221,14 @@ export function readLedgerRevision(path = LEDGER_PATH) {
export function readCommittedRevision(path = OUTPUT_PATH) {
try {
const revision = JSON.parse(readFileSync(path, "utf8")).ledger_revision;
if (typeof revision?.sha !== "string" || typeof revision?.committed_at !== "string") return null;
return { sha: revision.sha, committed_at: revision.committed_at };
if (typeof revision?.committed_at !== "string") return null;
// Normalised rather than taken verbatim: a v1 file on disk carries a full
// timestamp, and preserving that shape would write v1 bytes back into a v2
// file on the one path that preserves instead of reading git — the
// production image, which has no `.git`. Truncating makes the preserved
// value the same shape as a fresh read.
const committed_at = toRevisionDate(revision.committed_at);
return committed_at ? { committed_at } : null;
} catch {
return null;
}
Expand Down
61 changes: 59 additions & 2 deletions src/components/developer-area/hub/freshness-stamp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ import type { Freshness } from "@/lib/developer-area/freshness";
function formatDate(iso: string): string | null {
const parsed = new Date(iso);
if (Number.isNaN(parsed.getTime())) return null;
// A day-granularity value has no clock time to state. Formatting one with
// `timeStyle` renders `2026-09-07` as "7 Sept 2026, 08:00 AWST" — midnight UTC
// dressed as a Perth wall-clock reading, which is a fabricated instant. This
// component exists to stop confident-looking stamps carrying no information,
// so it must not manufacture one itself.
if (isDateOnly(iso)) {
return parsed.toLocaleDateString("en-AU", { dateStyle: "medium", timeZone: "Australia/Perth" });
}
const formatted = parsed.toLocaleString("en-AU", {
dateStyle: "medium",
timeStyle: "short",
Expand All @@ -30,6 +38,56 @@ function formatDate(iso: string): string | null {
return `${formatted} AWST`;
}

/**
* Both committed snapshots now date their revision to the day rather than the
* second — `captured_revision` in `repo-awareness-snapshot-v3` and
* `ledger_revision` in `outstanding-issues-snapshot-v2` — because a full
* timestamp is a per-branch value that makes two branches conflict on a file
* neither is editing.
*
* That precision is genuinely gone, and the stamp has to say so rather than
* imply otherwise. `resolveFreshnessFrom` parses `2026-09-07` as midnight UTC,
* so an hour count taken from it is wrong by however far into that day the
* commit actually was — up to a full day for a late-evening UTC commit, always
* in the direction of reporting the content as older than it is.
*/
function isDateOnly(iso: string): boolean {
return /^\d{4}-\d{2}-\d{2}$/u.test(iso);
}

/**
* Whole days between a day-granularity content date and the moment of viewing,
* compared as calendar days rather than elapsed milliseconds, because a day is
* the only claim the stored value supports.
*
* Both sides are UTC days, and that pairing is the point. The generators derive
* the stored date from a git commit's own `%cI`, so it is a UTC calendar date;
* comparing it against the reader's PERTH date mixes two zones and adds up to a
* further day of error on top of the day already lost to rounding. A commit at
* 23:59 UTC is 07:59 the next morning in Perth, so a Perth-day comparison calls
* it a day old the instant it lands. Rendering stays pinned to Perth like the
* rest of this component; only the arithmetic is UTC, and midnight UTC falls on
* the same calendar date in Perth either way.
*/
function ageInDays(contentDay: string, viewedAtIso: string): number | null {
const viewed = new Date(viewedAtIso);
if (Number.isNaN(viewed.getTime())) return null;
const contentMs = Date.parse(`${contentDay}T00:00:00Z`);
const viewedMs = Date.parse(`${viewed.toISOString().slice(0, 10)}T00:00:00Z`);
if (!Number.isFinite(contentMs) || !Number.isFinite(viewedMs)) return null;
return Math.max(0, Math.round((viewedMs - contentMs) / 86_400_000));
}

function describeAge(freshness: Freshness): string {
if (freshness.contentAt !== null && isDateOnly(freshness.contentAt)) {
const days = ageInDays(freshness.contentAt, freshness.viewedAt);
if (days === null) return "age unknown";
if (days === 0) return "same day";
return days === 1 ? "1 day old" : `${days} days old`;
}
return `${freshness.ageHours} ${freshness.ageHours === 1 ? "hour" : "hours"} old`;
}

/**
* Unconditional by design. There is no "fresh" short-circuit that could
* suppress it — a page that can hide its own age is the `#338` defect.
Expand Down Expand Up @@ -79,8 +137,7 @@ export function FreshnessStamp({
* whole job is stating age unambiguously.
*/}
{label} content as of {contentAt}
{viewedAt ? ` · viewed ${viewedAt}` : ""} · {freshness.ageHours} {freshness.ageHours === 1 ? "hour" : "hours"}{" "}
old
{viewedAt ? ` · viewed ${viewedAt}` : ""} · {describeAge(freshness)}
</span>
) : isLive ? (
<span>
Expand Down
7 changes: 5 additions & 2 deletions src/lib/developer-area/ledger-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import snapshotJson from "../../../data/outstanding-issues-snapshot.json";

import { resolveFreshnessFrom, type Freshness } from "./freshness";

export const LEDGER_SNAPSHOT_VERSION = "outstanding-issues-snapshot-v1";
export const LEDGER_SNAPSHOT_VERSION = "outstanding-issues-snapshot-v2";

export type LedgerPriority = "P1" | "P2" | "P3";

Expand Down Expand Up @@ -36,7 +36,10 @@ export type LedgerPendingRequest = {

export type LedgerSnapshot = {
version: string;
ledger_revision: { sha: string; committed_at: string } | null;
// A date (`2026-09-07`), not a timestamp, and no sha — `readLedgerRevision` in
// `scripts/generate-outstanding-issues-snapshot.mjs` carries the reasoning.
// Only `committed_at` was ever read, by `resolveFreshness` below.
ledger_revision: { committed_at: string } | null;
counts: { open: number; p1: number; p2: number; p3: number; queued: number; pending: number; resolved: number };
queue: LedgerQueueEntry[];
open: LedgerOpenItem[];
Expand Down
2 changes: 1 addition & 1 deletion tests/developer-clinical-answer-failures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function item(overrides: Partial<LedgerOpenItem> = {}): LedgerOpenItem {

function snapshotOf(open: LedgerOpenItem[]): LedgerSnapshot {
return {
version: "outstanding-issues-snapshot-v1",
version: "outstanding-issues-snapshot-v2",
ledger_revision: null,
counts: { open: open.length, p1: 0, p2: 0, p3: 0, queued: 0, pending: 0, resolved: 0 },
queue: [],
Expand Down
49 changes: 49 additions & 0 deletions tests/developer-hub-components.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,55 @@ describe("FreshnessStamp", () => {
expect(screen.getByTestId("developer-hub-freshness")).toHaveTextContent(/\b1 hour old\b/);
});

/**
* Both committed snapshots now date their revision to the day rather than the
* second, because a full timestamp is a per-branch value that made two
* branches conflict on a file neither was editing
* (`repo-awareness-snapshot-v3`, `outstanding-issues-snapshot-v2`).
*
* The stamp must not dress that day up as an instant. `new Date("2026-08-20")`
* is midnight UTC, which a Perth formatter renders as "08:00 AWST" — a clock
* reading nothing recorded — and an hour count taken from it is wrong by
* however far into the day the commit actually was.
*/
it("states a day-granularity revision as a day, with no fabricated clock time", () => {
render(<FreshnessStamp freshness={{ contentAt: "2026-08-20", viewedAt: "2026-08-21T00:00:00Z", ageHours: 24 }} />);
const stamp = screen.getByTestId("developer-hub-freshness");
expect(stamp).toHaveTextContent(mediumDate("2026-08-20T00:00:00Z"));
// The content date carries no time and no zone. `viewedAt` is a real
// instant and keeps both, so AWST still appears — scope the assertion to
// the content half rather than the whole stamp.
expect(stamp.textContent).toMatch(/content as of [^·]*\d{4}\s*·/);
expect(stamp.textContent).not.toMatch(/content as of [^·]*\d{1,2}:\d{2}/);
});

it("ages a day-granularity revision in days, not in hours it cannot support", () => {
// The reported case, and the one a Perth-day comparison also gets wrong: a
// ledger commit late in a UTC day is stored as that day, and an hour count
// read from midnight then calls it a day old within minutes of landing.
// 23:59 UTC is 07:59 next morning in Perth, so the arithmetic must stay in
// UTC to match the zone the stored date came from.
render(<FreshnessStamp freshness={{ contentAt: "2026-08-20", viewedAt: "2026-08-20T23:59:00Z", ageHours: 24 }} />);
const stamp = screen.getByTestId("developer-hub-freshness");
expect(stamp).toHaveTextContent(/same day/);
expect(stamp).not.toHaveTextContent(/hours old/);
});

it("says '1 day old', not '1 days old'", () => {
// One UTC calendar day after the recorded content day.
render(<FreshnessStamp freshness={{ contentAt: "2026-08-20", viewedAt: "2026-08-21T04:00:00Z", ageHours: 28 }} />);
expect(screen.getByTestId("developer-hub-freshness")).toHaveTextContent(/\b1 day old\b/);
});

it("keeps hour precision for a full timestamp, which still carries it", () => {
render(
<FreshnessStamp
freshness={{ contentAt: "2026-08-20T00:00:00Z", viewedAt: "2026-08-21T00:00:00Z", ageHours: 24 }}
/>,
);
expect(screen.getByTestId("developer-hub-freshness")).toHaveTextContent(/24 hours old/);
});

it("renders live status when freshness status is live", () => {
render(
<FreshnessStamp
Expand Down
4 changes: 2 additions & 2 deletions tests/developer-ledger-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { loadLedgerSnapshot, openItemsByPriority, resolveFreshness } from "@/lib
describe("ledger snapshot", () => {
it("loads the generated snapshot and validates its version", () => {
const snapshot = loadLedgerSnapshot();
expect(snapshot.version).toBe("outstanding-issues-snapshot-v1");
expect(snapshot.version).toBe("outstanding-issues-snapshot-v2");
expect(snapshot.counts.open).toBeGreaterThan(0);
});

Expand All @@ -17,7 +17,7 @@ describe("ledger snapshot", () => {
it("reports a gap between ledger content and build", () => {
const snapshot = {
...loadLedgerSnapshot(),
ledger_revision: { sha: "a".repeat(40), committed_at: "2026-08-20T00:00:00Z" },
ledger_revision: { committed_at: "2026-08-20" },
};
const freshness = resolveFreshness(snapshot, new Date("2026-08-21T00:00:00Z"));
expect(freshness.ageHours).toBe(24);
Expand Down
22 changes: 13 additions & 9 deletions tests/outstanding-issues-snapshot-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
import { compareSnapshots } from "../scripts/check-outstanding-issues-snapshot.mjs";

const BASE = {
version: "outstanding-issues-snapshot-v1",
ledger_revision: { sha: "a".repeat(40), committed_at: "2026-08-20T00:00:00Z" },
version: "outstanding-issues-snapshot-v2",
ledger_revision: { committed_at: "2026-08-20" },
counts: { open: 2, p1: 1, pending: 0 } as Record<string, number>,
queue: [{ order: 1, ids: ["#1"] }],
open: [{ id: "#1" }, { id: "#2" }],
Expand Down Expand Up @@ -46,14 +46,18 @@ describe("compareSnapshots", () => {
expect(compareSnapshots(old, BASE).join(" ")).toMatch(/version/);
});

// The regression this test exists for: `ledger_revision` is the sha of the
// commit that last touched the ledger, so committing a ledger edit changes it
// as a side effect. Comparing it made the gate fail on every ledger change
// with nothing stale, which would turn `main` red after each squash merge.
// The regression this test exists for: `ledger_revision` dates the commit
// that last touched the ledger, so committing a ledger edit changes it as a
// side effect. Comparing it made the gate fail on every ledger change with
// nothing stale, which would turn `main` red after each squash merge.
//
// v2 narrowed the field to a date, which is what stops two branches
// conflicting on it in git — but the gate still must not compare it, because
// branches a day apart legitimately differ here.
it("ignores a differing ledger_revision, which changes as a side effect of committing", () => {
const differentSha = structuredClone(BASE);
differentSha.ledger_revision = { sha: "b".repeat(40), committed_at: "2026-08-21T00:00:00Z" };
expect(compareSnapshots(differentSha, BASE)).toEqual([]);
const differentDate = structuredClone(BASE);
differentDate.ledger_revision = { committed_at: "2026-08-21" };
expect(compareSnapshots(differentDate, BASE)).toEqual([]);
});

it("still detects drift in queue, not just open", () => {
Expand Down
Loading
Loading