@@ -85,8 +85,9 @@ Consequences vs merging at enqueue: `JellyfishQuietDelayMaximum` is no longer ne
8585a shared row, so nothing can be starved), and a long jellyfish upload recalculates roughly once per
868690s quiet window rather than once at the end.
8787
88- ** Processor** (Quantity 15, Frequency 30s): ` updateSummaries ` (all 3 types) → ` triggerEHRSync `
89- (` SyncEHRDataForPatient ` , at-least-once) → ` Delete ` .
88+ ** Processor** (Quantity 15, Frequency 30s): ` updateSummaries ` (all 3 types) → ` updateClinicSummaries `
89+ (cgm/bgm changes only, at-least-once — Step 16a) → ` triggerEHRSync ` (` SyncEHRDataForPatient ` ,
90+ at-least-once) → ` Delete ` .
9091Failures → Failing (exponential 1m/5s jitter/1h cap).
9192
9293** Logging** : INFO — item lifecycle + data-shape changes (processor start/finish: userId, reasons,
@@ -667,6 +668,103 @@ bump the abbott plugin's platform pin (it pins the exact commit `9ee577a1c`, whi
667668 run the remediation (work user ids → ` dates.outdatedSince ` ), confirm the runners pick them up
668669- [ ] Done — ** ⏸ approval — PR1 checkpoint**
669670
671+ ### Step 16a — Clinic summary updates move from clinic-worker CDC into the work item (2026-08-26)
672+
673+ The clinic service stores a copy of each patient's cgm/bgm summaries, and the EHR sync generates
674+ reports ** from that copy** — so the copy must be current before the sync fires. Today clinic-worker's
675+ ` patientsummary ` CDC consumer maintains it (Mongo change stream → Kafka ` data.summary ` →
676+ ` UpdatePatientSummary ` /` DeletePatientSummary ` ). Moved into the processor as a step strictly between
677+ ` updateSummaries ` and ` triggerElectronicHealthRecordSync ` .
678+
679+ - [x] ** Clinic client bumped** ` v0.0.0-20250122… ` → ` v0.0.0-20260814105914-911a077531db ` (the old pin
680+ predates ` PatientSummaryV1 ` /` CgmStatsV1 ` /` DeletePatientSummary ` and the ranges-based schema).
681+ Generated names lost their unsuffixed aliases (` Clinic ` →` ClinicV1 ` , ` Patient ` →` PatientV1 ` ,
682+ ` EHRSettings ` →` EhrSettingsV1 ` , …) — mechanical renames in clinics/ehr/prescription/notifications
683+ + ` clinics/test/clinics.go ` rebuilt against the new ` EhrSettingsV1 ` shape
684+ - [x] ` clinics/summaries.go ` : ` NewPatientSummary(cgm, bgm) *clinic.PatientSummaryV1 ` — the
685+ clinic-worker mapping (` patientsummary/models.go ` ) ported onto native ` summary/types ` , keeping
686+ the UI eligibility rules verbatim (percentages only with records; TIR percent gated on >70%
687+ single-day use or >24h CGM use; GMI only >70% use; percent deltas only when the reconstructed
688+ previous period also qualifies; BGM stdev/CoV only ≥30 records and ≥7 days). Two clinic-worker
689+ bugs fixed in the port: dead ` IsZero() ` guards on ` time.UnixMilli ` values (absent dates were
690+ sent as 1970-01-01 with ` Has*: true ` — now omitted with ` Has*: false ` ), and ~ 40 unguarded
691+ ` period.Delta ` dereferences (nil delta panicked the consumer — now treated as a zero delta).
692+ Type aliases ` types.CGMSummary ` /` BGMSummary ` /` ContinuousSummary ` added for the generic
693+ instantiations
694+ - [x] ` clinics.Client ` gains ` UpdatePatientSummary ` (200/204/404 → success; 204 = not a patient of
695+ any clinic, the common case) and ` DeletePatientSummary ` (200/204 → success), mirroring
696+ clinic-worker's accepted statuses plus the ` SyncEHRDataForPatient ` 404 precedent
697+ - [x] ** Only an actual change is reported** (requirement). The ` Summarizers ` seam returns
698+ ` SummariesUpdate{CGM, BGM, UpdatedTypes, Deleted} ` (by value — a pointer invited nil checks its
699+ callers never needed): each cgm/bgm update is classified against a pre-read (` GetSummary `
700+ before ` UpdateSummary ` ) — recalculation always advances ` Dates.LastUpdatedDate ` , the
701+ no-data-modified branch never does, and deletion is a non-nil→nil transition (recording the
702+ deleted summary's id for the clinic delete). The legacy outdated-flag clear (` SetNotOutdated ` )
703+ is deliberately classified as no change. Continuous summaries are calculated but never
704+ classified or reported — the clinic model has no ` con `
705+ - [x] ** ` LastUpdatedDate ` discriminator audited (2026-08-26 review)** : the field is written in
706+ exactly two places. ` Dates.Update ` sets it, and is reachable only past the
707+ ` status.EarliestModified.IsZero() ` guard — i.e. only when the fetcher reports data modified
708+ since the watermark. ` Dates.Reset ` zeroes it on the schema-migration path, but the zero can
709+ never be persisted: the zero watermark makes the follow-on fetch either find data (full
710+ recalculation → ` Dates.Update ` stamps a fresh value; classified updated, correctly — the
711+ rebuild may change content) or find none (summary deleted; classified deleted). The
712+ no-data-modified branch rewrites the summary without touching it. So "timestamp advanced" ⇔
713+ "the summarizer was given new or modified data (or rebuilt in full)" — the invariant the
714+ classification relies on holds
715+ - [x] ** At-least-once across retries** : the pipeline retries from the start, and a retried
716+ calculation reports no further change, so changes are persisted into the work metadata
717+ (` pendingSummaryUpdates ` types / ` pendingSummaryDeletes ` ids) BEFORE they are reported —
718+ the absorb pattern. On the success path that is one extra ` ProcessingUpdate ` per changing item;
719+ on the failing path the merged metadata rides the failing update for free (including changes
720+ calculated before a mid-` UpdateSummaries ` error — the seam returns partial results with the
721+ error). Not cleared after reporting: the item is deleted at the end, and a re-report after an
722+ EHR-sync failure stores the same summary again rather than twice. Remaining window, accepted: a
723+ crash between the summary write and the metadata persist loses that report; the next actual
724+ change reports the full current summary, so updates self-heal — a lost * delete* report does not
725+ - [x] ** Retry-interleaving analysis (2026-08-26 review)** : deletions are reported before updates, and
726+ a deletion matches only the summary id it names — never the id of a recreation — so every
727+ single-pod retry interleaving converges (update-then-deleted → delete reported, update skipped
728+ via nil content; deleted-then-recreated → old id unset, new stats stored; pending updates
729+ re-send the CURRENT content re-read each attempt, so a sticky record never pushes stale stats;
730+ the delete loop and the update re-send are idempotent). ONE genuine race was found: a processor
731+ reaped mid-report (>5m stall) whose ` UpdatePatientSummary ` completes AFTER another pod's retry
732+ pushed newer stats would regress the clinic copy until the user's next change — unlike CDC
733+ there is no partition ordering. CLOSED clinic-side by ** tidepool-org/clinic#265**
734+ (2026-08-26): ` UpdateSummaryInAllClinics ` applies each stats type only when the incoming
735+ ` dates.lastUpdatedDate ` is not older than the stored one (` $cond ` pipeline update, per type,
736+ per patient document; equal dates still apply so re-reports stay idempotent; BSON ordering
737+ missing < null < date means absent stored stats always accept and an undated report replaces
738+ only undated stats). Deploy #265 before or with this step — before it, the race stands as
739+ originally accepted (requires a stalled-then-successful HTTP call spanning the reap grace plus
740+ the retry's full recalculation)
741+ - [x] ` updateClinicSummaries ` step: deletes first (each id, tolerant of ids the clinic no longer
742+ holds), then one ` UpdatePatientSummary ` carrying only the changed types' stats (the clinic
743+ service merges per type, leaving the other untouched). An updated type whose summary was
744+ deleted by a later attempt has its deletion recorded and no summary to report — skipped. Not
745+ reason-gated, unlike the EHR sync: every actual change reaches the clinic
746+ - [x] ` absorbPending ` validates each sibling before absorbing it — ` validateIdentity ` (the same
747+ check the claimed work gets) on top of the field validation ` metadata.Decode ` already runs —
748+ so a row listed by group but naming another user in its metadata is left pending to fail on
749+ its own pickup, rather than having that user's change merged into this one and destroyed
750+ - [x] Tests: processor — record→report→sync ordering pinned (` gomock.InOrder ` ); only changed types in
751+ the payload; no clinic call when nothing changed; delete before update; pre-populated metadata
752+ reports with a no-change recalculation (the retry case); failing metadata carries the
753+ unreported changes (both after a report failure and after a partial calculation failure); three
754+ new Failing entries. clinics — mapping (ids/config/dates, zero-date omission, empty-reason
755+ replacement, every eligibility gate, nil delta, non-day period keys dropped) and both new
756+ client methods (paths, tolerated statuses, error statuses). Metadata parse/validate/round-trip
757+ entries for the new fields
758+ - [x] Done: ` GOTEST_PKGS='./data/... ./summary/... ./clinics/... ./work/... ./ehr/... ./prescription/... ./notifications/...' make test-go `
759+ green (84 packages); ` go build ./... ` /` go vet ./... ` /` gofmt ` /` goimports -local ` clean
760+ - [ ] ** Cross-repo consequence** : once this deploys, clinic-worker's ` patientsummary ` consumer and
761+ the processor both push (benign duplicates — same content, idempotent upsert). Retire the
762+ consumer in clinic-worker with the same gate as clinic-worker #123 : the drain endpoint's inline
763+ recalculations (alive until PR2/PR3) reach the clinic only via CDC, so the consumer must
764+ outlive the legacy queue (` tidepool_summary_queue_length ` → 0). The patient-create
765+ ` populateSummary ` path in clinic-worker (` patients/consumer.go ` ) stays — it pulls, not CDC
766+ - ** ⏸ approval**
767+
670768## PR2 — task service retirement
671769
672770### Step 17 — Remove runners + task-row cleanup
0 commit comments