{ "sourceHead": "12faead4636b97305348e25fc12258a56fcf6868", "sourceProof": ".context/ship-source-ai-delta-paid-20260910-v1/sdk-first-report-ledger-v1/proof.json", "sourceProofSha256": "97a89224783a5c5dba5e8c5cd9663f5493fda4164dd51646277a005243cb53c8", "report": "# Plan: cache profile summaries in one process\n\n> Reviewed by `/plan-ceo-review` on 2026-09-10, mode HOLD SCOPE (user-selected).\n> Original plan content is preserved below. Accepted amendments are marked\n> `[AMENDED Dn]` and reference the decision registry in the review record.\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\nKeep the current read-through repository interface and shared adapters.\n\n`[AMENDED D4, D5, D9]` The original sketch (read-through fill, delete-on-write,\n\"no additional coordination between a cache fill and a write\") violates the\nread-after-write contract above (finding F1). The ordering rules below replace\nit. \"Write completes\" means `writeProfile` settles for its caller.\n\n```javascript\n// inflight: existing per-key single-flight wrapper, extended with\n// run(key, fn) -> joins or creates entry {promise, stale:false}\n// invalidate(key) -> entry.stale = true; detach entry so later callers start fresh\n// cache: existing LRU adapter; get() maps the absent-sentinel back to null (D3)\nasync function readProfile(key) {\n if (!flag.cacheEnabled(key)) return repository.read(key); // same bucket fn as writes (D9)\n const cached = cache.get(key);\n if (cached !== undefined) return cached;\n return inflight.run(key, async (entry) => {\n const value = await repository.read(key); // typed errors propagate; entry released\n if (!entry.stale) cache.set(key, value); // D4: skip fill if a write committed meanwhile\n return value;\n });\n}\n\nasync function writeProfile(key, update) {\n try {\n return await repository.write(key, update);\n } finally {\n inflight.invalidate(key); // D4: in-flight fills for this key must not land\n cache.delete(key); // D5/D9: always, regardless of outcome or flag bucket\n }\n}\n```\n\n`[AMENDED D3]` The repository's absent result is `null`. The adapter maps\n`null` to the absent sentinel on `set` and back to `null` on `get`; the wrapper\nnever sees the sentinel and `undefined` remains the only miss signal.\n\n`[AMENDED D9]` Any change to the runtime flag value (on, off, or percentage)\nresets the cache to empty. The read and write paths evaluate the same\ndeterministic key-to-bucket function.\n\n## Verification and rollout\nExisting repository contract tests cover tenant isolation, key validation,\nabsence, DB failures, and authorization. `[AMENDED D6]` That suite also runs\nagainst the cache-enabled wrapper on a warm cache (each case read twice).\n\nNew wrapper tests cover hit/miss, eviction and byte limits, TTL,\nadapter-failure fallback, successful-write invalidation, `[AMENDED D5]`\nfailed-write invalidation (replaces \"failed-write preservation\"), and\nconcurrent-miss coalescing. `[AMENDED D4]` Ordering tests use a pausable\nrepository stub and cover both completion orders of an in-flight miss against a\nwrite, the late-joiner case, and the absent-sentinel variant (see Section 4\nschedule). `[AMENDED D9]` Flag tests cover bucket parity between reads and\nwrites and flush on percentage change. Oversize single values, negative caching,\nand DTO mutation isolation have explicit assertions (Section 6).\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, DB CPU, and read p95 without raw IDs.\n`[AMENDED D7]` Read latency is recorded per path (`hit`, `miss`, `bypass`) so\nthe p95 target is checked against all reads and diagnosed per path.\n`[AMENDED D8]` Two alerts ship with the change: cache in bypass or fallback\nstate for more than 5 minutes, and hit ratio below 40% for 15 minutes after a\n5-minute warmup. The runbook entry is in Section 8.\nOn error-rate or latency regression, disable the flag immediately; both reads\nand writes bypass the cache while disabled, and enabling creates an empty cache.\nCold starts remain within the existing DB capacity. The service owner monitors\nthe rollout and records the results 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)\n\nRun context: automated headless capture. System audit, environment setup,\ntelemetry, codebase exploration, and all shell commands were skipped by run\nrules. Base branch: `main` (from git status snapshot). No design doc or handoff\nnote was checked (skipped audit). Every decision point below was auto-decided to\nthe recommended option per run rules and is recorded in the decision registry.\nOutside review: disabled by `.gstack-section-state-PL5pWm/config.yaml`\n(`codex_reviews: disabled`); no fallback dispatched.\n\n### Step 0\n\n**0A Premise.** Measured, not hypothetical: one-week trace, 900 hot keys, DB CPU\n70%, p95 120 ms. A process-local cache is the most direct lever for repeated\nreads in a single-process service. Doing nothing leaves the DB one traffic step\nfrom saturation. Not a proxy problem. OK.\n\n**0B Existing code leverage.** Reused: LRU adapter (limits, TTL, sentinel,\nfallback), per-key single-flight wrapper, runtime feature flag, repository\ncontract tests, typed error mapping. Nothing is rebuilt. The single-flight\nwrapper gains one method (`invalidate`) under D4.\n\n**0C Dream state.**\n```\n CURRENT STATE THIS PLAN 12-MONTH IDEAL\n repo -> DB on every read repo <- LRU wrapper, flagged, same repo seam; cache\n DB CPU 70%, p95 120 ms ---> invariant-safe fills ---> impl swappable (local or\n single process DB CPU <50%, p95 <60 ms distributed) w/o caller change\n```\nMoves toward the ideal: the seam stays at the repository interface.\n\n**0C-bis Implementation alternatives.**\n```\nAPPROACH A: Sketch as written (minimal) Effort S Risk High Completeness 5/10\n Pros: fewest lines; reuses adapters unchanged\n Cons: violates read-after-write contract (F1); late joiners get stale snapshots;\n failed ambiguous writes leave stale entries (F2)\n Reuses: LRU adapter, single-flight, flag\nAPPROACH B: Sketch + in-flight invalidation (recommended) Effort S Risk Low Completeness 9/10\n Pros: meets the stated invariant with a bounded structure (no per-key counters);\n one new method on an existing wrapper; smallest diff that is correct\n Cons: single-flight wrapper changes; needs ordering tests with a pausable stub\n Reuses: everything in A plus the single-flight entry as the invalidation token\nAPPROACH C: Write-through + in-flight invalidation (ideal-architecture candidate)\n Effort M Risk Med Completeness 9/10\n Pros: warm entry right after a write; slightly higher hit rate on write-heavy keys\n Cons: couples the write result shape to the read DTO; same guard still required;\n hot data is read-heavy so gain is marginal\n Reuses: same as B\nRECOMMENDATION: B. Right-sized diff that meets the contract; C adds coupling for\nno measured benefit; A fails a stated invariant.\n```\nD1 auto-decided: B.\n\n**0F Mode.** HOLD SCOPE, set by the user for this run (D2, not asked). Fits the\n\"iteration on existing system, correctness-critical\" default. Committed: no\nexpansions surfaced; repairs needed to meet stated invariants are in scope.\n\n**0D HOLD SCOPE analysis.** Complexity: about 5 files (wrapper, single-flight\nwrapper, metrics/alerts config, wrapper tests, contract-suite harness). No new\nservices. Minimum change set equals the amended plan; nothing deferrable without\nbreaking an acceptance target or invariant.\n\n**0E Temporal interrogation (resolved now).**\n- Hour 1: where sentinel mapping lives (D3: adapter, not wrapper).\n- Hours 2-3: how a fill knows a write intervened (D4), and what a failed write\n does to the cache (D5).\n- Hours 4-5: flag bucket parity for reads and writes, and flush on percentage\n change (D9); metric label set without raw IDs (Section 8).\n- Hour 6+: ordering tests need a repository stub with explicit pause/release\n points and fake timers for TTL (Section 6).\nHuman ~6 h / CC ~40 min.\n\n### Decision registry (all auto-decided to the recommended option)\n\n| ID | Where | Decision | Chosen | Completeness | Why |\n|----|-------|----------|--------|--------------|-----|\n| D1 | 0C-bis | Implementation approach | B: in-flight invalidation | A 5, B 9, C 9 | Meets invariant, smallest correct diff |\n| D2 | 0F | Review mode | HOLD SCOPE | kind, not coverage | User-specified for this run |\n| D3 | 0E | Sentinel mapping location | Adapter maps null<->sentinel; wrapper never sees sentinel | kind, not coverage | Explicit over clever; one place owns the encoding |\n| D4 | S1/S4, F1 | Stale fill and late-joiner race | `entry.stale` token + detach on write | A 9 vs B (global write counter) 7 | Bounded memory, per-key precision, tests both orders |\n| D5 | S2, F2 | Cache after failed write | Invalidate in `finally`; test renamed | A 9 vs B (classify errors) 7 vs C (keep) 4 | Ambiguous commit is unknowable; one extra miss is cheap |\n| D6 | S6, F3 | Contract suite vs cached path | Run suite through warm wrapper | A 9 vs B (rely on unit tests) 6 | Helper coverage does not prove the caller path |\n| D7 | S7, F4 | p95 target diagnosability | Per-path latency histograms | A 9 vs B (single histogram) 6 | Hit floor alone does not imply p95 target |\n| D8 | S8, F5 | Silent bypass after rollout | Alerts + runbook | A 9 vs B (dashboard only) 5 | Zero silent failures |\n| D9 | S9, F6 | Stale entries across flag bucket changes | Flush on any flag change + writes always invalidate | A 9 vs B (flush only) 7 | Belt and braces, both cheap |\n| D10 | Next steps | Follow-up review | Run `/plan-eng-review` next (recommended; not executed) | kind | Required shipping gate |\n\nLake Score: 7/7 coverage-scored decisions (D1, D4-D9) chose the complete option.\n\n### Findings registry\n\n| ID | Sev | Evidence (plan text) | Remedy | Residual risk | Verification |\n|----|-----|----------------------|--------|---------------|--------------|\n| F1 | CRITICAL | Lines \"no additional version checks or coordination between a cache fill and a write\" vs invariant \"every read begun after that write completes must observe the committed version\". Schedule in Section 4 shows a pre-write DB snapshot filled after `cache.delete`, served up to 30 s; late joiner via single-flight gets same stale value. Same for absent sentinel (10 s). | D4 | Readers that began before the write may still see the old snapshot (permitted by contract) | Ordering tests, both orders + late joiner + sentinel variant |\n| F2 | WARNING | \"failed-write preservation\" test; a write that times out after the DB committed leaves a stale entry for up to 30 s | D5 | Failed writes cost one extra DB read | Test: rejected write leaves no entry; next read hits DB |\n| F3 | WARNING | \"Existing repository contract tests cover tenant isolation...\" run against raw repository only; cached hit path untested for tenant isolation and authorization | D6 | None material | Suite green with cache enabled, each case read twice |\n| F4 | WARNING | Targets: hits >= 60% and p95 < 60 ms. At the hit floor, 40% of reads are misses, so p95 is a miss latency; target depends on DB latency dropping under reduced load | D7 | Target may need DB p95 < 60 ms at <50% CPU; plan keeps targets unchanged | Per-path histograms visible at stage 1 |\n| F5 | WARNING | \"adapter bypasses the cache until an empty cache is reinitialized\" plus \"service owner monitors the rollout\": after rollout, bypass state has no alert and DB CPU drifts back to 70% silently | D8 | Alert threshold tuning | Alert fires in staging when adapter error is injected |\n| F6 | WARNING | \"enable for 10% of keys, then 50%\"; \"enabling creates an empty cache\" covers only off->on. Writes in a disabled bucket skip `cache.delete`; a later percentage change can re-enable a key with an entry that predates the write (within 30 s) | D9 | None material | Flag test: 50->10->50 within TTL after a write yields fresh value |\n\nLow, no change: within-tenant timing side channel (hit vs miss) reveals recent\nreads of a profile by the same tenant. Likelihood Low, impact Low; keys are\ntenant-scoped, so no cross-tenant signal.\n\n### Section outcomes\n\n1. **Architecture.** 1 issue: F1 (D4). Dependency graph and system diagram\n below. Coupling added: wrapper depends on single-flight entry state and flag;\n justified. Single point of failure: the one process (pre-existing). 10x load:\n 1000-entry cap thrashes first (eviction metric shows it; capacity changes are\n out of HOLD scope). Rollback: flag off, seconds; code revert not needed.\n2. **Error & rescue map.** 12 paths mapped, 1 GAP: F2 (D5). Registry below. No\n catch-all in the wrapper; the adapter's internal fallback must log the error\n class and increment `cache_fallback_errors_total` (existing behavior to verify).\n3. **Security.** 0 issues requiring a decision. No new endpoints, params, or\n secrets. Authorization stays upstream; cache key includes tenant. Cross-tenant\n key ambiguity and warm-path authorization are verified by F3/D6. Memory bounded\n by adapter caps. No raw IDs in logs or metric labels (asserted in tests).\n4. **Data flow & edge cases.** 10 edge cases mapped; 3 originally unhandled\n (F1, F2, F6), all remedied (D4, D5, D9). Async schedule below. No user-visible\n interactions (internal backend).\n5. **Code quality.** 0 new decisions. Wrapper has 3 branches; fits existing\n patterns. Immutable DTO contract is verified at the cache boundary with a\n mutation-isolation test (implementation of a stated contract, no approval\n needed). Undefined-as-miss requires D3.\n6. **Tests.** Test map below; 1 gap: F3 (D6). Pyramid: mostly unit, one contract\n suite integration, one trace replay. Flakiness: TTL via fake timers; ordering\n via explicit pause/release, no sleeps.\n7. **Performance.** 1 issue: F4 (D7). No N+1, no new queries or indexes. Memory\n max 16 MiB plus in-flight map bounded by concurrent misses. Slowest paths:\n miss (DB p95), bypass (DB p95), hit (<1 ms). No new connections.\n8. **Observability.** 1 gap: F5 (D8). Metrics: `cache_hits_total`,\n `cache_misses_total`, `cache_fill_skipped_total{reason=stale|oversize}`,\n `cache_evictions_total`, `cache_bytes`, `cache_mode` (enabled|bypass|disabled),\n `cache_fallback_errors_total`, `cache_flush_total{reason}`,\n `read_latency_seconds{path=hit|miss|bypass}`. Logs: structured, operation +\n error class, no keys. Runbook: bypass alert -> read adapter error log -> toggle\n flag off/on to reinitialize -> if it recurs within an hour, leave disabled and\n file a defect. Debuggability: a stale-read report 3 weeks later is reconstructed\n from `fill_skipped{stale}` and `flush` counters plus write timestamps.\n9. **Deployment.** 1 risk: F6 (D9). No migration. Flag evaluation failure\n defaults to bypass (existing SDK default, verify). Old/new code overlap: none\n (single process, flag-gated). Post-deploy checklist: first 5 min: `cache_mode`\n enabled, fallback errors 0, hits rising; first hour: hit ratio, eviction rate,\n DB CPU trend, per-path p95, error rate within SLO. Smoke: read twice, second is\n a hit; write then read observes new value.\n10. **Long-term trajectory.** Reversibility 5/5. Debt: 1 item, the in-flight\n token needs an ASCII comment in the single-flight wrapper and must be kept\n current. No path dependency; the repository seam is preserved. 1-year\n readability: good once the ordering rule is in the code comment.\n11. **Design & UX.** SKIPPED, justified: no UI, API, or user-visible change.\n\n### Diagrams\n\nSystem architecture (new parts marked `*`):\n```\n caller \u2500\u2500\u25b6 authz \u2500\u2500\u25b6 readProfile*/writeProfile* \u2500\u2500\u25b6 repository \u2500\u2500\u25b6 DB\n \u2502 \u2502 \u2502\n \u2502 \u2502 \u2514\u2500\u2500\u25b6 flag (bucket per key)\n \u2502 \u2514\u2500\u2500\u25b6 inflight* (per-key entry {promise, stale})\n \u2514\u2500\u2500\u25b6 LRU adapter (1000 / 16 MiB / 30 s, sentinel 10 s)\n \u2514\u2500\u2500\u25b6 metrics/logs (no raw IDs)\n Before: caller \u2500\u25b6 authz \u2500\u25b6 repository \u2500\u25b6 DB\n After: caller \u2500\u25b6 authz \u2500\u25b6 wrapper \u2500\u25b6 {adapter, inflight, flag} \u2500\u25b6 repository \u2500\u25b6 DB\n```\n\nData flow with shadow paths (read):\n```\n key \u2500\u2500\u25b6 flag? \u2500\u2500\u25b6 cache.get \u2500\u2500\u25b6 inflight.run \u2500\u2500\u25b6 repository.read \u2500\u2500\u25b6 fill? \u2500\u2500\u25b6 value\n \u2502 \u2502 \u2502 \u2502 \u2502 \u2502\n nil/bad: eval err: throws: joins existing: typed error: entry.stale:\n rejected bypass + adapter same promise propagate, no skip fill,\n upstream log bypass + (same gen only) fill, release return value\n (existing) metric entry\n empty result (absent): null \u2500\u2500\u25b6 adapter stores sentinel (10 s) \u2500\u2500\u25b6 get returns null\n oversize value: adapter skips set, metric reason=oversize, cache stays enabled\n```\n\nAsync ordering schedule (F1, invariant boundary = `writeProfile` settles):\n```\n t | R1 (began before W) | W | cache[k] | inflight[k]\n 1 | get->undef; run(); DB read sent | | - | E1 stale=F\n 2 | | DB write commits V2 | - | E1\n 3 | DB returns V1 (resume queued) | | - | E1\n 4 | | resume: invalidate(E1), delete | - | - (E1 detached)\n 5 | | settles -> W complete | - | -\n 6 | resume: E1.stale -> skip fill | | - | -\n | return V1 (allowed: began < 5) | | |\n 7 | R2 begins: miss, new E2, DB->V2, fill V2 | V2 OK | E2\n Order B (6 before 4): R1 fills V1, then W deletes at 4 -> R2 misses -> V2 OK\n Late joiner R3 arriving after 5: E1 detached -> new entry -> V2 OK\n Original sketch, order A: fill V1 at 6 after delete at 4 -> R2 reads V1 for <=30 s VIOLATION\n Original sketch, R3 after 5: joins E1 -> V1 VIOLATION\n```\n\nCache mode state machine:\n```\n DISABLED \u2500\u2500flag on (empty cache)\u2500\u2500\u25b6 ENABLED \u2500\u2500adapter failure\u2500\u2500\u25b6 BYPASS\n \u25b2 \u2502 \u25b2 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500flag off\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500flag toggle (reinit)\u2500\u2500\u2500\u2500\u2518\n Any flag value change (incl. %) while ENABLED: flush -> ENABLED (empty)\n Invalid: BYPASS -> ENABLED without reinit (prevented: adapter only re-enables on init)\n```\n\nError flow:\n```\n repository error \u2500\u2500\u25b6 typed API error (existing) \u2500\u2500\u25b6 caller; no fill; entry released\n adapter error \u2500\u2500\u25b6 BYPASS + fallback metric + log(class) \u2500\u2500\u25b6 caller unaffected \u2500\u2500\u25b6 alert >5 min\n flag eval error \u2500\u2500\u25b6 bypass for that call + log \u2500\u2500\u25b6 caller unaffected\n write error \u2500\u2500\u25b6 finally: invalidate + delete \u2500\u2500\u25b6 typed error to caller\n```\n\nDeployment sequence:\n```\n deploy code (flag off) \u2500\u25b6 smoke (bypass path) \u2500\u25b6 10% keys \u2500\u25b6 1 h healthy \u2500\u25b6 50% \u2500\u25b6 1 h \u2500\u25b6 100%\n healthy = error rate in SLO, fallback errors 0, per-path p95 not worse, hits rising\n```\n\nRollback flowchart:\n```\n regression? \u2500\u2500yes\u2500\u2500\u25b6 flag off (seconds) \u2500\u2500\u25b6 cache dropped \u2500\u2500\u25b6 verify error rate/p95 \u2500\u2500\u25b6 file defect\n \u2502no \u2502 still bad\n \u25bc \u25bc\n continue stage revert deploy (not cache-related)\n```\n\nTest map:\n```\n NEW UX FLOWS: none\n NEW DATA FLOWS: miss fill; negative fill; write invalidation; in-flight\n invalidation; flag bucket routing; flag-change flush\n NEW CODEPATHS: hit; miss+fill; miss+skip(stale); join in-flight; oversize skip;\n adapter bypass; flag-disabled bypass\n NEW ASYNC WORK: in-flight entry lifecycle (create, join, invalidate, release)\n NEW INTEGRATIONS: none (flag SDK existing)\n NEW ERROR/RESCUE PATHS: see Error & Rescue Registry\n```\nAssertions (unit unless noted): hit returns cached value without DB call; miss\ncalls DB once and fills; TTL expiry misses at 30 s (fake timers); 1001st entry\nevicts one; byte cap evicts; oversize single value is not stored and mode stays\nenabled; absent read stores sentinel, second read makes no DB call, expires at\n10 s; write after absent clears sentinel; successful write leaves no entry;\nrejected write leaves no entry (D5); ordering order A and order B both yield V2\nfor a post-write reader (D4); late joiner after write gets fresh read (D4);\nsentinel variant of order A (D4); N concurrent misses make 1 DB call; adapter\n`get` throw enters bypass, increments fallback metric, caller gets value;\nadapter `delete` throw does not fail the write; read and write map a key to the\nsame bucket; disabled bucket makes no cache calls; percentage change flushes\n(D9); flag evaluation error bypasses; mutating a returned DTO does not change\nthe next read; contract suite passes through warm wrapper (D6, integration);\nmetric labels and log lines match no ID pattern; trace replay of 900 hot keys\nreaches >= 60% hits with per-path histograms (D7, load).\nFriday-2am test: order A ordering test. Hostile QA: late joiner after write.\nChaos: adapter throws on every 3rd call, service stays correct in bypass.\n\n### Error & Rescue Registry\n\n| Method | What can go wrong | Class | Rescued? | Action | User sees |\n|--------|-------------------|-------|----------|--------|-----------|\n| readProfile | flag evaluation fails | flag SDK error (existing) | Y | bypass this call, log | normal latency |\n| readProfile | `cache.get` throws | adapter failure (existing) | Y | BYPASS mode, metric, log class, alert (D8) | nothing |\n| readProfile | repository.read fails | existing typed errors | Y (existing) | propagate, no fill, release entry | existing API error |\n| readProfile | fill after write committed | none (ordering) | Y (D4) | skip fill | fresh value on next read |\n| readProfile | late joiner on stale entry | none (ordering) | Y (D4) | entry detached, fresh read | fresh value |\n| readProfile | value exceeds byte cap | adapter oversize (existing) | Y | skip set, metric reason=oversize | nothing |\n| readProfile | `cache.set` throws | adapter failure | Y | BYPASS + metric + alert | nothing |\n| readProfile | absent record | null result | Y | sentinel 10 s (D3) | existing not-found |\n| writeProfile | repository.write rejects | existing typed errors | Y (D5) | invalidate + delete in finally | existing API error |\n| writeProfile | write times out after commit | existing timeout error | Y (D5) | same as above; next read hits DB | existing API error |\n| writeProfile | `cache.delete` throws | adapter failure | Y | BYPASS + metric; write still succeeds | nothing |\n| flag change | percentage/on/off change | none | Y (D9) | flush, `cache_flush_total` | brief miss burst |\n| reinit | adapter init fails on enable | adapter failure | Y | stay BYPASS + alert | nothing |\n\nCritical gaps after amendments: 0.\n\n### Failure Modes Registry\n\n```\n CODEPATH | FAILURE MODE | RESCUED? | TEST? | USER SEES? | LOGGED?\n --------------------|---------------------------------|----------|-------|-----------------|--------\n readProfile fill | stale fill after write (F1) | Y (D4) | Y | fresh value | metric\n readProfile join | late joiner stale (F1) | Y (D4) | Y | fresh value | metric\n readProfile | adapter get/set throws | Y | Y | nothing | Y + alert\n readProfile | repository error | Y | Y | typed error | Y (existing)\n readProfile | oversize value | Y | Y | nothing | metric\n readProfile | flag eval error | Y | Y | nothing | Y\n writeProfile | failed/ambiguous write (F2) | Y (D5) | Y | typed error | Y\n writeProfile | delete throws | Y | Y | nothing | Y + alert\n flag transition | stale entry across bucket (F6) | Y (D9) | Y | fresh value | metric\n post-rollout | silent bypass (F5) | Y (D8) | Y | nothing | alert\n acceptance | p95 miss-dominated (F4) | Y (D7) | Y | n/a | histogram\n capacity | hot set > 1000 thrash | n/a | Y | slower reads | eviction metric\n```\nCRITICAL GAPS: 0 (3 before amendments: F1 x2 rows, F6).\n\n### NOT in scope\n- Distributed or cross-process cache: plan constraint; single process only.\n- Prewarming: cold start fits existing DB capacity.\n- Consistency semantics change: invariant retained as written.\n- Capacity increase beyond 1000 entries / 16 MiB: hypothetical load, not evidenced.\n- Auto-recovery from BYPASS without a flag toggle: alert plus runbook is adequate.\n- Write-through (Approach C): marginal benefit, adds coupling.\n\n### What already exists\nLRU adapter (limits, TTL, sentinel, bypass) reused as-is; single-flight wrapper\nreused with one added method; runtime flag reused; repository contract tests\nreused and extended to the warm path; typed error mapping unchanged.\n\n### Dream state delta\nAfter this plan the service has a flag-gated, invariant-safe local cache behind\nthe unchanged repository interface. Remaining distance to the 12-month ideal is\nonly a swappable cache implementation, which the seam already permits.\n\n### TODOS.md updates\n0 items proposed. Every evidenced gap is repaired in scope; remaining candidates\nwere expansions or alternatives to adequate remedies and are not surfaced in\nHOLD SCOPE.\n\n### Stale diagram audit\nCodebase exploration was skipped, so existing diagrams in touched files were\nnot enumerated. Implementer must check the single-flight wrapper and adapter\ncomments and add the ordering-rule diagram (Section 10 debt item).\n\n### Outside voice\nCodex review skipped (codex_reviews disabled). Re-enable:\n`gstack-config set codex_reviews enabled`. No native fallback dispatched.\noutside_status: disabled. The `gstack-review-log` persistence command, the\nplan-ceo-review review-log entry, the decision-log entry, and the tasks JSONL\nartifact were not run: shell commands were excluded from this run.\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 single-flight wrapper + readProfile \u2014 Add `invalidate(key)` with `entry.stale` and detach; skip fill when stale\n - Surfaced by: Section 1/4 \u2014 F1, D4\n - Files: single-flight wrapper module, profile repository wrapper\n - Verify: ordering tests order A, order B, late joiner, sentinel variant\n- [ ] **T2 (P1, human: ~1h / CC: ~10min)** \u2014 writeProfile \u2014 Move invalidation into `finally`; rename test to failed-write invalidation\n - Surfaced by: Section 2 \u2014 F2, D5\n - Files: profile repository wrapper, wrapper tests\n - Verify: rejected write leaves no entry; next read hits DB\n- [ ] **T3 (P1, human: ~1h / CC: ~10min)** \u2014 flag routing \u2014 Shared bucket function for reads and writes; always delete on write; flush on any flag value change\n - Surfaced by: Section 9 \u2014 F6, D9\n - Files: profile repository wrapper, flag subscription hook\n - Verify: 50->10->50 within TTL after write yields fresh value; parity test\n- [ ] **T4 (P1, human: ~1h / CC: ~10min)** \u2014 adapter \u2014 Confirm `null`<->sentinel mapping in adapter; wrapper treats only `undefined` as miss\n - Surfaced by: 0E \u2014 D3\n - Files: LRU adapter, wrapper tests\n - Verify: absent read twice makes one DB call; sentinel expires at 10 s\n- [ ] **T5 (P1, human: ~2h / CC: ~15min)** \u2014 tests \u2014 Run repository contract suite through the warm cache-enabled wrapper; add DTO mutation-isolation and oversize-skip assertions\n - Surfaced by: Section 6 \u2014 F3, D6; Section 5\n - Files: contract test harness, wrapper tests\n - Verify: suite green with cache enabled, each case read twice\n- [ ] **T6 (P1, human: ~2h / CC: ~15min)** \u2014 observability \u2014 Emit metric set from Section 8 with `path` and `reason` labels; assert no raw IDs\n - Surfaced by: Section 7/8 \u2014 F4, D7\n - Files: wrapper metrics, log format tests\n - Verify: histogram labels present in staging; ID-regex assertion passes\n- [ ] **T7 (P2, human: ~2h / CC: ~15min)** \u2014 alerting \u2014 Bypass/fallback >5 min alert; hit ratio <40% for 15 min post-warmup alert; runbook entry\n - Surfaced by: Section 8 \u2014 F5, D8\n - Files: alert rules, runbook doc\n - Verify: inject adapter error in staging, alert fires\n- [ ] **T8 (P2, human: ~30min / CC: ~5min)** \u2014 docs \u2014 ASCII ordering-rule diagram in single-flight wrapper comment\n - Surfaced by: Section 10 \u2014 debt item\n - Files: single-flight wrapper module\n - Verify: diagram matches T1 behavior\n\n_No new tasks from Section 3 (Security) or Section 11 (Design)._\n\n### Completion Summary\n```\n +====================================================================+\n | MEGA PLAN REVIEW \u2014 COMPLETION SUMMARY |\n +====================================================================+\n | Mode selected | HOLD SCOPE (user-specified) |\n | System Audit | SKIPPED by run rules |\n | Step 0 | Approach B (D1); HOLD; D3 sentinel in adapter|\n | Section 1 (Arch) | 1 issue found (F1) |\n | Section 2 (Errors) | 12 error paths mapped, 1 GAP (F2, resolved) |\n | Section 3 (Security)| 0 issues found, 0 High severity |\n | Section 4 (Data/UX) | 10 edge cases mapped, 0 unhandled (3 fixed) |\n | Section 5 (Quality) | 0 issues found |\n | Section 6 (Tests) | Diagram produced, 1 gap (F3) |\n | Section 7 (Perf) | 1 issue found (F4) |\n | Section 8 (Observ) | 1 gap found (F5) |\n | Section 9 (Deploy) | 1 risk flagged (F6) |\n | Section 10 (Future) | Reversibility: 5/5, debt items: 1 |\n | Section 11 (Design) | SKIPPED (no UI scope) |\n +--------------------------------------------------------------------+\n | NOT in scope | written (6 items) |\n | What already exists | written |\n | Dream state delta | written |\n | Error/rescue registry| 2 methods + flag/reinit, 0 CRITICAL GAPS |\n | Failure modes | 12 total, 0 CRITICAL GAPS |\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 (config) |\n | Lake Score | 7/7 recommendations chose complete option |\n | Diagrams produced | 8 (arch, data flow, async schedule, state, |\n | | error flow, deploy, rollback, test map) |\n | Stale diagrams found | 0 (audit limited: no codebase exploration) |\n | Unresolved decisions | 0 |\n +====================================================================+\n```\n\n### Unresolved decisions\nNone. All ten decision points were auto-decided to the recommended option under\nthis run's headless rules and are recorded in the decision registry.\n\n### Next steps\nRun `/plan-eng-review` next (required shipping gate; D10). No design review\nneeded: no UI scope.\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, 0 critical gaps, 6 findings remedied (D4-D9) |\n| Outside Review | codex via `/plan-ceo-review` outside voice | Independent 2nd opinion | 0 | disabled | codex_reviews: disabled in config; no fallback run |\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, outside_status disabled (config `codex_reviews: disabled`), findings none. Review-log persistence not run this session (shell excluded).\n- **VERDICT:** CEO CLEARED \u2014 eng review required\n\nNO UNRESOLVED DECISIONS\n", "reportSha256": "b6327f0dac756ce1829328904251ebed5e42af6f0d2b6e07cd40f0966ee9f249", "finding": "| F1 | CRITICAL | Lines \"no additional version checks or coordination between a cache fill and a write\" vs invariant \"every read begun after that write completes must observe the committed version\". Schedule in Section 4 shows a pre-write DB snapshot filled after `cache.delete`, served up to 30 s; late joiner via single-flight gets same stale value. Same for absent sentinel (10 s). | D4 | Readers that began before the write may still see the old snapshot (permitted by contract) | Ordering tests, both orders + late joiner + sentinel variant |", "heading": "Async ordering schedule (F1, invariant boundary = `writeProfile` settles):", "trace": " t | R1 (began before W) | W | cache[k] | inflight[k]\n 1 | get->undef; run(); DB read sent | | - | E1 stale=F\n 2 | | DB write commits V2 | - | E1\n 3 | DB returns V1 (resume queued) | | - | E1\n 4 | | resume: invalidate(E1), delete | - | - (E1 detached)\n 5 | | settles -> W complete | - | -\n 6 | resume: E1.stale -> skip fill | | - | -\n | return V1 (allowed: began < 5) | | |\n 7 | R2 begins: miss, new E2, DB->V2, fill V2 | V2 OK | E2\n Order B (6 before 4): R1 fills V1, then W deletes at 4 -> R2 misses -> V2 OK\n Late joiner R3 arriving after 5: E1 detached -> new entry -> V2 OK\n Original sketch, order A: fill V1 at 6 after delete at 4 -> R2 reads V1 for <=30 s VIOLATION\n Original sketch, R3 after 5: joins E1 -> V1 VIOLATION", "retry": { "report": "# Plan: cache profile summaries in one process\n\n> Reviewed by `/plan-ceo-review` on 2026-09-10 (branch `main`, mode HOLD SCOPE).\n> Plan content below is the original plan with accepted amendments marked `[F]`.\n> The review record follows the plan. Findings are defined once in the Findings\n> Registry and cross-referenced everywhere else.\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 single-flight wrapper coalesces simultaneous\n 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)\nKeep the current read-through repository interface and shared adapters. The\noriginal sketch (invalidate-on-write only, \"no additional version checks or\ncoordination between a cache fill and a write\") violates the read-after-write\ninvariant above; see [F1]. The accepted ordering rules are:\n\n```javascript\n// Generation guard [F1]: a fill lands only if no write to the key completed\n// between this read's cache miss and its DB result. Stamps come from one\n// process-wide monotonic clock, so a stamp never repeats after eviction or\n// reinit; a lost stamp can only cause a harmless skipped fill, never a stale one.\nlet writeClock = 0; // never reset\nlet writeGen = new BoundedMap(10_000); // key -> stamp of last completed write attempt\nconst stamp = (key) => writeGen.get(key) ?? 0;\n\nasync function readProfile(key) {\n if (!inCohort(key)) return repository.read(key); // [F3] shared deterministic cohort\n const cached = cache.get(key);\n if (cached !== undefined) return fromCached(cached); // [F2] ABSENT -> same absent result as repository\n const seen = stamp(key);\n const value = await singleFlight(`${key}#${seen}`, // [F1] post-write readers never join a pre-commit flight\n () => repository.read(key));\n if (stamp(key) === seen) cache.set(key, toCached(value)); // [F2] absent -> ABSENT sentinel, 10 s TTL (adapter)\n else metrics.increment('profile_cache.fill_skipped_stale'); // [F1] skipped fill is visible, never silent\n return value;\n}\n\nasync function writeProfile(key, update) {\n let saved;\n try {\n saved = await repository.write(key, update);\n } finally {\n writeGen.set(key, ++writeClock); // [F1] bump on success AND failure (a timed-out write may have committed)\n }\n cache.delete(key); // success only: failed-write preservation retained\n return saved;\n}\n\n// [F3] Any runtime flag change (percentage up, down, or off) reinitializes an\n// empty cache and clears writeGen; writeClock is kept. Logged without keys.\nflag.onChange((next) => { cache = adapter.createEmpty(); writeGen.clear();\n log.info('profile_cache.reinit', { reason: 'flag_change', cohort_pct: next.pct }); });\n```\n\n`toCached`/`fromCached` map the repository's absence signal to the adapter's\nABSENT sentinel and back, so a cached absence produces exactly the same caller\noutcome as an uncached one [F2]. `inCohort(key)` is one deterministic hash\nfunction used by both `readProfile` and `writeProfile` [F3].\n\n## Verification and rollout (amended)\nExisting repository contract tests cover tenant isolation, key validation,\nabsence, DB failures, and authorization. That same suite also runs against the\ncaching wrapper with the flag enabled, once cold and once pre-warmed [F6].\nNew wrapper tests cover hit/miss, eviction and byte limits, TTL, adapter-failure\nfallback, successful-write invalidation, failed-write preservation,\nconcurrent-miss coalescing, controlled-schedule read/write interleavings [F1],\nabsent-sentinel behavior [F2], cohort membership and flag-change reinit [F3],\nand cached DTO immutability [F5].\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, fill_skipped_stale, DB CPU, and read p95\nwithout raw IDs. Two alerts and a runbook ship with the change [F4].\nOn error-rate or latency regression, disable the flag immediately; both reads\nand writes bypass the cache while disabled, and any flag change creates an\nempty cache [F3]. Cold starts remain within the existing DB capacity. The\nservice owner monitors the rollout and records the results against the\nacceptance 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## Accepted plan amendments\n| Ref | Amendment | Decision |\n|-----|-----------|----------|\n| F1 | Per-key write-generation guard, stamped single-flight key, skipped-fill metric, schedule tests | Auto-accepted (recommended) |\n| F2 | Explicit absence-to-sentinel mapping in wrapper plus tests | Auto-accepted (recommended) |\n| F3 | Shared deterministic cohort function; reinit empty cache on any flag change | Auto-accepted (recommended) |\n| F4 | Two alerts, one runbook, reinit/bypass log lines | Auto-accepted (recommended) |\n| F5 | Wrapper test asserting cached DTOs are deep-frozen | Auto-accepted (recommended) |\n| F6 | Repository contract suite runs against the wrapper, cold and warm | Auto-accepted (recommended) |\n\nNo stated requirement, invariant, or non-goal was changed. \"Failed-write\npreservation\" is retained as written; its residual risk is recorded under F1.\n\n---\n\n# CEO Review Record\n\n## Run controls\n- Mode: HOLD SCOPE (set by the user; no expansion surfaced, no reduction proposed).\n- Session: automated, no human present. Every decision point auto-selected the recommended option and is recorded here. Destructive options: none offered.\n- Skipped by instruction: system audit, environment setup, telemetry, codebase exploration, git and all mutating shell commands. Consequence: review-log, decision-log, and the tasks JSONL artifact were not written; no shell was available in this run.\n- Outside voice: `codex_reviews: disabled` in `.gstack-section-state-IyOF8a/config.yaml`. Extra outside-review step skipped in full, no native fallback dispatched. Outside coverage: disabled. The persistence command for the disabled record could not run (no shell).\n\n## Step 0\n**0A Premise.** Real, measured pain (70% DB CPU, p95 120 ms, ~900 hot keys over one week). A process-local read-through cache is the most direct path; the single active process and no external writers make local coherence sufficient. Doing nothing leaves DB headroom at 30% with no cheaper lever. Not a proxy problem.\n\n**0B Existing leverage.** Reused: LRU adapter (caps, TTL, sentinel, bypass-on-failure), per-key single-flight wrapper, runtime feature flag, repository contract tests, typed error mapping. Nothing is rebuilt. New: the generation guard [F1] and cohort function [F3], neither of which exists today.\n\n**0C Dream state.**\n```\n CURRENT STATE THIS PLAN 12-MONTH IDEAL\n every read hits DB ---> read-through LRU in-process ---> same repository interface,\n DB CPU 70%, p95 120ms coherent under RAW invariant swappable coherent cache tier\n no cache metrics hit/miss/skip metrics, alerts if a second process ever ships\n```\nMoves toward the ideal: interface unchanged, coherence rule made explicit and tested.\n\n**0C-bis Approaches (D1, auto-chose B).**\n```\nAPPROACH A: Sketch as written (invalidate-on-write only) Effort S Risk High Completeness 4/10\n Pros: smallest diff; reuses adapter unchanged\n Cons: violates read-after-write invariant (schedule in Section 4); relies on TTL, which the plan forbids\nAPPROACH B: Sketch + per-key write-generation guard Effort S Risk Low Completeness 9/10 (recommended)\n Pros: meets invariant with ~15 lines; no new dependency; failure of the guard degrades to a miss, never a stale hit\n Cons: bespoke mechanism needs a comment diagram and schedule tests\nAPPROACH C: Write-through with versioned DTOs Effort M Risk Med Completeness 8/10\n Pros: warm after write; natural path to a distributed tier\n Cons: still needs a version check to beat the late-fill race; adds DTO versioning outside accepted scope\nRECOMMENDATION: B. It is the smallest diff that meets the stated invariant (explicit over clever, complete over shortcut).\n```\n\n**0F Mode.** HOLD SCOPE, user-specified; no question fired.\n\n**0D HOLD analysis.** Touches 1 wrapper module, 1 test file, alert/runbook config. No new services. Minimum change set equals accepted scope; repairs needed to meet stated invariants (F1, F2, F3) are in scope by rule.\n\n**0E Temporal interrogation.** Resolved now, not later: where `writeGen` lives and its bound (wrapper module, 10k entries); how the repository signals absence (null vs typed not-found error, decided in Hour 1 and encoded in `toCached`/`fromCached`); cohort hashing shared by both paths; metric and log names listed under F4. With CC + gstack this is roughly 30 to 45 minutes of implementation against a human-team day.\n\n## Findings Registry\nEach finding is recorded once. Sections and tasks reference these IDs.\n\n| ID | Sev | Failure mode and evidence | Selected remedy | Residual risk | Verification |\n|----|-----|---------------------------|-----------------|---------------|--------------|\n| F1 | P1 CRITICAL | Late fill after write. Read misses, DB returns v1, write commits v2 and deletes (no-op), read then fills v1; every later read gets v1 until TTL. Plan lines 27-29 forbid this; sketch lines 36-49 and 32-34 explicitly add no coordination. Second path: a post-write reader joins the pre-commit single-flight. | Per-key write stamp from a process-wide clock, bumped in `finally` of `writeProfile`; fill only if stamp unchanged; single-flight key `${key}#${stamp}`; `fill_skipped_stale` metric; comment diagram in module. | A write that times out after committing keeps the old cached value up to 30 s (retained \"failed-write preservation\" contract). Bounded `writeGen` eviction causes only extra misses. | Controlled pause/release tests for schedules S1, S2, S3 (Section 4); assert post-write read observes v2 and metric increments exactly once for S1. |\n| F2 | P1 | Absence path unspecified in wrapper. Plan line 20-21 promises an ABSENT sentinel with 10 s TTL, but the sketch sets the raw value; if `repository.read` returns `undefined` or throws, absence is never cached and hot absent keys always hit the DB; wrapper tests (line 54-56) omit absence. | `toCached`/`fromCached` map the repository's absence signal to the adapter sentinel and back; if absence is a typed not-found error, catch only that class, set sentinel, rethrow. | None beyond adapter TTL semantics. | Tests: two absent reads within 10 s make one DB call; cached absence yields the identical caller outcome (same null or same error class); create-after-absent write deletes the sentinel. |\n| F3 | P1 | Cohort changes can resurrect stale entries. Plan line 60-61 empties the cache only on disable to enable; stepping 50% to 10% leaves entries for de-enrolled keys, whose writes now bypass `cache.delete`; re-enrolling serves them stale. Also, per-request random sampling instead of per-key hashing would let reads fill while writes skip invalidation. | One deterministic `inCohort(key)` used by both paths; any flag change reinitializes an empty cache and clears `writeGen`; reinit logged with reason and percentage. | Brief miss burst after each stage change; within existing DB capacity per plan (cold-start statement). | Tests: same key gives same cohort answer for read and write; 50% to 10% to 50% sequence never returns a pre-write value; flag change emits reinit log and resets hit/miss counters. |\n| F4 | P2 | No alert or runbook defined; plan line 62 relies on the owner watching dashboards. Silent degradation possible if the adapter enters bypass at 3 a.m. | Alerts: (a) `fallback_errors` > 0 for 5 min, (b) hit rate < 60% for 15 min after 100% stage. Runbook: check reinit/bypass log, disable flag, confirm DB CPU, file follow-up. Structured logs at reinit and bypass entry/exit, no keys or IDs. | Alert thresholds may need tuning after first week. | Alert rules deployed with the change; runbook linked from the alert; a test asserts the bypass-entry log fires on injected adapter failure. |\n| F5 | P2 | Cache hands one object reference to all callers; a single mutation by any caller becomes cross-request corruption. Plan line 17 asserts immutability but nothing verifies it at the wrapper boundary. | One wrapper test asserting the cached DTO and nested objects are frozen; no runtime change. | If DTOs are not actually frozen, the test fails and forces the decision before rollout. | Test in the wrapper suite. |\n| F6 | P1 | Contract suite scope ambiguous. Plan line 53-54 runs tenant-isolation and authorization tests against the repository; unclear whether the caching wrapper is under test, so a cross-tenant cache hit would go untested. | Run the existing repository contract suite against the wrapper with the flag on, cold and pre-warmed. | None. | CI job matrix entry; suite passes in both states. |\n\n## Section outcomes\n1. **Architecture:** 1 finding (F1). Boundaries unchanged; new coupling is wrapper to flag and to generation map, justified. Under 10x load the byte cap evicts first; under 100x single-flight bounds DB fan-out. Single point of failure: the one process, unchanged. Rollback: flag off, seconds. Diagrams in Diagrams 1.\n2. **Error and rescue map:** 9 paths mapped, 1 gap (F2), closed. No catch-all handlers; all rescues are typed. Registry below.\n3. **Security and threat model:** 1 finding (F6), 0 High severity. No new endpoints, params, secrets, or dependencies. Cross-tenant leak: likelihood Low (tenant in key), impact High, mitigated by key design and F6 verification. Keys and IDs excluded from logs and metrics.\n4. **Data flow and interaction edge cases:** 10 edge cases mapped, 2 unhandled in original (F1 schedules, F3 cohort), both closed. No user-visible interaction. Diagrams 2 and 3.\n5. **Code quality:** 1 finding (F5); DRY note folded into F3 (one cohort function). Naming explicit; no method exceeds 5 branches; no premature abstraction (generation guard solves a demonstrated race, not a hypothetical one).\n6. **Test review:** diagram produced (Diagrams 7), 4 gaps, all mapped to T1, T2, T3, T4, T6. Pyramid is unit-heavy with one integration matrix run. Flakiness: schedule tests use explicit pause/release hooks, not timers. Load test: hit-rate soak during the 10% stage stands in for a synthetic load run.\n7. **Performance:** no issues. Byte sizing per fill is bounded by fill rate (at most 40% of reads at target hit rate). `writeGen` bounded at 10k entries, roughly 1 MB worst case. No new connections. Slowest new path is a miss: unchanged DB latency plus microseconds.\n8. **Observability:** 1 gap (F4), closed. Metrics: hit, miss, eviction, bytes, fallback_errors, fill_skipped_stale, reinit count. Logs at reinit and bypass transitions. Three-week-later debuggability: reinit and bypass logs plus stage timestamps reconstruct cache state.\n9. **Deployment and rollout:** 1 risk (F3), closed. No migration. Flag-gated, staged, one healthy hour per stage. Deploy-time window: old code has no cache, new code disabled by default, so mixed states are safe. Post-deploy checks in Diagrams 5.\n10. **Long-term trajectory:** reversibility 5/5 (flag plus one module). Debt items: 1 (bespoke generation guard), paid down by the module comment diagram and schedule tests in T1. Interface preserved for a future cache tier; no path dependency added.\n11. **Design and UX:** SKIPPED, justified. Internal backend change with no UI, API, or user-visible surface (plan line 8-9).\n\n## Diagrams\n**1. System architecture (after)**\n```\n caller --> auth/authz --> readProfile/writeProfile (wrapper)\n | | | |\n v v v v\n inCohort LRU adapter writeGen singleFlight(key#stamp)\n (flag) (1000 / 16MiB / (10k, clock) |\n 30s, ABSENT 10s) v\n repository ---> DB (single process, no external writers)\n flag.onChange ---> reinit empty cache + clear writeGen ---> log reinit\n```\n\n**2. Data flow with shadow paths (read)**\n```\n key \u2500\u2500\u25b6 inCohort? \u2500\u2500\u25b6 cache.get \u2500\u2500\u25b6 [miss] stamp \u2500\u2500\u25b6 singleFlight(DB read) \u2500\u2500\u25b6 stamp equal? \u2500\u2500\u25b6 set \u2500\u2500\u25b6 return\n \u2502 \u2502 \u2502 \u2502 \u2502\n \u25bc \u25bc \u25bc \u25bc \u25bc\n nil/invalid: no: DB hit: return value DB error: typed error, changed: skip fill,\n rejected by direct or absent result [F2] no fill, flight released metric++, return value\n existing key (bypass) [adapter fault: bypass, (existing API mapping) (allowed to this caller)\n validation fallback_errors++]\n empty DTO: valid value, cached as-is (byte cap enforces size)\n```\n\n**3. Async ordering schedules (shared state: cache[key], writeGen[key])**\n```\n S1 late fill R1 (read, began before W) W (write) R2 (read, began after W) cache gen\n 1 get->miss, seen=0 - 0\n 2 DB SELECT -> v1\n 3 DB UPDATE commits v2\n 4 sketch delete (no-op), return - 0\n 5 sketch promise resolves, set(v1) v1\n 6 sketch get -> v1 VIOLATION\n 4'guarded gen=1, delete, return - 1\n 5'guarded stamp 1 != seen 0: skip fill, metric++, return v1 (allowed) - 1\n 6'guarded miss, seen=1, flight key#1 -> v2, set v2\n S2 join stale flight R2 misses while R1's flight is open: sketch coalesces R2 onto key (v1) VIOLATION;\n guarded uses key#1, a new flight issued after commit -> v2.\n S3 read begins mid-write R3 seen=0 issues SELECT before commit -> v1; W bumps gen=1 after commit; R3 skips fill.\n Mechanism excluding violating orders: bump happens synchronously after the write settles and before any\n later read can capture a stamp; a fill requires stamp equality; flights are stamp-scoped.\n```\n\n**4. State machine (cache entry per key)**\n```\n [MISS] --fill(value, stamp ok)--> [PRESENT] --TTL 30s / evict / write success--> [MISS]\n [MISS] --fill(absent, stamp ok)--> [ABSENT] --TTL 10s / evict / write success--> [MISS]\n [MISS] --fill(stamp changed)-----> [MISS] (metric fill_skipped_stale)\n any --adapter failure---------> [BYPASS: get=undefined, set/delete no-op] --reinit--> [MISS]\n any --flag change-------------> [MISS] (whole cache reinitialized)\n Impossible: PRESENT -> PRESENT with older value (prevented by stamp check); ABSENT after a successful\n create (prevented by delete on write success).\n```\n\n**5. Error flow**\n```\n repository.read error ---> singleFlight releases ---> no fill ---> existing typed error to caller\n repository.write error --> finally: gen bump ---> cache retained ---> existing typed error to caller\n adapter fault ---------> bypass mode + fallback_errors++ + log ---> alert (a) after 5 min\n invalid key -----------> existing validation error before cache touch\n multi-process config + caching on ---> existing startup ConfigurationError, process refuses start\n```\n\n**6. Deployment sequence and rollback**\n```\n deploy (flag off) -> smoke: read/write path unchanged, metrics registered\n -> 10% keys, 1 healthy hour -> 50%, 1 healthy hour -> 100%\n checks each stage (first 5 min): fill_skipped_stale small and nonzero under writes, fallback_errors 0,\n error rate flat; (first hour): hit rate rising toward 60%, DB CPU falling, p95 falling\n ROLLBACK: regression seen? --yes--> flag off (seconds) --> cache empty, reads/writes bypass\n wrapper itself faulting? --yes--> revert deploy; no data migration to undo\n```\n\n**7. Test diagram**\n```\n NEW UX FLOWS: none\n NEW DATA FLOWS: read-through fill; stamped fill skip; absent sentinel; write invalidate + bump; flag reinit\n NEW CODEPATHS: inCohort branch; hit/miss; stamp equal/changed; ABSENT/present decode; finally bump; onChange reinit\n NEW ASYNC WORK: stamp-scoped single-flight\n NEW INTEGRATIONS: none (same repository, same DB)\n NEW ERROR PATHS: adapter fault -> bypass; DB read error -> release; DB write error -> preserve + bump\n Coverage: unit (all above); integration (contract suite vs wrapper cold and warm [F6]); soak (10% stage).\n 2 a.m. Friday test: S1 schedule asserting v2 after write. Hostile QA: S2 join-stale-flight and 50->10->50 cohort.\n Chaos: inject adapter throw mid-run, assert bypass log, zero stale hits, recovery on reinit.\n```\n\n## Error & Rescue Registry\n| Method/codepath | What can go wrong | Exception class | Rescued? | Rescue action | User sees |\n|-----------------|-------------------|-----------------|----------|---------------|-----------|\n| readProfile cache.get/set | adapter fault | adapter's CacheAdapterError | Y (adapter) | bypass until reinit, fallback_errors++, log | nothing (DB read) |\n| readProfile repository.read | DB timeout / pool exhausted | existing typed repository errors | Y (existing mapping) | no fill, flight released, error propagated | existing 5xx mapping |\n| readProfile repository.read | record absent | typed not-found or null | Y | ABSENT sentinel 10 s [F2] | existing not-found result |\n| readProfile fill | write completed during flight | none (logic) | Y [F1] | skip fill, metric | fresh value on next read |\n| readProfile cache.set | value over byte cap | none (adapter rejects/evicts) | Y | eviction metric | nothing |\n| writeProfile repository.write | DB error | existing typed repository errors | Y | gen bump, cache preserved, propagate | existing error |\n| writeProfile repository.write | timeout after commit | existing timeout error | Y (partial) | gen bump blocks in-flight fills; cached value may persist to TTL | possible 30 s stale read (retained contract, F1 residual) |\n| startup | multi-process with caching on | existing ConfigurationError | Y | refuse start | deploy fails loudly |\n| flag.onChange | reinit throws | adapter's CacheAdapterError | Y | bypass mode, log | nothing |\n\n## Failure Modes Registry\n```\n CODEPATH | FAILURE MODE | RESCUED? | TEST? | USER SEES? | LOGGED?\n ----------------------|--------------------------------|----------|-------|--------------------|--------\n read fill | late fill after write (S1-S3) | Y [F1] | Y T1 | fresh value | metric\n read miss | join pre-commit flight (S2) | Y [F1] | Y T1 | fresh value | metric\n read absent | absence never cached | Y [F2] | Y T2 | same absent result | metric (hit/miss)\n cohort change | stale entry resurrected | Y [F3] | Y T3 | fresh value | reinit log\n adapter fault | silent bypass | Y | Y T5 | nothing | log + alert (a)\n low hit rate | targets missed silently | Y [F4] | n/a | nothing | alert (b)\n write timeout | committed but reported failed | partial | Y | up to 30 s stale | existing error log\n cross-tenant hit | wrong tenant's DTO | Y (key) | Y T4 | never | n/a\n CRITICAL GAPS: 0 after amendments (original plan had 1: late fill, unrescued, untested, silent).\n```\n\n## NOT in scope\n- Distributed or cross-process cache: single process today; startup rejects multi-process with caching on.\n- Prewarming: cold start fits existing DB capacity.\n- Write-through (Approach C) and DTO versioning: not needed to meet the invariant.\n- Consistency-semantics changes, including relaxing \"failed-write preservation\": explicit non-goal.\n- General cache framework or new product surfaces: explicit non-goal.\n\n## What already exists\nLRU adapter (caps, TTL, ABSENT sentinel, bypass-on-failure): reused unchanged. Per-key single-flight wrapper: reused with a stamped key. Runtime feature flag: reused with an onChange hook. Repository contract tests and typed error mapping: reused and extended to the wrapper [F6].\n\n## Dream state delta\nInterface unchanged, coherence rule explicit and tested, metrics and alerts in place. A future cache tier replaces one module and keeps the same tests. Nothing here blocks the 12-month ideal.\n\n## TODOS.md updates\n0 items proposed. HOLD SCOPE: every evidenced gap was repaired in scope; no hypothetical capacity or alternative surfaced.\n\n## Stale diagram audit\nNo ASCII diagrams exist in the files this plan touches. T1 adds the module comment diagram (state machine plus S1 schedule); it must be updated with any change to the 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: ~15min)** \u2014 wrapper \u2014 Add write-generation guard, stamped single-flight key, `fill_skipped_stale` metric, module comment diagram\n - Surfaced by: Section 1 / Section 4 \u2014 F1 late-fill race (schedules S1-S3)\n - Files: repository cache wrapper module; wrapper test file\n - Verify: schedule tests S1, S2, S3 pass with pause/release hooks; metric increments once in S1\n- [ ] **T2 (P1, human: ~1h / CC: ~5min)** \u2014 wrapper \u2014 Map repository absence to ABSENT sentinel and back\n - Surfaced by: Section 2 \u2014 F2 absence path unspecified\n - Files: wrapper module; wrapper test file\n - Verify: two absent reads in 10 s make one DB call; identical caller outcome; create clears sentinel\n- [ ] **T3 (P1, human: ~1.5h / CC: ~10min)** \u2014 wrapper + flag \u2014 Shared `inCohort(key)`; reinit empty cache and clear `writeGen` on any flag change; reinit log\n - Surfaced by: Section 4 / Section 9 \u2014 F3 cohort change resurrects stale entries\n - Files: wrapper module; flag wiring; wrapper test file\n - Verify: cohort parity test; 50% to 10% to 50% sequence never returns pre-write value; reinit log asserted\n- [ ] **T4 (P1, human: ~1h / CC: ~5min)** \u2014 CI \u2014 Run repository contract suite against the wrapper, cold and pre-warmed, flag on\n - Surfaced by: Section 3 \u2014 F6 contract suite scope ambiguous\n - Files: CI config; test harness setup\n - Verify: suite green in both states\n- [ ] **T5 (P2, human: ~2h / CC: ~10min)** \u2014 observability \u2014 Alerts (a) fallback_errors > 0 for 5 min, (b) hit rate < 60% for 15 min at 100%; runbook; bypass/reinit structured logs without keys\n - Surfaced by: Section 8 \u2014 F4 no alert or runbook\n - Files: alert rules; runbook doc; wrapper module log lines\n - Verify: injected adapter failure fires bypass log in test; alert rules lint in CI\n- [ ] **T6 (P2, human: ~20min / CC: ~3min)** \u2014 tests \u2014 Assert cached DTO and nested objects are frozen\n - Surfaced by: Section 5 \u2014 F5 shared reference mutation hazard\n - Files: wrapper test file\n - Verify: test passes; if it fails, stop rollout and decide freezing strategy\n- _No new tasks from Section 7 (Performance), Section 10 (Trajectory), Section 11 (skipped, no UI scope)._\n\nJSONL task artifact: not written this run (no shell; git and mutating commands disallowed).\n\n## Completion Summary\n```\n +====================================================================+\n | MEGA PLAN REVIEW \u2014 COMPLETION SUMMARY |\n +====================================================================+\n | Mode selected | HOLD SCOPE (user-specified) |\n | System Audit | skipped by run instruction |\n | Step 0 | premise holds; approach B auto-chosen (D1) |\n | Section 1 (Arch) | 1 issue found (F1) |\n | Section 2 (Errors) | 9 error paths mapped, 1 GAP (F2), closed |\n | Section 3 (Security)| 1 issue found (F6), 0 High severity |\n | Section 4 (Data/UX) | 10 edge cases mapped, 2 unhandled -> closed |\n | Section 5 (Quality) | 1 issue found (F5) |\n | Section 6 (Tests) | Diagram produced, 4 gaps -> T1-T4, T6 |\n | Section 7 (Perf) | 0 issues found |\n | Section 8 (Observ) | 1 gap found (F4) |\n | Section 9 (Deploy) | 1 risk flagged (F3, shared) |\n | Section 10 (Future) | Reversibility: 5/5, debt items: 1 |\n | Section 11 (Design) | SKIPPED (no UI scope) |\n +--------------------------------------------------------------------+\n | NOT in scope | written (5 items) |\n | What already exists | written |\n | Dream state delta | written |\n | Error/rescue registry| 9 methods/paths, 0 CRITICAL GAPS |\n | Failure modes | 8 total, 0 CRITICAL GAPS (1 in original) |\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 (config), no fallback run |\n | Lake Score | 7/7 recommendations chose complete option |\n | Diagrams produced | 7 (arch, data flow, async schedule, state, |\n | | error flow, deploy+rollback, test) |\n | Stale diagrams found | 0 |\n | Unresolved decisions | 0 |\n +====================================================================+\n```\nAuto-decided (recommended option, no human present): D1 approach B; D2 F1 remedy; D3 F2 remedy; D4 F3 remedy; D5 F4 remedy; D6 F5 remedy; D7 F6 remedy. Mode was user-specified, not a decision.\nPersistence not performed this run (no shell): review-log, decision-log, disabled-outside-voice record, tasks JSONL, learnings log, telemetry. No durable learnings this session beyond the plan content itself.\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, 0 critical gaps (6 findings, all remedied in plan) |\n| Outside Review | codex (auto after `/plan-ceo-review`) | 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, disabled by `codex_reviews: disabled`; no outside process started, no native fallback dispatched, no outside findings. Disabled record not persisted (no shell this run).\n\n**VERDICT:** CEO CLEARED \u2014 eng review required.\n\nNO UNRESOLVED DECISIONS\n", "reportSha256": "facc56ad6a7011eaf4db629f8102c7edbd7f9b68eeac95445e0fc546d0d23baf", "sourceProof": ".context/ship-source-ai-delta-paid-20260910-v1/sdk-retry-report-ledger-v1/proof.json", "sourceProofSha256": "4cdadffa0b97546925d4cdcacd27a8beed9e323951116d389387d5423c042f49", "finding": "| F1 | P1 CRITICAL | Late fill after write. Read misses, DB returns v1, write commits v2 and deletes (no-op), read then fills v1; every later read gets v1 until TTL. Plan lines 27-29 forbid this; sketch lines 36-49 and 32-34 explicitly add no coordination. Second path: a post-write reader joins the pre-commit single-flight. | Per-key write stamp from a process-wide clock, bumped in `finally` of `writeProfile`; fill only if stamp unchanged; single-flight key `${key}#${stamp}`; `fill_skipped_stale` metric; comment diagram in module. | A write that times out after committing keeps the old cached value up to 30 s (retained \"failed-write preservation\" contract). Bounded `writeGen` eviction causes only extra misses. | Controlled pause/release tests for schedules S1, S2, S3 (Section 4); assert post-write read observes v2 and metric increments exactly once for S1. |", "heading": "**3. Async ordering schedules (shared state: cache[key], writeGen[key])**", "trace": " S1 late fill R1 (read, began before W) W (write) R2 (read, began after W) cache gen\n 1 get->miss, seen=0 - 0\n 2 DB SELECT -> v1\n 3 DB UPDATE commits v2\n 4 sketch delete (no-op), return - 0\n 5 sketch promise resolves, set(v1) v1\n 6 sketch get -> v1 VIOLATION\n 4'guarded gen=1, delete, return - 1\n 5'guarded stamp 1 != seen 0: skip fill, metric++, return v1 (allowed) - 1\n 6'guarded miss, seen=1, flight key#1 -> v2, set v2\n S2 join stale flight R2 misses while R1's flight is open: sketch coalesces R2 onto key (v1) VIOLATION;\n guarded uses key#1, a new flight issued after commit -> v2.\n S3 read begins mid-write R3 seen=0 issues SELECT before commit -> v1; W bumps gen=1 after commit; R3 skips fill.\n Mechanism excluding violating orders: bump happens synchronously after the write settles and before any\n later read can capture a stamp; a fill requires stamp equality; flights are stamp-scoped." } }