{ "provenance": { "sourceHead": "ca16058341c2e073051340d9653bbbc900f3ca36", "sourceProof": ".context/ship-source-af-delta-paid-20260909-v1/sdk-reports-ledger-v1/proof-v1.json", "sourceProofSHA256": "20169897aa7a4a4434c4bcc67d4fdca3f8b13d572400c513b8741bc4c5fab42c", "sourceReports": [ { "path": ".context/ship-source-af-delta-paid-20260909-v1/sdk-reports-ledger-v1/4d0b8839-feed-4fbc-b612-f7531c13d6c2-report.md", "sha256": "26c82847fb02ecdd2d8d452b6be569e0eff6028bfe50da639c7961f297a6c07b", "bytes": 29481 }, { "path": ".context/ship-source-af-delta-paid-20260909-v1/sdk-reports-ledger-v1/cf44b2fe-0639-4970-81ba-016494739ee7-report.md", "sha256": "1c23d482482f44de9878a15daff13fdc6ff05a7cae1e64782de01c43000b664f", "bytes": 30690 } ], "actualTestOutcomes": [ "failed", "failed" ], "paidCoverageCredit": false }, "retryReport": "# Plan: cache profile summaries in one process\n\n## Measured problem and accepted scope\nThe existing profile-summary service has one active process. A one-week trace\nshows repeated reads of about 900 hot keys: DB CPU is 70%, with read p95 120 ms.\nAdd a process-local LRU wrapper to the existing repository. Acceptance targets\nare at least 60% cache hits, DB CPU below 50%, and read p95 below 60 ms, with the\nexisting error-rate and correctness SLOs unchanged. This is an internal backend\nchange with no UI, API, schema, pricing, or developer onboarding change.\n\n## Existing contracts retained\n- All reads and writes use this repository in the same process; there are no\n external DB writers. Multi-process operation remains unsupported and startup\n rejects that configuration while caching is enabled.\n- Authentication and authorization run before repository access. Keys encode\n the authenticated tenant ID and validated profile ID without ambiguity.\n Values are immutable profile-summary DTOs; secrets and cache keys are never\n logged. Cached results cannot bypass authorization.\n- The existing LRU adapter supports 1000 entries, a 16 MiB byte cap, and a\n 30-second TTL. Recorded hot data fits those limits. Absent records use a\n distinct sentinel with a 10-second TTL; undefined means a cache miss.\n- Cache operations are synchronous and atomic in the single JS event loop.\n On any cache failure the existing adapter bypasses the cache until an empty\n cache is reinitialized; repository errors keep the current typed API error\n mapping. The existing per-key\n single-flight wrapper coalesces simultaneous misses and releases on failure.\n- A read already in progress when a write commits may return its earlier DB\n snapshot to that caller. Every read begun after that write completes must\n observe the committed version. TTL expiry is not a substitute for this rule.\n\n## Proposed wrapper integration (amended by CEO review, 2026-09-09)\nKeep the current read-through repository interface and shared adapters. The\noriginal sketch (read: get, miss, DB read, set; write: DB write, delete) had no\ncoordination between a cache fill and a write. Review finding F1 showed that\nviolates the read-after-write contract above, so the ordering rules now include\na per-key fill-generation guard. These are the complete read/write ordering\nrules:\n\n```javascript\n// F1: fill-generation guard. singleFlight is the existing per-key coalescer,\n// extended (or wrapped) with a per-key generation and an invalidate(key) call.\nasync function readProfile(key) {\n if (!cacheEnabledFor(key)) return repository.read(key); // F3: one shared flag evaluation\n const cached = cache.get(key); // F4: adapter decodes the absent sentinel\n if (cached !== undefined) return cached;\n return singleFlight.run(key, async (gen) => {\n const value = await repository.read(key); // absent -> adapter stores sentinel (F4)\n if (singleFlight.generation(key) === gen) cache.set(key, value); // stale fill after a write is dropped\n return value;\n });\n}\n\nasync function writeProfile(key, update) {\n if (!cacheEnabledFor(key)) return repository.write(key, update);\n const saved = await repository.write(key, update);\n singleFlight.invalidate(key); // bump generation, drop the in-flight entry so later readers refill\n cache.delete(key); // same tick as invalidate: atomic relative to other tasks\n return saved;\n}\n```\n\nAccepted amendments (details in the review record below):\n- F1: `singleFlight.invalidate(key)` on successful write; fill only stores when\n its captured generation is still current. Generation state lives only for\n keys with an in-flight fill, so it is bounded by concurrent misses.\n- F3: `cacheEnabledFor(key)` is one deterministic function used by both paths.\n Any flag-state change (off/on or any percentage change in either direction)\n reinitializes both the cache and the single-flight state as empty.\n- F4: the adapter owns sentinel encode/decode. A cached absence must produce\n the identical result the repository produces for an uncached absence. If the\n repository signals absence by throwing its typed not-found error, the wrapper\n catches only that class, stores the sentinel, and rethrows.\n\n## Verification and rollout (amended by CEO review, 2026-09-09)\nExisting repository contract tests cover tenant isolation, key validation,\nabsence, DB failures, and authorization. New wrapper tests cover hit/miss,\neviction and byte limits, TTL, adapter-failure fallback, successful-write\ninvalidation, failed-write preservation, and concurrent-miss coalescing.\n\nAdded by review (see Implementation Tasks T1-T4):\n- Controlled-schedule ordering tests for the fill/write race, both the\n late-set variant and the single-flight-join variant (F1).\n- Absent-sentinel wrapper tests: sentinel hit serves no DB read, expires at\n 10 s, is invalidated by a create, and is indistinguishable from an uncached\n absence (F4).\n- Flag-transition tests: every percentage change flushes; read and write\n evaluate the flag identically for the same key (F3).\n- Bypass observability: `profile_cache_bypass_active` gauge, one structured\n log line on entering bypass naming the adapter error class and no key, alert\n when the gauge stays 1 for 5 minutes or hit ratio is below 0.6 for 15 minutes\n at 100% rollout, runbook step \"flag off then on reinitializes\" (F2).\n\nThe rollout uses the existing runtime feature flag: enable for 10% of keys,\nthen 50%, then all keys after one healthy hour at each stage. Monitor hit/miss,\neviction, cache bytes, fallback errors, bypass gauge, DB CPU, and read p95\nwithout raw IDs. On error-rate or latency regression, disable the flag\nimmediately; both reads and writes bypass the cache while disabled, and any\nflag-state change creates an empty cache. Cold starts remain within the\nexisting DB capacity. The service owner monitors the rollout and records the\nresults against the acceptance targets.\n\n## Out of scope\nDistributed caching, cross-process coherence, prewarming, changing consistency\nsemantics, or adding new product surfaces. The repository interface preserves a\nfuture replacement path without introducing a general cache framework now.\n\n## CEO Review Record (HOLD SCOPE, 2026-09-09)\n\nRun conditions: automated run, no human present. System audit, environment\nsetup, telemetry, codebase exploration, and all shell commands (review-log,\ndecision-log, tasks JSONL, review-read) were skipped per run rules. Every\ndecision point that would call AskUserQuestion was auto-decided to the skill's\nrecommended option and is recorded here. `codex_reviews: disabled` in the\nrun config: the outside-voice step was skipped in full, no native fallback.\n\n### Step 0 outcomes\n- **0A Premise:** Right problem. The trace quantifies the pain (70% DB CPU,\n p95 120 ms on ~900 hot keys). Doing nothing leaves DB headroom at 30% with\n no lever. A process-local cache is the most direct path given one process\n and no external writers. Not a proxy problem.\n- **0B Existing leverage:** Reuses the LRU adapter (limits, TTL, sentinel,\n failure bypass), the per-key single-flight wrapper, the typed API error\n mapping, the runtime flag, and the repository contract tests. Nothing is\n rebuilt. Gap: the single-flight wrapper needs a generation/invalidate\n extension (F1); no existing mechanism covers it.\n- **0C Dream state:**\n ```\n CURRENT STATE THIS PLAN 12-MONTH IDEAL\n every read hits DB ---> read-through LRU behind the ---> same repository interface,\n 1 process, 70% CPU same interface, flagged, cache backend swappable\n coherent with writes (local or shared) if a\n second process is ever needed\n ```\n Moves toward the ideal: the interface is unchanged and the wrapper is the\n seam a future backend would replace.\n- **0C-bis Alternatives (D1, auto-decided):**\n ```\n APPROACH A: Inline wrapper functions in the repository module (plan's structure)\n Effort S | Risk Low | Reuses adapter, single-flight, flag\n + smallest diff, interface unchanged - flag/cache/repo logic share one module\n APPROACH B: Decorator class implementing the repository interface\n Effort M | Risk Low | Reuses the same pieces\n + clean bypass (return raw repo when off), isolated tests - new class, more files, same behavior\n APPROACH C: Cache at the DB query layer\n Effort L | Risk Med\n + transparent to callers - loses the DTO-level sentinel and key contract; wrong layer\n RECOMMENDATION: A. Note: options differ in kind, not coverage. Smallest diff that\n cleanly expresses the change; B is a taste call with no behavioral gain in this scope.\n ```\n Decision: A (recommended, auto-chosen).\n- **0F Mode:** HOLD SCOPE, specified by the user. No question asked.\n- **0D HOLD SCOPE analysis:** Touches 2-3 files (repository module, single-flight\n helper, tests) and adds no new class. No complexity smell. Minimum change is\n the sketch plus the F1 guard; F1 is not deferrable because it is required to\n meet a stated invariant. Nothing else can be cut without breaking a stated\n requirement.\n- **0E Temporal interrogation:** Decisions resolved now rather than mid-build:\n where generation state lives (F1: inside single-flight, bounded by in-flight\n keys), where sentinel encode/decode lives (F4: adapter), what \"enabling\n creates an empty cache\" means for partial percentage changes (F3: every\n change flushes), and who acts on bypass (F2: alert + runbook). Human ~6 h,\n CC ~45 min for the whole change.\n\n### Findings registry (each recorded once; sections cross-reference by ID)\n\n| ID | Sev | Section | Evidence | Selected remedy | Decision | Residual risk | Verification |\n|----|-----|---------|----------|-----------------|----------|---------------|--------------|\n| F1 | CRITICAL GAP | 1, 4 | Sketch fills cache after an awaited DB read with no version check; plan states \"no additional version checks or coordination\". Schedule S1 below shows a read begun after a completed write returning the pre-write value for up to 30 s, violating the retained contract. Single-flight join variant (S1 R3) has the same effect. | Per-key fill generation captured before the DB read; `invalidate(key)` on successful write bumps it and drops the in-flight entry; fill stores only when generation matches. Sentinel fills use the same guard. | Accepted (auto, recommended, Completeness 10/10 vs 4/10 for \"document as acceptable\", which is prohibited by the requirement rule) | Generation state must be cleared on cache reinitialize; covered by \"new single-flight state\" in F3 flush. | Tests T1: pause/release both S1 orders; assert R2 and R3 read DB and return v2; assert cache holds no v1 after R1 resumes. |\n| F2 | WARNING | 8 | Adapter failure puts the process in bypass \"until an empty cache is reinitialized\". Plan monitors a fallback-error counter only; no gauge, alert, or named recovery action. Correct results continue, so the perf regression is silent. | `profile_cache_bypass_active` gauge; one structured log at bypass entry (error class, stage, no key); alerts: gauge 1 for 5 min, or hit ratio below 0.6 for 15 min at 100%; runbook: flag off then on reinitializes, repeat means leave off and file bug. | Accepted (auto, recommended, 10/10 vs 6/10 counter-only) | Alert thresholds are initial guesses; tune after first week. | Test T4: inject adapter throw, assert gauge 1, log line without key, reads served from DB; flag cycle resets gauge to 0. |\n| F3 | WARNING | 9 | Plan defines an empty cache only on \"enabling\". A 50% to 10% partial rollback then re-advance would expose entries whose keys had writes bypass `cache.delete` while disabled. Per-key percentage evaluation on read and write is not stated to be the same function. | Any flag-state change reinitializes cache and single-flight state. One deterministic `cacheEnabledFor(key)` used by both paths. | Accepted (auto, recommended, 10/10 vs 7/10 \"document: only use full disable\") | Each stage advance is a cold start; plan already states cold starts fit DB capacity. | Test T3: enable K, write K while K disabled, re-enable, assert miss and fresh DB read; property test that read/write evaluation agree for the same key and flag state. |\n| F4 | WARNING | 5, 2, 6 | Sketch returns `cached` directly and calls `cache.set(key, value)`, so sentinel encode/decode is unspecified. If the repository signals absence by throwing, the sketch never caches absence and the 10 s sentinel is dead code. Wrapper test list omits sentinel hit, TTL, and create-invalidation. | Adapter owns encode/decode; cached absence yields the identical result as uncached absence; if repository throws typed not-found, wrapper catches only that class, stores sentinel, rethrows. | Accepted (auto, recommended, 10/10 vs 6/10 \"positive caching only\") | Repository absence form not verified (codebase not explored); implementer confirms in T2. | Test T2: absent read twice within 10 s hits DB once and both results are identical; after 10 s DB is read again; create invalidates sentinel via F1 path. |\n\n### Section outcomes (1-11)\n1. **Architecture:** 1 finding (F1). Diagram D1 below. No new coupling beyond\n repository to single-flight generation API. SPOF is the single process\n (pre-existing, documented). 10x load: 900 hot keys still fit; 100x: entry\n and byte caps evict, hit rate falls, DB absorbs the rest. Rollback: flag off\n (seconds) or revert (no migration).\n2. **Error & rescue map:** 7 error paths mapped, 0 unrescued gaps after F2 and\n F4. Registry below. No catch-all handlers introduced; the only new catch is\n the typed not-found class in F4.\n3. **Security & threat model:** No issues found. No new endpoints, inputs, or\n dependencies. Keys carry tenant and validated profile ID, so cross-tenant\n reads via key manipulation are not possible; auth runs before the cache.\n Memory exhaustion bounded by 16 MiB and 1000 entries. Logs exclude keys\n (F2 log line asserts this). Low likelihood, low impact across the board.\n4. **Data flow & edge cases:** 1 finding (F1, async ordering). Shadow paths\n traced in D2; all handled. No user-visible interaction (backend only).\n5. **Code quality:** 1 finding (F4). readProfile has 3 branches, writeProfile 2;\n no complexity concern. No DRY violation: flag evaluation is one function by\n F3. Not over-engineered: generation state is the minimum coordination that\n satisfies the invariant.\n6. **Tests:** Diagram D7 produced; 4 gaps, each closed by T1-T4. 2am test: S1\n schedule tests. Hostile QA: write during coalesced fill with 50 waiters.\n Chaos: adapter throws mid-set. Pyramid: unit-heavy, correct. Flakiness:\n TTL tests must use an injected clock, not wall time.\n7. **Performance:** No issues found. Hit path is one synchronous map lookup.\n Max memory 16 MiB plus bounded generation entries. No new DB queries,\n indexes, or connections. Coalescing already bounds miss storms.\n8. **Observability:** 1 finding (F2). Existing metric list is adequate once the\n bypass gauge, alert, and runbook exist. Three-week debuggability: bypass\n entry log names the error class and time.\n9. **Deployment & rollout:** 1 finding (F3). No migration. Flag-gated, staged,\n with explicit rollback (D5, D6). Post-deploy checklist: first 5 min watch\n error rate and bypass gauge; first hour watch hit ratio and p95.\n10. **Long-term trajectory:** No issues found. Reversibility 5/5. Debt: one\n small generation API in single-flight, documented in code comment with\n diagram D3. A new engineer reading this plan in 12 months sees the invariant\n and the guard side by side.\n11. **Design & UX:** SKIPPED, justified: no UI scope (internal backend change).\n\n### Outside voice\nCodex review skipped (codex_reviews disabled). Re-enable: `gstack-config set\ncodex_reviews enabled`. No native fallback was dispatched; disabled is an\nintentional opt-out. The review-log persistence command was not run in this\nsession (no shell per run rules); outside_status is recorded here as disabled.\n\n### NOT in scope\n- Distributed or shared cache: no second process exists; startup rejects it.\n- Prewarming: cold start fits DB capacity per plan.\n- Cache framework or decorator class (Approach B): no behavioral gain now.\n- Alert-threshold tuning beyond initial values: needs a week of data.\n\n### What already exists\n- LRU adapter with caps, TTL, sentinel, failure bypass: reused as is.\n- Per-key single-flight wrapper: reused, extended with generation/invalidate (F1).\n- Typed API error mapping: reused unchanged.\n- Runtime feature flag: reused; flush-on-change rule added (F3).\n- Repository contract tests: reused; wrapper tests added (T1-T4).\n\n### Dream state delta\nAfter this plan the repository interface is unchanged and the cache is a\nsingle seam. The 12-month ideal (swappable backend) needs only a new adapter\nbehind the same wrapper. Nothing here forecloses it.\n\n### Error & Rescue Registry\n```\nMETHOD/CODEPATH | WHAT CAN GO WRONG | EXCEPTION CLASS | RESCUED? | RESCUE ACTION | USER SEES\n---------------------------|------------------------------------------|----------------------------------|----------|-------------------------------------------------|---------------------------\nreadProfile cache.get/set | adapter fault (size calc, accounting) | adapter's internal error classes | Y | adapter enters bypass; F2 gauge+log; DB read | nothing (slower)\nreadProfile repository.read| DB timeout / connection failure | existing typed repository errors | Y (exist)| rethrown; single-flight releases; nothing cached| existing API error\nreadProfile repository.read| record absent | typed not-found (or null result) | Y | F4: store sentinel 10 s; return identical result| existing not-found\nreadProfile singleFlight | fill rejects with N waiters coalesced | same repository error | Y (exist)| all waiters receive the error; entry released | existing API error\nwriteProfile repository.write| validation / conflict / timeout | existing typed repository errors | Y (exist)| rethrown; cache untouched (failed-write test) | existing API error\nwriteProfile invalidate/delete| adapter fault | adapter's internal error classes | Y | adapter bypass; F2 gauge+log | nothing\ncacheEnabledFor(key) | flag client lookup failure | flag client's existing error | Y (exist)| treated as disabled; existing flag client metric| nothing (slower)\n```\nImplementer confirms the concrete class names for adapter and flag-client\nerrors in T4; catch-all handlers are not permitted.\n\n### Failure Modes Registry\n```\nCODEPATH | FAILURE MODE | RESCUED? | TEST? | USER SEES? | LOGGED?\n--------------------|---------------------------------------|----------|-----------|---------------------|--------\nreadProfile fill | stale fill after completed write (F1) | Y (guard)| Y (T1) | correct value | n/a\nreadProfile join | later reader joins pre-write fill (F1)| Y (guard)| Y (T1) | correct value | n/a\nreadProfile | absent result, sentinel path (F4) | Y | Y (T2) | not-found (same) | n/a\nreadProfile | DB error on miss | Y | Y (exist) | API error | Y (exist)\nadapter | cache fault -> bypass (F2) | Y | Y (T4) | nothing, slower | Y (F2)\nwriteProfile | DB write fails | Y | Y (exist) | API error | Y (exist)\nflag | partial rollback leaves stale entries (F3)| Y (flush)| Y (T3) | correct value | flag change log (exist)\nflag | lookup failure | Y | Y (exist) | nothing, slower | Y (exist)\n```\nBefore remedies: F1 was RESCUED=N, TEST=N, USER SEES=stale value silently,\na CRITICAL GAP. After accepted remedies: 0 CRITICAL GAPS remain.\n\n### TODOS.md updates\n0 items proposed. Every evidenced gap is an in-scope repair (T1-T4). No\nhypothetical capacity or optional feature is surfaced in HOLD SCOPE.\n\n### Diagrams\nD1 System architecture and dependencies (before -> after):\n```\nBEFORE: caller -> auth -> repository -> DB\nAFTER: caller -> auth -> repository wrapper -> cacheEnabledFor(flag)\n | hit | miss | off\n v v v\n LRU adapter singleFlight(gen) ---> DB\n (sentinel, ^ invalidate(key) on write\n bypass) |\n writeProfile ---> DB, then invalidate + delete\nNew edge: repository wrapper -> singleFlight generation API (F1). No other new coupling.\n```\nD2 Data flow with shadow paths:\n```\nkey \u2500\u25b6 cacheEnabledFor \u2500\u25b6 cache.get \u2500\u25b6 singleFlight.run \u2500\u25b6 repository.read \u2500\u25b6 gen check \u2500\u25b6 cache.set \u2500\u25b6 value\n \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502\n nil/invalid: rejected undefined=miss join in-flight absent: sentinel mismatch: adapter fault:\n upstream by key validation sentinel=absent (same gen only) (F4) skip set (F1) bypass (F2)\n (existing tests) error: all waiters error: typed, nothing cached\n```\nD3 State machine, cache entry for key K:\n```\n fill (gen ok, value) write / TTL 30s / evict\n [ABSENT] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 [VALUE] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 [ABSENT]\n \u2502 fill (gen ok, absent) write (create) / TTL 10s\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 [SENTINEL] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 [ABSENT]\n Invalid: [ABSENT] -> [VALUE] via stale fill after write (prevented by gen mismatch, F1)\n Invalid: entry survives a flag change (prevented by flush, F3)\n Process-level: [ACTIVE] --adapter fault--> [BYPASS] --flag off/on--> [ACTIVE, empty]\n```\nD4 Error flow: see registry; every path ends in \"existing typed API error\",\n\"served from DB\", or \"correct value\", never in a swallowed error.\nS1 Async ordering schedule (F1 evidence):\n```\n t | R1 read (begins before W) | W write | R2 read (begins after W) | cache[K] | DB[K]\n 1 | get(K) -> undefined | | | - | v1\n 2 | await repository.read -> v1 | | | - | v1\n 3 | paused | await write commits v2 | | - | v2\n 4 | paused | delete(K) (no entry) | | - | v2\n 5 | paused | returns | | - | v2\n 6 | resume: set(K, v1); return v1 | | | v1 STALE | v2\n 7 | | | get(K) -> v1; return v1 | v1 | v2\n VIOLATION t7: R2 began after W completed (t5), observes v1 for up to 30 s.\n Variant R3: begins at t5, misses, joins R1's in-flight promise, returns v1. Same violation.\n Guarded: t4 invalidate(K) bumps gen, drops in-flight; t6 gen mismatch -> no set;\n t7 miss -> fresh fill -> v2. R3 at t5 finds no in-flight entry -> fresh fill -> v2.\n Allowed by contract: R1 itself returns v1 (read in progress when write committed).\n```\nD5 Deployment sequence:\n```\ndeploy code (flag 0%) -> smoke: reads/writes bypass, gauge 0\n -> flag 10% (flush) -> 1 h healthy -> flag 50% (flush) -> 1 h healthy -> flag 100% (flush)\n -> record hit ratio, DB CPU, p95 against targets\n```\nD6 Rollback flowchart:\n```\nregression seen? \u2500\u2500 no \u2500\u2500\u25b6 continue stage\n \u2502 yes\n \u25bc\nflag 0% (both paths bypass, cache emptied) \u2500\u2500\u25b6 error/latency recovered?\n \u2502 yes: investigate with bypass log + metrics \u2502 no: revert deploy (no migration)\n```\nD7 Test map (Section 6):\n```\nNEW DATA FLOWS: read hit; read miss+fill; read absent (sentinel); write invalidate; flag bypass\nNEW CODEPATHS: gen match/mismatch; invalidate(key); cacheEnabledFor; flush on flag change\nNEW ASYNC WORK: single-flight fill with concurrent write (S1 both orders)\nNEW INTEGRATIONS: none external\nNEW ERROR PATHS: adapter fault -> bypass + gauge; typed not-found -> sentinel + rethrow\nCoverage: unit (all above, injected clock and deferred DB promises); integration\n(existing contract tests run with flag on and off); no E2E needed.\n```\n\n### Stale diagram audit\nNot audited: codebase exploration was skipped per run rules. T1 includes\nchecking the repository module, adapter, and single-flight helper for existing\nASCII diagrams and updating them to show the generation guard.\n\n## Implementation Tasks\nSynthesized from this review's findings. Each task derives from a specific\nfinding above. Run with Claude Code or Codex; checkbox as you ship.\n\n- [ ] **T1 (P1, human: ~3h / CC: ~20min)** \u2014 repository wrapper + single-flight \u2014 Add per-key fill generation and `invalidate(key)`; store only on generation match\n - Surfaced by: Section 1/4 \u2014 F1, schedule S1\n - Files: repository module, single-flight helper, wrapper tests\n - Verify: deferred-promise tests for both S1 orders; R2 and R3 return v2 and hit DB; no v1 in cache after R1 resumes; update any existing ASCII diagrams in touched files\n- [ ] **T2 (P1, human: ~1.5h / CC: ~10min)** \u2014 LRU adapter \u2014 Own sentinel encode/decode; cached absence identical to uncached absence\n - Surfaced by: Section 5 \u2014 F4\n - Files: adapter, repository module, wrapper tests\n - Verify: two absent reads within 10 s hit DB once with identical results; DB re-read after 10 s; create invalidates sentinel\n- [ ] **T3 (P2, human: ~1h / CC: ~10min)** \u2014 flag gating \u2014 Single `cacheEnabledFor(key)`; flush cache and single-flight state on any flag-state change\n - Surfaced by: Section 9 \u2014 F3\n - Files: repository module, flag wiring, wrapper tests\n - Verify: enable K, write K while disabled, re-enable, assert miss; read/write evaluation agrees for every key and flag state\n- [ ] **T4 (P2, human: ~1.5h / CC: ~10min)** \u2014 observability \u2014 Bypass gauge, entry log without key, two alerts, runbook entry\n - Surfaced by: Section 8 \u2014 F2\n - Files: adapter bypass path, metrics/alert config, runbook doc\n - Verify: injected adapter throw sets gauge 1 and emits log with error class and no key; flag cycle resets gauge; alert rules lint\n- _No new tasks from Section 3 (Security)._\n- _No new tasks from Section 7 (Performance)._\n- _No new tasks from Section 10 (Trajectory)._\n- _No new tasks from Section 11 (Design, skipped)._\n\nTasks JSONL artifact: not written (no shell in this run). Autoplan\naggregation for this phase must be regenerated from the list above.\n\n### Completion Summary\n```\n +====================================================================+\n | MEGA PLAN REVIEW \u2014 COMPLETION SUMMARY |\n +====================================================================+\n | Mode selected | HOLD SCOPE |\n | System Audit | skipped per run rules |\n | Step 0 | HOLD; approach A (inline wrapper) auto-chosen|\n | Section 1 (Arch) | 1 issue found (F1) |\n | Section 2 (Errors) | 7 error paths mapped, 0 GAPS after F2/F4 |\n | Section 3 (Security)| 0 issues found, 0 High severity |\n | Section 4 (Data/UX) | 9 edge cases mapped, 0 unhandled after F1 |\n | Section 5 (Quality) | 1 issue found (F4) |\n | Section 6 (Tests) | Diagram produced, 4 gaps (closed by T1-T4) |\n | Section 7 (Perf) | 0 issues found |\n | Section 8 (Observ) | 1 gap found (F2) |\n | Section 9 (Deploy) | 1 risk flagged (F3) |\n | Section 10 (Future) | Reversibility: 5/5, debt items: 1 (gen API) |\n | Section 11 (Design) | SKIPPED (no UI scope) |\n +--------------------------------------------------------------------+\n | NOT in scope | written (4 items) |\n | What already exists | written |\n | Dream state delta | written |\n | Error/rescue registry| 7 methods, 0 CRITICAL GAPS |\n | Failure modes | 8 total, 0 CRITICAL GAPS (1 fixed: F1) |\n | TODOS.md updates | 0 items proposed |\n | Scope proposals | 0 proposed, 0 accepted (HOLD) |\n | CEO plan | skipped (HOLD) |\n | Outside voice | codex, disabled |\n | Lake Score | 4/4 recommendations chose complete option |\n | Diagrams produced | 8 (arch, data flow, state, error, schedule, |\n | | deploy, rollback, test map) |\n | Stale diagrams found | not audited (see Stale diagram audit) |\n | Unresolved decisions | 0 |\n +====================================================================+\n```\n\n### Unresolved Decisions\nNone. D1 and F1-F4 were auto-decided to the recommended option per run\nrules; no destructive or irreversible option was involved.\n\n## GSTACK REVIEW REPORT\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | clean | mode: HOLD_SCOPE, 4 findings accepted, 0 critical gaps remaining |\n| Outside Review | codex (`codex_reviews: disabled`) | Independent 2nd opinion | 0 | disabled | \u2014 |\n| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 0 | \u2014 | \u2014 |\n| Design Review | `/plan-design-review` | UI/UX gaps | 0 | \u2014 | \u2014 |\n| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | \u2014 | \u2014 |\n\n- **OUTSIDE COVERAGE:** provider codex, phase plan-review, status disabled by config; no native fallback dispatched; no outside findings.\n- **VERDICT:** CEO CLEARED \u2014 eng review required\n\nNO UNRESOLVED DECISIONS\n", "retrySchedule": "S1 Async ordering schedule (F1 evidence):\n```\n t | R1 read (begins before W) | W write | R2 read (begins after W) | cache[K] | DB[K]\n 1 | get(K) -> undefined | | | - | v1\n 2 | await repository.read -> v1 | | | - | v1\n 3 | paused | await write commits v2 | | - | v2\n 4 | paused | delete(K) (no entry) | | - | v2\n 5 | paused | returns | | - | v2\n 6 | resume: set(K, v1); return v1 | | | v1 STALE | v2\n 7 | | | get(K) -> v1; return v1 | v1 | v2\n VIOLATION t7: R2 began after W completed (t5), observes v1 for up to 30 s.\n Variant R3: begins at t5, misses, joins R1's in-flight promise, returns v1. Same violation.\n Guarded: t4 invalidate(K) bumps gen, drops in-flight; t6 gen mismatch -> no set;\n t7 miss -> fresh fill -> v2. R3 at t5 finds no in-flight entry -> fresh fill -> v2.\n Allowed by contract: R1 itself returns v1 (read in progress when write committed).\n```", "retryFinding": "| F1 | CRITICAL GAP | 1, 4 | Sketch fills cache after an awaited DB read with no version check; plan states \"no additional version checks or coordination\". Schedule S1 below shows a read begun after a completed write returning the pre-write value for up to 30 s, violating the retained contract. Single-flight join variant (S1 R3) has the same effect. | Per-key fill generation captured before the DB read; `invalidate(key)` on successful write bumps it and drops the in-flight entry; fill stores only when generation matches. Sentinel fills use the same guard. | Accepted (auto, recommended, Completeness 10/10 vs 4/10 for \"document as acceptable\", which is prohibited by the requirement rule) | Generation state must be cleared on cache reinitialize; covered by \"new single-flight state\" in F3 flush. | Tests T1: pause/release both S1 orders; assert R2 and R3 read DB and return v2; assert cache holds no v1 after R1 resumes. |", "firstGuardedEvidence": "| F1 | CRITICAL | \"no additional version checks or coordination between a cache fill and a write\" vs contract \"every read begun after that write completes must observe the committed version\". Schedule S1 below shows a fill storing V0 after V1 committed. Same race caches `ABSENT` after a create. | D2: `stale` flag on the in-flight single-flight record; write marks it after commit; stale fill returns but does not store. Counter `profile_cache_fill_skipped_stale`. | None for the invariant. A burst of writes on a hot key lowers hit ratio for that key only. | Paused-fill test, both orders, plus create-after-absent variant (T1). |\n\nS1 Async schedule (F1), shared state = cache[K]\n```\n t | Read R1 (fill) | Write W | cache[K]\n 1 | get(K) \u2192 undefined | | empty\n 2 | await repository.read(K) | | empty\n 3 | DB snapshot = V0 | await repository.write(K) | empty\n 4 | | commit V1 | empty\n 5 | | finally: mark stale, delete | empty\n 6 | | return (W complete) | empty\n 7 | resume: stale \u2192 no store | | empty (unguarded: V0 stored = violation)\n 8 | R2 begins: miss \u2192 read V1 | | V1\n Other order: R1 resumes at t5 before W's continuation, stores V0; W's delete at t5 removes it. Safe.\n```" }