The loop earned its keep
PR #39 shipped the Sentinel and, alongside it, a read-only moderation-audit loop — MHE's reflective guardrail, "are our guardrails still working?". On its first run the loop did exactly its job: it flagged an untrusted-input → model → player path the rollout had missed. This PR closes it (plus a minor silent-block the loop also raised), and re-runs the loop to confirm the fix.
Two gaps. One real: /api/research (the Archivist brief) returned a model brief to the player with no moderation, while every other model surface had it — a crafted POST could use the Archivist as an unmoderated model proxy. One minor: /api/suggestions blocked a custom objective silently.
The research gate
The fix gives /api/research the same two stages as the turn loop and the Archive lookup: a deterministic hardBlockCheck on the untrusted subject before the model runs, then moderateAction (detector + Sentinel) over the generated brief before it ships. A block returns { blocked, error } — the shape the store already knows how to surface.
Input hard-block up front, Sentinel review of the brief after — the brief never ships if either trips.
server/api/research.post.ts · 66 lines
⋯ 31 lines hidden (lines 1–31)
Surfacing the block (no more silent failures)
Two new store notices, studyNotice and suggestionsNotice, mirror the existing moderationNotice / archiveNotice — one per surface that takes untrusted input into a model. The study notice is cleared on the same paths that clear a stale study (re-study, re-ground, reset); the suggestions notice replaces the old silent empty-list. The suggestions block previously masqueraded as success: true with no ideas; now it says why.
studyActiveFigure surfaces a blocked brief as a notice, not a study; loadSuggestions surfaces a blocked objective instead of an empty list.
stores/game.ts · 973 lines
⋯ 451 lines hidden (lines 1–451)
⋯ 57 lines hidden (lines 479–535)
⋯ 419 lines hidden (lines 555–973)
Two distinct red block banners, consistent with the dispatch + Archive banners.
components/FigurePicker.vue · 458 lines
⋯ 131 lines hidden (lines 1–131)
⋯ 18 lines hidden (lines 138–155)
⋯ 296 lines hidden (lines 163–458)
Verification
410 unit tests (two new: a blocked study surfaces a notice not a brief; a blocked objective surfaces a notice not an empty list), clean typecheck, 12/12 Playwright e2e, PR CI green. The fix targets the exact path the moderation-audit loop flagged — the same loop, run on demand (npm run loop:moderation-audit), is the standing check that it stays closed. That loop's first run is what surfaced this gap; closing the loop on its own signal is the whole point of a reflective guardrail.
The discriminating tests — a moderation block is a distinct notice, never silently dropped.