diff --git a/docs/branch-review-records/5b8f236b72e13bc70a99be7384b79d387fc2317f46ed5f222d2f5ef425a0aadc.record.md b/docs/branch-review-records/5b8f236b72e13bc70a99be7384b79d387fc2317f46ed5f222d2f5ef425a0aadc.record.md new file mode 100644 index 0000000000..adda06c94e --- /dev/null +++ b/docs/branch-review-records/5b8f236b72e13bc70a99be7384b79d387fc2317f46ed5f222d2f5ef425a0aadc.record.md @@ -0,0 +1 @@ +| 2026-08-17 | claude/s1c-residuals-r2-r3-4pb1at | 3960c46a7322018f53ab79441234132f07657197 | S1c follow-ups: use-theme transition-timer guard + issues:done ULID display-id fingerprint, tests | PR #2063 open; both loose ends from the S1c babysit fixed; no RAG surface touched | focused vitest 20/20 + repo-hygiene 57/57; verify:pr-local (lint, typecheck, test, build) failed:(none); live-ledger fingerprint spot-check 3/3 | diff --git a/scripts/check-outstanding-issues.mjs b/scripts/check-outstanding-issues.mjs index 56352940d8..dfac7ee768 100644 --- a/scripts/check-outstanding-issues.mjs +++ b/scripts/check-outstanding-issues.mjs @@ -251,15 +251,20 @@ export function parseIssues(markdown) { } export function issueRowFingerprint(markdown, issueId) { - const match = String(issueId) - .trim() - .match(/^#(\d+)$/); - if (!match) return null; - const number = Number(match[1]); - if (!Number.isFinite(number)) return null; - + const trimmed = String(issueId).trim(); + const numericMatch = trimmed.match(/^#(\d+)$/); + const number = numericMatch ? Number(numericMatch[1]) : null; + if (numericMatch && !Number.isFinite(number)) return null; + + // Legacy numeric ids match by number (tolerating zero-padding differences); + // ULID-suffix display ids minted by reconcile match by the exact display id. + // Without the second arm, issues:done could never close a reconciled row. const row = parseIssues(markdown).rows.find( - (entry) => entry.number === number && entry.table === "open" && entry.valid && entry.raw, + (entry) => + entry.table === "open" && + entry.valid && + entry.raw && + (numericMatch ? entry.number === number : entry.id === trimmed), ); if (!row) return null; const normalized = `| ${cells(row.raw).join(" | ")} |`; diff --git a/src/components/clinical-dashboard/use-theme.ts b/src/components/clinical-dashboard/use-theme.ts index b5d5562d8e..d82bca7d07 100644 --- a/src/components/clinical-dashboard/use-theme.ts +++ b/src/components/clinical-dashboard/use-theme.ts @@ -81,6 +81,12 @@ function syncThemeColorMetadata(theme: ResolvedTheme) { } } +// The transition class comes off on a short timer. Track the pending timer so a +// rapid second toggle replaces it instead of stacking removals, and bail out if +// it fires after the owning environment is gone — a leaked firing after DOM test +// teardown ("document is not defined") intermittently failed Unit coverage. +let themeTransitionTimer: ReturnType | null = null; + function applyResolvedTheme(theme: ResolvedTheme) { const isCurrentlyDark = document.documentElement.classList.contains("dark"); const willBeDark = theme === "dark"; @@ -89,7 +95,10 @@ function applyResolvedTheme(theme: ResolvedTheme) { document.documentElement.classList.add("theme-transitioning"); document.documentElement.classList.toggle("dark", willBeDark); syncThemeColorMetadata(theme); - window.setTimeout(() => { + if (themeTransitionTimer !== null) clearTimeout(themeTransitionTimer); + themeTransitionTimer = setTimeout(() => { + themeTransitionTimer = null; + if (typeof document === "undefined") return; document.documentElement.classList.remove("theme-transitioning"); }, 200); } else { diff --git a/tests/repo-hygiene.test.ts b/tests/repo-hygiene.test.ts index 99d988231f..efe60bd3c6 100644 --- a/tests/repo-hygiene.test.ts +++ b/tests/repo-hygiene.test.ts @@ -737,6 +737,62 @@ describe("outstanding-issues inbox", () => { expect(() => applyRequest(stale, update)).toThrow(/stale|no longer open/); }); + it("fingerprints and closes ULID-display-id rows minted by reconcile", () => { + // Reconcile mints rows whose display id is the ULID's chars 10-16, not a + // legacy number. issues:done must be able to fingerprint and close them. + const ledger = [ + "# Outstanding issues", + "", + "", + "", + "## Recommended execution queue", + "", + "", + "", + "| Order | ID(s) |", + "| --- | --- |", + "| 1 | `#001` |", + "", + "## Open items", + "", + "", + "", + "| ID | Pri | Type | Summary | Detail / next action | Source | Added |", + "| --- | --- | --- | --- | --- | --- | --- |", + "| #001 | P2 | issue | original summary | original detail | source | 2026-01-01 |", + "| #6BG9X2 | P2 | task | reconciled summary | reconciled detail | source | 2026-08-17 |", + "", + "## Resolved / archive", + "", + "", + "", + "| ID | Type | Summary | Outcome | Resolved |", + "| ---- | ---- | ---- | ---- | ---- |", + "| #000 | issue | old | done | 2026-01-01 |", + "", + ].join("\n"); + + const fingerprint = issueRowFingerprint(ledger, "#6BG9X2"); + expect(fingerprint).not.toBeNull(); + expect(issueRowFingerprint(ledger, "#000")).toBeNull(); + expect(issueRowFingerprint(ledger, "#ZZZZZZ")).toBeNull(); + + const done = { + version: 2, + id: "99999999-9999-4999-8999-999999999999", + createdOn: "2026-08-17", + action: "done", + payload: { id: "#6BG9X2", outcome: "closed by test", baseRowFingerprint: fingerprint }, + }; + expect(validateRequest(done)).toEqual([]); + const applied = applyRequest(ledger, done); + expect(applied).not.toContain("| #6BG9X2 | P2 |"); + expect(applied.slice(applied.indexOf("## Resolved / archive"))).toContain("closed by test"); + + const stale = ledger.replace("reconciled summary", "mutated summary"); + expect(() => applyRequest(stale, done)).toThrow(/stale|no longer open/); + }); + it("rejects done requests targeting archived or nonexistent issues", () => { const ledger = [ "",