feat(awol): accountable-day count + days-AWOL severity report (#159)#162
Merged
Conversation
Implements #159 (parent PRD #157, ADR 0008). /awol now flags on accountable days — UTC calendar dates in (lastPost, today] not covered by a valid LOA window — instead of raw time since last forum post, and renders the agreed days-AWOL severity report. - New pure calc utils.AccountableDaysAWOL(lastPost, now, []LOAEntry) int: candidate set minus the union of inclusive [Start,End] LOA ranges, overage past the 7-day requirement. Overlap/adjacent/multiple/future windows fall out of set coverage; backwards ranges are ignored and DEBUG-logged. Exhaustively table-tested. Companion utils.RawDaysAWOL / utils.DaysSinceLastPost and a clock-injected utils.ActiveWindow selector. - loaCacheReader gains GetEntries (drops GetEntry); diverges from /loa's loaCacheView, which stays {GetEntry, IsHealthy} and single-window (#161 #6). - AwolUser models the LOA backing as *utils.LOAEntry (nil = none) instead of the zero-value LOAEntry + HasLOAEntry pair (#161 #4). - Active-window selection and the accountable-day verdict both run against the handler's single injected now off one GetEntries read per user, so selection and verdict can't disagree at a boundary (#161 #2). - Render: worst-first by days AWOL (tie-break username), severity glyphs (🔴 >14 · 🟠 >7 · 🟡 >0 · ⚪ active LOA), milpac + [LOA] thread links, "N total · M LOA" summary, "AWOL days subtracts valid LOA days." footer. - Degraded mode (unhealthy cache): raw inactivity, NO LOA subtraction, loud "accountable-day adjustment SKIPPED; LOA NOT subtracted" warning on both embed and file paths; summary reads "LOA unknown", never a silent non-LOA all-clear. /loa is unchanged. No-LOA troopers render raw == accountable as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… degraded empty result Reserve the summary-line prefix + separator length when sizing the per-chunk budget so the final embed description (summary + "\n\n" + chunk) can never exceed Discord's 4096-char cap (was: raw chunk capped at 4096, then ~18 bytes of prefix prepended → API 400). Also surface the degraded/SKIPPED warning on the empty-result reply when the LOA cache is unhealthy, so an unhealthy cache is never reported as a silent all-clear (every other terminal degraded path already warns). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validLOAWindows previously only dropped end-before-start ranges; a zero-value LOAEntry (both dates zero) slipped through and would "cover" the epoch-zero UTC date. Unreachable from production today (parseLOAPost only emits when both dates parse) but cheap to harden. Also reword the coveredByLOA/validLOAWindows comments: a zero-width range (StartDate == EndDate) is VALID and covers exactly one day — only end-before-start covers nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… comments
IsActive had zero callers after this PR switched /awol to ActiveWindow/isActiveAt
(`rg IsActive` shows only its own def). Delete it and scrub its stale doc comment.
Correct loaCacheReader's doc (it is {GetEntries,IsHealthy}, a different surface
from /loa's {GetEntry,IsHealthy}, not a superset) and /loa's loaCacheView comment
that still claimed /awol's reader had the same {GetEntry,IsHealthy} shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…age gaps New behavioral coverage: - embed description stays <= 4096 with a near-limit accumulated chunk - unhealthy cache + empty list surfaces the SKIPPED warning; healthy stays all-clear New boundary/contract coverage: - DaysSinceLastPost no-clamp contract vs RawDaysAWOL; rawAccountableDates early-return guard - severityGlyph EXACT tier edges (14->orange, 7->yellow) + active-LOA override - ActiveWindow now==StartDate inclusive edge and the no-active (_, false) return - validLOAWindows drops a zero-value entry; mixed slice still subtracts the valid window Also fix test fixture comment drift (expired-LOA EndDate, scratch glyph notes) and the IsActive reference in the isActiveAt doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework TestRunAwol_EmbedDescriptionWithinDiscordLimit so a chunk provably lands in the danger band (4077, 4096]: 51 no-LOA rows render at exactly 80 bytes each, so the pre-fix budget (4096) packs all 51 into one chunk (19-byte summary prefix + 4080 = 4099 > 4096) while the prefix-aware budget (4096 - 19 = 4077) flushes after 50 rows (4019 <= 4096). The old ~139-byte lines stepped over the band, so the test passed even against the un-fixed budget. Assert on bytes (the conservative production measure) across every emitted embed, and reference discordEmbedDescriptionLimit. Verified: PASS on current code, FAIL (4099 bytes) against a reverted budget, PASS after restore. Relabel TestAccountableDaysAWOL_MixedSliceValidStillSubtracts as a calc-level smoke test: it does not pin the validLOAWindows end-before-start filter (that branch is guarded by TestValidLOAWindows_MixedSliceKeepsValid); coveredByLOA's range check already covers nothing for a backwards window. No production logic changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #159. Parent #157. Builds on #158 (LOA history store).
What changed
/awolnow counts accountable days — raw inactivity minus valid LOA-covered dates — instead of flagging on raw time since last forum post, and renders the agreed days-AWOL severity report.Pure calc (highest seam, exhaustively table-tested):
utils.AccountableDaysAWOL(lastPost, now, windows) int— candidate UTC dates(lastPost, today]minus the union of inclusive[StartDate, EndDate]LOA ranges, overage past 7, clamped at 0. Backwards/zero ranges covered nothing and are DEBUG-logged. Companions:RawDaysAWOL,DaysSinceLastPost,ActiveWindow(windows, now).Display: flag when days AWOL > 0, sorted days-AWOL desc (tie-break username). Glyphs: 🔴
>14· 🟠>7· 🟡>0· ⚪ active-LOA override (with thread link, incl. the "on LOA, still AWOL" row). SummaryN total · M LOA, footerAWOL days subtracts valid LOA days.Degraded mode (
IsHealthyfalse): raw whole-date inactivity, NO LOA subtraction, still flags>7, footer⚠️ LOA cache unavailable — accountable-day adjustment SKIPPED; figures are raw inactivity (LOA NOT subtracted)., summaryN total · LOA unknown. Applies to both embed and file-output paths; never a silent non-LOA all-clear.Folded-in PR #161 re-review deferrals
GetEntriesand threads a single injectednowinto both the calc andActiveWindow, so flag/figure/[[LOA]]link can't disagree at a boundary.GetEntriesstays clock-free.loaWindow *utils.LOAEntry(nil = none), replacing the zero-valueLOAEntry+HasLOAEntrypair.loaCacheReadergainsGetEntries(dropsGetEntry), diverging from/loa'sloaCacheView{GetEntry, IsHealthy}./loauntouched (single-window per LOA history store: multi-window per user, ThreadID-keyed, 1-year retention #158/S3).Regression guard
No-LOA troopers render exactly as before (raw == accountable).
/loabyte-identical (zero diff).Gate
lint 0 ·
go mod tidyclean ·go test ./... -racepass · floors OK (utils 89.8% ≥ 87, commands 83.0% ≥ 81) · build OK. Independently re-verified by orchestrator.User-facing
/awolrendering change — needs a test-guild smoke test (CI can't exercise the live Discord gateway).🤖 Generated with Claude Code