feat(repo): the page notices when it contradicts itself #5588
3 changed files+273−0
Addedsrc/__tests__/rendered-fact-crosscheck.test.ts+114−0View fileUnifiedSplit
@@ -0,0 +1,114 @@
1/**
2 * When one page states the same fact twice, the two had better agree.
3 *
4 * ccantynz/Vapron on 2026-08-30 rendered "Updated 9d ago" in its header,
5 * directly above a file list whose newest entry read "19 days ago". Same
6 * repository, same question — when did something last happen here — ten days
7 * apart, on one screen. Every individual signal was internally consistent: a
8 * real column, real commit dates, nothing threw. The contradiction existed
9 * only BETWEEN them, and nothing was looking there. The owner spotted it.
10 *
11 * These pin the behaviour that makes such a check worth running: it must
12 * catch that gap, stay quiet on ordinary git history, and never treat "we
13 * could not compute this" as a disagreement.
14 */
15
16import { describe, it, expect } from "bun:test";
17import {
18 findFreshnessContradiction,
19 FRESHNESS_DISAGREEMENT_MS,
20} from "../lib/rendered-fact-crosscheck";
21
22const header = (v: Date | null) => ({
23 label: "header Updated",
24 source: "repositories.pushed_at / git refs",
25 value: v,
26});
27const listing = (v: Date | null) => ({
28 label: "newest file in listing",
29 source: "git log per tree entry",
30 value: v,
31});
32
33describe("it catches the contradiction that shipped", () => {
34 it("flags the real Vapron numbers", () => {
35 const c = findFreshnessContradiction("ccantynz/Vapron", [
36 header(new Date("2026-08-21T17:17:41Z")), // what the header claimed
37 listing(new Date("2026-08-11T00:01:00Z")), // what the page showed below
38 ]);
39 expect(c).not.toBeNull();
40 expect(c!.message).toContain("ccantynz/Vapron");
41 // Names BOTH sides and the gap — "inconsistent" alone sends someone
42 // hunting for which number to believe.
43 expect(c!.message).toContain("header Updated");
44 expect(c!.message).toContain("newest file in listing");
45 // 10d 17h rounds to 11 — the point is that it states the size of the
46 // disagreement, not that it is off by a day.
47 expect(c!.message).toMatch(/11 day\(s\) apart/);
48 expect(c!.message).toContain("repositories.pushed_at");
49 });
50
51 it("is symmetric — it does not care which source is ahead", () => {
52 const a = findFreshnessContradiction("r", [
53 header(new Date("2026-08-21T00:00:00Z")),
54 listing(new Date("2026-08-11T00:00:00Z")),
55 ]);
56 const b = findFreshnessContradiction("r", [
57 header(new Date("2026-08-11T00:00:00Z")),
58 listing(new Date("2026-08-21T00:00:00Z")),
59 ]);
60 expect(a).not.toBeNull();
61 expect(b).not.toBeNull();
62 expect(a!.gapMs).toBe(b!.gapMs);
63 });
64});
65
66describe("it stays quiet when it should", () => {
67 it("tolerates ordinary skew between a push time and commit dates", () => {
68 // These are derived differently on purpose: a push lands commits authored
69 // earlier, a merge carries old dates. Hours of gap is normal and a check
70 // that fires on normal gets muted, at which point it protects nothing.
71 const base = new Date("2026-08-30T12:00:00Z");
72 const hoursEarlier = new Date(base.getTime() - 6 * 3600_000);
73 expect(findFreshnessContradiction("r", [header(base), listing(hoursEarlier)])).toBeNull();
74 // Right at the threshold is still quiet; past it is not.
75 const atLimit = new Date(base.getTime() - FRESHNESS_DISAGREEMENT_MS);
76 expect(findFreshnessContradiction("r", [header(base), listing(atLimit)])).toBeNull();
77 const pastLimit = new Date(base.getTime() - FRESHNESS_DISAGREEMENT_MS - 1000);
78 expect(findFreshnessContradiction("r", [header(base), listing(pastLimit)])).not.toBeNull();
79 });
80
81 it("never treats absence as disagreement", () => {
82 // "We could not compute this" is not a contradiction. Reporting it would
83 // fill the log with pages that simply had nothing to show — the noise
84 // that gets a check switched off.
85 const d = new Date("2026-08-30T12:00:00Z");
86 expect(findFreshnessContradiction("r", [header(d), listing(null)])).toBeNull();
87 expect(findFreshnessContradiction("r", [header(null), listing(null)])).toBeNull();
88 expect(findFreshnessContradiction("r", [header(null), listing(d)])).toBeNull();
89 });
90
91 it("ignores an unparseable date rather than comparing against NaN", () => {
92 const bad = new Date("not a date");
93 const good = new Date("2026-08-30T12:00:00Z");
94 expect(findFreshnessContradiction("r", [header(bad), listing(good)])).toBeNull();
95 });
96
97 it("needs two known values before it has an opinion", () => {
98 const d = new Date("2026-08-30T12:00:00Z");
99 expect(findFreshnessContradiction("r", [header(d)])).toBeNull();
100 expect(findFreshnessContradiction("r", [])).toBeNull();
101 });
102});
103
104describe("it reports, it does not repair", () => {
105 it("returns a finding and changes nothing about the values", () => {
106 // Silently adjusting a rendered number would erase the evidence that
107 // something upstream is wrong — which is how the original bug survived.
108 const h = new Date("2026-08-21T17:17:41Z");
109 const l = new Date("2026-08-11T00:01:00Z");
110 const c = findFreshnessContradiction("r", [header(h), listing(l)]);
111 expect(c!.facts.map((f) => f.iso)).toEqual([h.toISOString(), l.toISOString()]);
112 expect(h.toISOString()).toBe("2026-08-21T17:17:41.000Z");
113 });
114});
Addedsrc/lib/rendered-fact-crosscheck.ts+129−0View fileUnifiedSplit
@@ -0,0 +1,129 @@
1/**
2 * When one page states the same fact twice, the two had better agree.
3 *
4 * WHY THIS EXISTS. On 2026-08-30 the repository page for ccantynz/Vapron
5 * rendered "Updated 9d ago" in its header, directly above a file list whose
6 * newest entry read "19 days ago". Both numbers came from the same repository
7 * and described the same question — when did something last happen here — and
8 * they were ten days apart. The header was reading a database column that
9 * recorded a push which left no objects and no refs; the file list was reading
10 * git. Nothing noticed. The owner did, by eye.
11 *
12 * That is the shape worth generalising. Every individual signal was
13 * internally consistent — the column was a real column, the file dates were
14 * real dates, no code threw — and the contradiction only existed BETWEEN them.
15 * A platform that renders two derivations of one fact can check them against
16 * each other for almost nothing, and the alternative is that a person has to.
17 *
18 * WHAT THIS IS NOT. It is not a validator that decides which value is right;
19 * `repo-freshness.ts` does that, with a precedence rule and a corroboration
20 * test. This runs AFTER, on what is about to be rendered, and asks a smaller
21 * and more honest question: do the numbers on this page contradict each other?
22 * A disagreement is reported, never repaired — silently "fixing" a rendered
23 * value would destroy the evidence that something upstream is wrong.
24 *
25 * COST. Pure comparison of values the page has already computed. It adds no
26 * query, no git call and no I/O to the render path; the only work off the hot
27 * path is a fire-and-forget write when a contradiction is found, which by
28 * definition should be rare and is fingerprint-deduplicated by the error store.
29 */
30
31/** One rendered fact: a value, and where the page got it. */
32export interface RenderedFact {
33 /** Human name for the number as the reader sees it, e.g. "header Updated". */
34 label: string;
35 /** Where it came from, so a report says which source to distrust. */
36 source: string;
37 value: Date | null;
38}
39
40export interface FactContradiction {
41 subject: string;
42 message: string;
43 gapMs: number;
44 facts: ReadonlyArray<{ label: string; source: string; iso: string | null }>;
45}
46
47/**
48 * How far two renderings of "last activity" may drift before it is a bug.
49 *
50 * Twelve hours, not minutes. These are derived differently on purpose — a
51 * push time against the newest commit touching a tree entry — and legitimate
52 * gaps exist: a push lands commits authored earlier, a merge carries old
53 * dates. What is NOT legitimate is the header claiming activity days more
54 * recent than anything the repository can show for it. Vapron's gap was ten
55 * days; the threshold only has to be tighter than that to have caught it, and
56 * loose enough that ordinary git history never trips it.
57 */
58export const FRESHNESS_DISAGREEMENT_MS = 12 * 60 * 60_000;
59
60/**
61 * Compare renderings of one fact. Pure, and returns null when they agree.
62 *
63 * Deliberately ignores facts whose value is null. "We could not compute this"
64 * is not a contradiction, and treating absence as disagreement would fill the
65 * report with pages that simply had nothing to show — the noise that gets a
66 * check muted, which is how it stops protecting anything.
67 */
68export function findFreshnessContradiction(
69 subject: string,
70 facts: ReadonlyArray<RenderedFact>,
71 toleranceMs: number = FRESHNESS_DISAGREEMENT_MS
72): FactContradiction | null {
73 const known = facts.filter(
74 (f): f is RenderedFact & { value: Date } =>
75 f.value instanceof Date && !Number.isNaN(f.value.getTime())
76 );
77 if (known.length < 2) return null;
78
79 let newest = known[0]!;
80 let oldest = known[0]!;
81 for (const f of known) {
82 if (f.value.getTime() > newest.value.getTime()) newest = f;
83 if (f.value.getTime() < oldest.value.getTime()) oldest = f;
84 }
85
86 const gapMs = newest.value.getTime() - oldest.value.getTime();
87 if (gapMs <= toleranceMs) return null;
88
89 const days = Math.round(gapMs / 86_400_000);
90 return {
91 subject,
92 // Names both sides and the gap, because "inconsistent" alone sends
93 // somebody hunting for which number to believe.
94 message:
95 `${subject}: "${newest.label}" says ${newest.value.toISOString()} but ` +
96 `"${oldest.label}" says ${oldest.value.toISOString()} — ` +
97 `${days} day(s) apart on the same page. ` +
98 `Sources: ${newest.source} vs ${oldest.source}.`,
99 gapMs,
100 facts: facts.map((f) => ({
101 label: f.label,
102 source: f.source,
103 iso: f.value ? f.value.toISOString() : null,
104 })),
105 };
106}
107
108/**
109 * Report a contradiction, fire-and-forget.
110 *
111 * Never awaited by a renderer and never throws: a consistency check that can
112 * break the page it audits is worse than the inconsistency it looks for.
113 */
114export function reportContradiction(c: FactContradiction, path?: string): void {
115 void (async () => {
116 try {
117 const { recordError } = await import("./error-store");
118 await recordError({
119 source: "server",
120 kind: "rendered-fact-contradiction",
121 message: c.message,
122 path: path ?? null,
123 context: { subject: c.subject, gapMs: c.gapMs, facts: c.facts },
124 });
125 } catch {
126 /* the audit must never cost the reader their page */
127 }
128 })();
129}
Modifiedsrc/routes/web.tsx+30−0View fileUnifiedSplit
@@ -10,6 +10,10 @@ import { db } from "../db";
1010import { fireWebhooks } from "./webhooks";
1111import { config } from "../lib/config";
1212import { resolvePushedAt } from "../lib/repo-freshness";
13import {
14 findFreshnessContradiction,
15 reportContradiction,
16} from "../lib/rendered-fact-crosscheck";
1317import { normalizeRepoName, REPO_NAME_PATTERN } from "../lib/repo-name";
1418import { hostHas } from "../lib/host-capabilities";
1519import {
@@ -3350,6 +3354,7 @@ web.get("/:owner/:repo", async (c) => {
33503354 "",
33513355 tree.map((e) => e.name)
33523356 );
3357
33533358 const {
33543359 starCount,
33553360 starred,
@@ -3364,6 +3369,31 @@ web.get("/:owner/:repo", async (c) => {
33643369 forkedFrom,
33653370 } = starInfo;
33663371
3372 // Cross-check the two freshness numbers this page is about to print.
3373 //
3374 // The header's "Updated N ago" and the file list's newest entry answer the
3375 // same question from different sources. On 2026-08-30 they disagreed by ten
3376 // days on ccantynz/Vapron — header 9d, newest file 19d — and nothing
3377 // noticed; the owner spotted it by eye. Both values are already computed
3378 // here, so comparing them costs nothing and needs no query.
3379 //
3380 // Reported, never repaired: quietly adjusting a rendered number would erase
3381 // the evidence that something upstream is wrong.
3382 try {
3383 let newestEntryAt: Date | null = null;
3384 for (const c of Object.values(homeTreeCommits)) {
3385 const at = new Date(c.at * 1000);
3386 if (!newestEntryAt || at > newestEntryAt) newestEntryAt = at;
3387 }
3388 const contradiction = findFreshnessContradiction(`${owner}/${repo}`, [
3389 { label: "header Updated", source: "repositories.pushed_at / git refs", value: pushedAt },
3390 { label: "newest file in listing", source: "git log per tree entry", value: newestEntryAt },
3391 ]);
3392 if (contradiction) reportContradiction(contradiction, `/${owner}/${repo}`);
3393 } catch {
3394 /* an audit of the page must never be able to break the page */
3395 }
3396
33673397 // Health score badge — fire-and-forget, best-effort. If the DB call fails
33683398 // or repoId is null (anonymous view of non-DB repo), healthScore stays null
33693399 // and the badge simply doesn't render.
33703400
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts