Skip to content

v4.4 — Keystone Findings Remediation

Fixed the v4.3 Keystone Architecture Sweep's 18-item fix-before-ship shortlist — unwrapped multi-write transactions, duplicated business rules, and architecture-registry blind spots — including three live, currently-shipped regressions that were silently corrupting data.

Summary

The v4.3 Keystone Architecture Sweep (9 audit passes across 14 registry domains, PR #1402) was report-only — it found problems but changed no app code. v4.4 is the fix. Its source is the sweep's consolidated triage index, §6 "fix-before-ship shortlist": 18 numbered items. Two were already ticketed and shipped standalone before this milestone kicked off (OBJ-1276, OBJ-1280, OBJ-1282); this milestone's scope was the remaining 16, tracked as 14 Multica sub-issues (OBJ-1287–OBJ-1300) plus two follow-ons spawned mid-milestone (OBJ-1319, OBJ-1321) and one new finding caught during review (OBJ-1332).

Unlike a standard GSD milestone, this one was driven entirely through Multica crew sub-issues rather than GSD phases — Orion scoped 16 tickets into two logical stages (code + decisions in parallel, then docs), Codi implemented each fix against its existing sibling pattern in the same file or directory ("mirror the sibling, don't invent"), Roy reviewed, and each PR shipped independently straight to master on the beta weekly-release-train policy. Three of the fixes were live production bugs actively producing bad data: StreakRepairOfferResolver wrote duplicate gamification records on every eligible dashboard read, EnqueueClear's job_id mismatch made the admin demo-data-clear progress indicator always read idle, and FinalizeCommunityChallenge's unwrapped transaction meant a daily cron retry could re-send "badge earned" notifications indefinitely after any partial failure.

All 16 real sub-issues plus the mid-milestone follow-ons are done. Combined with the three findings ticketed standalone outside this epic (OBJ-1276, OBJ-1281, OBJ-1283 — all independently shipped), the entire 18-item v4.3 §6 shortlist is now closed.

Goal

Ship the real code + doc fixes for the v4.3 Keystone Architecture Sweep's §6 fix-before-ship shortlist. Unlike v4.3 (report-only, no app code touched), v4.4 changes rails_api/app/**, scripts/, and docs/architecture/**. Close out the audit's highest-confidence, lowest-cost defects — several of which are live, currently-shipped regressions accumulating bad data.

.planning/milestones/v4.4-keystone-findings-remediation-ROADMAP.md

Scope — What Shipped

  • Atomicity fixes (6) — wrapped previously-unwrapped multi-write sequences in ActiveRecord::Base.transaction, each with a forced-mid-sequence-failure spec proving rollback: GoalTracking::CheckInHabit, AiWorkforce::ProcessWebhookResult, Social::FinalizeCommunityChallenge, Ai::CoachService#persist_turn_safe, ContentModeration::ReviewContentFlag, CriticalPath::RecordPlay/AwardStimXp.
  • Idempotency / re-entrancy guards (2)Gamification::StreakRepairOfferResolver no longer creates duplicate UserAction/badge-feed writes on repeated reads; Social::FinalizeCommunityChallenge's push notifications are idempotent across transaction retries (caught mid-milestone during code review, OBJ-1332).
  • Boundary fix (1)AiWorkforce::PauseAiEmployee's GraphQL resolver no longer writes to AiEmployee directly; the write moved into a new AiWorkforce::PauseEmployee < Interaction::Base.
  • Deduplicated business rules (1 batched ticket, 3 rules)Gamification::RepairStreak's XP debit now routes through the canonical GamificationService.award_xp single-writer path; the AI check-in streak-milestone tier list was unified; AcceptPartnerRequest/AcceptAllyInvite's duplicated award_xp_to_both! logic moved to a new Social::PartnershipActivation service; the "fail closed if Settings.ai missing" guard — reimplemented 6 times — consolidated into Ai::SettingsGuard.enabled?.
  • Live-data regression fix (1)DemoData::EnqueueClear's job_id now matches the key ClearDemoDataJob records progress under.
  • Decisions resolved (2)promotion_criteria vs. promotion_requirements_met divergence on AiEmployee (kept the model's real validation, removed unenforced tenure gates from the GraphQL type); schedule-vs-retire GenerateAiCheckInPromptsJob (Josh chose schedule, ramped behind AI_CHECK_IN_INTERNAL_ALLOWLIST).
  • Registry fix (1)scripts/arch-domains.mjs audited and corrected registry-wide so capture-arch-evidence.mjs reports accurate glob matches for every previously-blind domain.
  • Follow-on: Crono scheduling (1)AiCoaching::GenerateAiCheckInPromptsJob, fully built but never scheduled, now runs daily at 10:00 via rails_api/config/cronotab.rb, gated behind the internal allowlist ramp — the job that fires all 5 already-shipped frontend check-in-prompt surfaces for the first time.
  • Docs (2 passes) — a consolidated Keystone docs-drift pass (11-item punch list: acts_as_paranoid model list, bounded-context counts, missing Teams section, contradictory GDPR sections, domain-events subscribers, arch-gates.md exception lists) and a follow-up reconciling the AI check-in scheduling docs once OBJ-1319/OBJ-1297 landed.

Fixes shipped (16 tickets, 2 stages)

TicketFindingPR(s)Highlights
OBJ-1287goal_tracking: CheckInHabit transaction wrap (§6-2)#1408Mirrors 3 sibling interactions in the same dir
OBJ-1288gamification: StreakRepairOfferResolver idempotency (§6-3)#1407Live data-corruption fix — stops duplicate writes on repeated dashboard reads
OBJ-1289ai_workforce: ProcessWebhookResult atomicity (§6-4)#1411Terminal status commits last — mid-sequence failure stays retryable
OBJ-1290social: FinalizeCommunityChallenge transaction + re-processing guard (§6-6)#1412 · #1432Worst blast-radius fix in the sweep — unconditional daily cron; 3 CI-infra-blocked rounds, see Key Decisions
OBJ-1291ai_coaching: persist_turn_safe transaction wrap (§6-7)#1414No orphaned unanswered "user" turn on mid-sequence failure
OBJ-1292content_moderation: ReviewContentFlag transaction + re-entrancy guard (§6-8)#1416Mirrors sibling bulk_review_content_flags.rb
OBJ-1293critical_path: RecordPlay/AwardStimXp atomicity (§6-9)#1415Retry now recovers a crashed Stim XP award instead of losing it
OBJ-1294platform: EnqueueClear job_id mismatch (§6-1)#1417Live regression — admin progress indicator was always stuck on idle
OBJ-1295ai_workforce: PauseAiEmployee → extract interaction (§6-12)#1422 · #1424Closes the last resolver-writes-directly boundary violation in AI Workforce
OBJ-1296g27 batch: RepairStreak XP bypass + AI check-in tier list + award_xp_to_both + Settings.ai guard ×6 (§6-14a/b/c)#1421 · #14263 unrelated dedup findings bundled — all 218 existing specs on touched call sites passed unchanged
OBJ-1297DECISION: schedule-or-retire GenerateAiCheckInPromptsJob (§6-10)Josh: schedule + internal-allowlist ramp; spawned OBJ-1319
OBJ-1298DECISION+fix: promotion_criteria vs promotion_requirements_met divergence (§6-13)#1418Roy reversed his own first ruling after finding it would have disabled autonomous promote for every employee — see Key Decisions
OBJ-1299tooling: arch-domains.mjs registry-wide audit-and-fix (§6-17)#1420Coordinated with standalone OBJ-1283 (teams glob) rather than duplicating it
OBJ-1300docs: consolidated Keystone docs-drift pass (§6-15/16/18 + §9)#1410All 11 §9 punch-list items closed; ran before all code landed (see Key Decisions)
OBJ-1319ai_coaching: schedule GenerateAiCheckInPromptsJob via Crono + allowlist ramp (follow-on to OBJ-1297)#14195 already-shipped frontend surfaces go live for the first time
OBJ-1321ai_coaching: reconcile check-in-prompt scheduling docs (follow-on to OBJ-1319)#1437Replaced OBJ-1300's deliberately-neutral "not scheduled" wording with the shipped outcome
OBJ-1332social: FinalizeCommunityChallenge push notifications can duplicate on transaction retry (new finding, child of OBJ-1290)#1438Roy caught this reviewing OBJ-1290 — the idempotency fix's own retry path could re-send notifications

Every fix mirrors an established sibling pattern already in the same file or directory rather than inventing a new one (e.g. EnqueueReseed for OBJ-1294, WelcomeBackOfferResolver for OBJ-1288, BulkReviewContentFlags for OBJ-1292) — the fidelity contract set at kickoff.

Key Decisions

  • Schedule, not retire, GenerateAiCheckInPromptsJob — Josh confirmed Option A (schedule) over retiring the fully-built, never-scheduled job, and specifically chose to add an AI_CHECK_IN_INTERNAL_ALLOWLIST ramp before opening it to all users, over shipping immediately — a notification-fatigue/cost guard. Codi had already implemented and pushed the scheduling fix off a duplicate decision ticket created during the kickoff race (see below), skipping the ramp; that branch was held until the ramp was added.
  • promotion_confirmed_at gate rejected, twice-ruled — Roy independently ruled on whether AiEmployee's promotion flow needed a promotion_confirmed_at check twice, on two different tickets created by a duplicate-skeleton race, and reached opposite conclusions. The second, more thorough ruling won: do not add the check — promotion_confirmed_at is only ever set during the same mutation call that performs the promotion, so gating on it up front would have permanently disabled the "Promote to Autonomous" button for every AI employee. The fix (OBJ-1298 / PR #1418) instead removed the GraphQL type's own invented weeks_active tenure gates, which disagreed with the model's real promotion_requirements_met validation.
  • Docs ran ahead of code, deliberately worded around it — OBJ-1300's docs pass (PR #1410) merged while 5 code tickets were still open. It caused no damage: 10 of its 11 punch-list items were pre-existing doc drift unrelated to the atomicity fixes; the one item that touched an open decision (GenerateAiCheckInPromptsJob's schedule) was worded as "not yet scheduled" rather than asserting an outcome, specifically to avoid pre-empting OBJ-1297. OBJ-1321 later reconciled that wording once the decision shipped.
  • FinalizeCommunityChallenge blocked 3 rounds on CI infra, not codePlaywright Smoke Tests (Preview) was cancelled three times by the smoke-preview-lane repo-wide concurrency group under heavy PR volume (the same root cause as the already round-capped OBJ-1237). Riley escalated directly to a human rather than re-asking Tess for an answer already capped at 3 rounds; the fix itself was clean throughout and eventually merged via PR #1412 + a follow-up CHANGELOG PR #1432.
  • Duplicate sub-issue skeleton at kickoff — a parallel process created the canonical 14-ticket skeleton (OBJ-1287–1300) the same minute Orion, working from an empty issue children check, built a second 16-ticket skeleton (OBJ-1303–1318) on its epic: true handback. Both had dispatched Codi on the same fixes. Orion cancelled its own 16 duplicates once the collision was found; the canonical set is what shipped.
  • A cancelled duplicate skeleton produced a false "16/16 complete" stage signal — because the cancelled duplicates were the only staged children, and cancelled is a terminal status, the Multica stage barrier reported "Stage 1: 16/16 done" while the real, unstaged canonical work was still roughly half in flight. Orion caught it before the epic closed early. The canonical set was deliberately never retrofitted with stages afterward (tickets were already scattered across every status; retrofitting would have gated nothing).

Requirements Coverage

This milestone was driven entirely through Multica crew sub-issues rather than GSD phases, so there is no v4.4-keystone-findings-remediation-REQUIREMENTS.md or formal /gsd-audit-milestone pass — the same pattern as the v3.11 and v2.4 batches (see Project History). Coverage is tracked directly against the ROADMAP's Definition of Done and the source findings doc's §6 shortlist:

CategoryCountStatus
ATOMICITY-* (unwrapped multi-write → transaction)6All satisfied
IDEMPOTENCY-* (unguarded repeat side effect)2All satisfied
BOUNDARY-* (resolver writing outside the interaction layer)1Satisfied
DEDUP-* (duplicated business rule → shared home)3 (batched, 1 ticket)All satisfied
REGISTRY-* (arch-domains.mjs glob correctness)1Satisfied
DECISION-* (product/architecture call)2Both resolved, fixes landed
DOCS-* (architecture doc-drift reconciliation)2 passesSatisfied

16 / 16 real sub-issues done (OBJ-1287–1300, OBJ-1319, OBJ-1321) plus the mid-milestone OBJ-1332 finding, also done. The 16 cancelled tickets (OBJ-1303–1318) are duplicate-skeleton artifacts from the kickoff race, not open work. Combined with the three findings held out as standalone tickets — OBJ-1276, OBJ-1281, OBJ-1283 — which have all also shipped independently, the full 18-item v4.3 §6 fix-before-ship shortlist is closed.

Outcomes

  • Three live data-integrity bugs are stopped: StreakRepairOfferResolver no longer writes duplicate gamification records on repeated dashboard reads, EnqueueClear's admin progress indicator reflects real job state, and FinalizeCommunityChallenge's daily cron can no longer re-send duplicate "badge earned" notifications after a partial failure (and its own retry path can no longer duplicate push notifications either, per OBJ-1332).
  • Six previously-unwrapped multi-write sequences across goal tracking, AI Workforce, AI coaching, content moderation, and Critical Path are now atomic, each with a spec proving rollback on mid-sequence failure.
  • Three duplicated business rules (gamification XP writes, AI check-in tier lists, partner/ally XP-award logic, and a 6x-reimplemented AI settings guard) now have one shared home each.
  • AiWorkforce::PauseAiEmployee no longer writes outside the Interaction layer — closes the last known resolver-boundary violation flagged by the sweep.
  • scripts/arch-domains.mjs's registry is accurate for every domain the sweep flagged as blind, so capture-arch-evidence.mjs reports real coverage going forward.
  • The AI check-in prompt job is live (internal allowlist only, for now) — 5 frontend surfaces that were dark since their own shipping milestone now function.
  • Architecture docs (docs/architecture/ddd/*, arch-gates.md) reflect the current, post-fix state of the codebase, not the v4.3-era drift the sweep found.

Tech Debt

  • (carried forward, not silently dropped) Live-data cleanup — the OBJ-1288 and OBJ-1290 fixes stop future corruption but do not clean up already-written duplicate UserAction/badge-feed rows or already-sent duplicate notifications from before the fixes landed. No cleanup was performed. Needs its own ticket if volume warrants — Orion's call, not mechanically ticketed here.
  • (carried forward) CI smoke-lane concurrency (smoke-preview-lane, OBJ-1237 / OBJ-1290) — the contention that blocked OBJ-1290 three times is a standing infra problem, human-escalated, not fixed by this milestone.
  • (docs gap, this pass) Not every fix ticket has its own CHANGELOG.md entry — the highest-drama fixes (OBJ-1290, OBJ-1295, OBJ-1296, OBJ-1298, OBJ-1319/1321) do; the six smaller atomicity/idempotency fixes (OBJ-1287, 1289, 1291–1294, 1299, 1332) landed without one. All are internal correctness fixes with no user-visible behavior change, which is why this wasn't a blocker, but a future pass could backfill them for full engineering-log fidelity.
  • (process, filed for future milestones) Create the sub-issue skeleton exactly once, and re-run issue children immediately before creating it — the duplicate-skeleton race above cost real double-dispatch time. Stage the real ticket set at creation time if a docs-after-code ordering needs to be enforced; retrofitting later gates nothing. Never trust a stage-completion signal without checking actual child statuses — a cancelled set of duplicates falsely reported 16/16 done here.
  • 5c94b0fe9 — fix(goal_tracking): wrap CheckInHabit's write sequence in a transaction (OBJ-1287) (#1408)
  • 7dbd0d59a — fix(gamification): guard StreakRepairOfferResolver against duplicate offer writes (OBJ-1288) (#1407)
  • 4cf4c9f7a — fix(ai_workforce): wrap ProcessWebhookResult writes in a transaction (g32) (#1411)
  • 98a20ae85 — fix(social): wrap FinalizeCommunityChallenge participant loop in transaction + idempotency guard (OBJ-1290) (#1412)
  • f2846eda2 — fix(ai_coaching): wrap CoachService#persist_turn_safe writes in a transaction (OBJ-1291) (#1414)
  • f82bd767e — fix(content_moderation): wrap ReviewContentFlag in transaction with re-entrancy guard (#1416)
  • 0efc97004 — fix(critical-path): always attempt Stim XP award so retries recover from a crashed award (OBJ-1293) (#1415)
  • 56ff63778 — fix(demo-data): mirror EnqueueReseed job_id pattern in EnqueueClear (OBJ-1294) (#1417)
  • 43191b260 — fix(ai_workforce): extract PauseAiEmployee resolver writes into an interaction (#1422)
  • 5a3d0cd2b — fix(gamification): route RepairStreak XP debit through GamificationService (OBJ-1296) (#1421)
  • ee57f29d8 — fix(ai_workforce): remove unenforced weeks_active gate from promotion_criteria (#1418)
  • 221b63d46 — fix(tooling): registry-wide audit-and-fix pass for scripts/arch-domains.mjs (OBJ-1299) (#1420)
  • db0556fca — docs: v4.3 Keystone Sweep §9 doc-drift roll-up (#1410)
  • 29875e165 — feat(ai_coaching): schedule GenerateAiCheckInPromptsJob via Crono (OBJ-1319) (#1419)
  • 31661082b — docs(ai_coaching): reconcile check-in-prompt scheduling docs to shipped 10:00 + allowlist ramp (OBJ-1321) (#1437)
  • 9a06b42d7 — fix(social): make FinalizeCommunityChallenge push notifications idempotent across transaction retries (OBJ-1332) (#1438)

Last updated: 2026-07-11

Loading…