Skip to content

fix(brief-compile): refuse to compile a UTC day that hasn't ended#869

Merged
biwasxyz merged 1 commit into
mainfrom
fix/compile-guard-incomplete-day
Jul 16, 2026
Merged

fix(brief-compile): refuse to compile a UTC day that hasn't ended#869
biwasxyz merged 1 commit into
mainfrom
fix/compile-guard-incomplete-day

Conversation

@biwasxyz

Copy link
Copy Markdown
Contributor

Closes #815.

The bug

A brief is compiled once and never recompiled. So a signal filed after its day's compile is orphaned permanently — it can never reach the brief for its own UTC date, and no later brief covers it.

POST /api/brief/compile defaulted to date=today. Compiling mid-day therefore silently drops the rest of the day. Every brief from 2026-06-24 to 2026-07-14 was compiled this way:

Brief Compiled Day captured Orphaned
06-27 01:44Z 7% 93%
06-30 03:12Z 13% 87%
07-01 03:53Z 16% 84%
07-08 05:01Z 21% 79%
07-13 19:31Z 81% 19%

Even the best compile time in that window orphaned ~5 hours of filings.

Correspondents experienced this as editorial silence and filed #683 ("signals scoring 100 stuck in submitted while score-88 sit approved") and #660 ("Review Queue Discrepancy: Persistent Backlog"). It was never judgement — the brief had closed before their day was over. A recovery pass on 2026-07-15 recompiled 5 uninscribed briefs and swept +31 signals back in across 9 correspondents.

Why a doc or a runbook wouldn't hold

This is #815's own baseline, from April, under the previous publisher:

2026-04-24 brief → compiled 2026-04-25T05:02Z   (5.0h AFTER day end)
2026-04-25 brief → compiled 2026-04-26T05:02Z   (5.0h)
2026-04-30 brief → compiled 2026-05-01T05:19Z   (5.3h)

The correct pattern — compile the day after it ends — was in place, and #815 was filed about a minor 5.1h → 8.4h drift within it. @Robotbot69 stated the contract explicitly in May: "/api/brief/{D} compiles AFTER day D ends (UTC)", and it was acknowledged in writing.

Then the Publisher role transferred (#836/#837, 2026-06-18) and the very first brief after it — 06-24 — was already same-day. The knowledge lived in a comment thread; it didn't survive the handoff.

Meanwhile the contract appears in no doc in this repo, and both the route and the MCP tool advertised "Defaults to today." Nobody violated a rule they'd agreed to — they called the tool the way the tool documents it. The undocumented requirement was that you must never use the default.

So this makes the wrong thing unrepresentable rather than adding another note to be lost:

  • Default moves from getUTCDate(now) to getUTCYesterday(now). That helper already existed — seven lines below the one in use (helpers.ts:16).
  • date >= today (UTC) → 400, naming the date to use instead.

Blast radius

Compiling an incomplete day is never correct in production — it overwrites the brief and fires brief_inclusion payouts against a partial roster — so nothing legitimate is blocked.

  • Existing tests pass explicit past dates (compile("2026-04-10")) → unaffected.
  • seed.sh:239 posts -d '{}' with no btc_address and already 400s at brief-compile.ts:36, before any date logic → unchanged.
  • Recompiling a past date still works, which is what the recovery pass relies on. Inscribed briefs remain locked by the existing 409 in recordBriefSignals.

Tests

src/__tests__/brief-compile-day-guard.test.ts (5 tests). Verified they fail for the right reason by reverting the guard:

× rejects an explicit date of today
    AssertionError: expected 'Not enough signals to compile (found …' to contain 'has not ended yet'
× rejects a future date
    AssertionError: expected 'Not enough signals to compile (found …' to contain 'has not ended yet'
× defaults to yesterday, not today — the call that caused the regression
    AssertionError: expected 400 to be 201

The default test seeds yesterday and asserts the resulting brief's date, because asserting merely "didn't hit the day guard" passes under both defaults and proves nothing. The 2 remaining tests (past dates still compile; format validation still runs first) pass under both by design — they guard against breaking existing behavior rather than catching the regression.

Full suite: 45 files, 450 tests passing. Typecheck clean.

Follow-up (not in this PR)

The MCP wrapper still documents "Date to compile the brief for (YYYY-MM-DD). Defaults to today." That description should change to match — filing separately against aibtc-mcp-server, since it's the fourth instance of wrapper/API drift (see #402, #457, #597, #603, #604).

A brief is compiled once and never recompiled, so any signal filed after
the compile is orphaned permanently — it can never reach the brief for its
own UTC date, and no later brief covers it. Compiling date=today mid-day
therefore silently drops the tail of the day.

Every brief from 2026-06-24 to 2026-07-14 was compiled this way:

  06-27  compiled 01:44Z   captured  7%   orphaned 93%
  06-30  compiled 03:12Z   captured 13%   orphaned 87%
  07-01  compiled 03:53Z   captured 16%   orphaned 84%
  07-13  compiled 19:31Z   captured 81%   orphaned 19%

Correspondents read the result as editorial silence and filed #683 / #660
about high-scoring signals stuck in `submitted`. It wasn't judgement; the
brief had closed before their day was over.

The contract — a brief covers a complete UTC day, so compile after it ends
— was known (agent-news#815, May) but written down nowhere in this repo,
while both the route and the MCP wrapper documented "defaults to today".
The obvious call was the broken one, so a note wouldn't have held; this
makes the wrong thing unrepresentable instead.

- Default target moves from getUTCDate(now) to getUTCYesterday(now). The
  helper already existed, seven lines below the one in use.
- date >= today (UTC) returns 400 naming the date to use instead.

Compiling an incomplete day is never correct in production — it overwrites
the brief and fires brief_inclusion payouts against a partial roster — so
nothing legitimate is blocked. Existing tests pass explicit past dates and
are unaffected. seed.sh's compile step posts no btc_address and already
400s before reaching any date logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
agent-news a35148c Jul 15 2026, 03:54 PM

@github-actions

Copy link
Copy Markdown
Contributor

Preview deployed: https://agent-news-staging.hosting-962.workers.dev

This preview uses sample data — beats, signals, and streaks are seeded automatically.

@arc0btc arc0btc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix, and the write-up is one of the better root-cause postmortems I've seen on this repo — the historical compile-time table plus the April baseline really nails down that this was a knowledge-loss-on-handoff problem, not a one-off mistake.

① Functionality — Verified the logic directly against helpers.ts on the PR branch: getUTCYesterday/getUTCDate are pre-existing, UTC-safe (setUTCDate + toISOString().slice(0,10)), so no new date-math risk. The date >= today guard is a plain string comparison, which is safe here only because validateDateFormat enforces zero-padded YYYY-MM-DD before the guard runs (/^\d{4}-\d{2}-\d{2}$/) — lexical and chronological ordering agree for that format. Confirmed the ordering in the diff: format check → day guard → MIN_SIGNALS/compile, which matches what the "still validates date format before the day guard" test asserts.

② Security — No new surface. Auth/publisher-gate checks are untouched and run before this logic.

③ Performance — Two cheap string ops added to a low-QPS admin route. Non-issue.

④ Clean code — The inline comment above the guard usefully cites the concrete incident numbers (#683, #660) instead of a vague "see bug tracker," which will save someone a git-blame trip later.

⑤ Big-picture fit — Matches the pattern already established by resolveAgentNames/getConfig fail-closed handling elsewhere in this file. Good call filing the MCP wrapper doc-drift separately against aibtc-mcp-server rather than scope-creeping this PR — that's the right sequencing, especially given the wrapper/API drift precedent (#402/#457/#597/#603/#604) is a recurring pattern worth someone eventually fixing structurally, not just patching per-instance.

Code quality notes: Nothing to add — the test file's regression test (asserting the resulting brief date, not just "didn't hit the guard") is exactly the kind of test-quality detail that's easy to skip and easy to regret skipping.

No blocking or suggestion-level issues found. Approving.

@biwasxyz
biwasxyz merged commit 9b852ff into main Jul 16, 2026
7 checks passed
@biwasxyz
biwasxyz deleted the fix/compile-guard-incomplete-day branch July 16, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: per-day brief compile — May 2 never compiled, May 4-6 latency regressed from ~5.1h to ~8.4h post-day-end

2 participants