verify+fix: Pi's dedup + Convex-spike designs build-ready [CC]
- dedup: separate client-mapped plan_type (not payer) so CoverageLine never guesses plan type; fix merge_quantities to sum across distinct component configs (blanket-max dropped qty in mixed dup+split groups) - convex-spike: shipments orderNumber/hcpcs -> arrays (post-dedup multi-value); de-stale dedup note to LOCKED - ledger: Design Verification section added Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e5c2f13b7
commit
6f3ce16960
3 changed files with 36 additions and 18 deletions
|
|
@ -6,6 +6,7 @@
|
|||
**Approves:** Kisa
|
||||
**Source:** docs/convex-signal-instance-brief-2026-06-24.md
|
||||
**Status:** Design input. This is a SEPARATE, parallel spike in repo `signal-convex`. The current Signal build keeps running.
|
||||
**Verification fixes (Claude, 2026-06-24):** `shipments.orderNumber`/`hcpcs` changed to arrays to match the deduped multi-value model; §11 dedup note updated (the key is now LOCKED). Carry the dedup doc's plan_type fix: grade on the client-mapped `planType`, never the payer name.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -214,10 +215,10 @@ export default defineSchema({
|
|||
shipments: defineTable({
|
||||
lineId: v.id("coverageLines"),
|
||||
dateOfService: v.string(), // YYYY-MM-DD
|
||||
quantity: v.number(),
|
||||
quantity: v.number(), // total after dedup (sum across distinct component configs)
|
||||
components: v.array(v.string()),
|
||||
orderNumber: v.string(),
|
||||
hcpcs: v.string(),
|
||||
orderNumbers: v.array(v.string()), // post-dedup: collected, display-only
|
||||
hcpcs: v.array(v.string()), // post-dedup: collected, display-only
|
||||
isCurrent: v.boolean(),
|
||||
}).index("by_line", ["lineId"])
|
||||
.index("by_line_dos", ["lineId", "dateOfService"]),
|
||||
|
|
@ -513,7 +514,7 @@ The priority is proving the core thesis: CSV-in → grade → worklist-display o
|
|||
|
||||
### Dedup key (carried from readiness-model decision)
|
||||
|
||||
**Pending Kisa's decision** — same patient + device + DOS under two order numbers → one worklist line or two? The spike design supports both. The `shipments` table has `orderNumber` as a displayed field (not a dedup key), and the dedup logic lives in `readiness/engine.ts`. When Kisa decides, one line changes in the engine.
|
||||
**LOCKED 2026-06-24** — key = `(patient_id, device_type, date_of_service)`; order numbers are display-only (stored as `orderNumbers: v.array`), never in the dedup key. Full logic (tiebreakers, quantity-merge guard, test cases) in `docs/dedup-design-2026-06-24.md`. Dedup runs inside the CSV import mutation, before grading.
|
||||
|
||||
### Data-access interface boundary
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
**Design by:** Pi (decision locked by Kisa)
|
||||
**Builds by:** Claude (Phase: patient/line/shipment grouping + required-vs-input verdict)
|
||||
**Source:** docs/readiness-model-brief-2026-06-23.md, Kisa dedup decision 2026-06-24
|
||||
**Verification fixes (Claude, 2026-06-24):** (1) added a separate client-mapped `plan_type` field, distinct from display `payer`, so CoverageLine keys on the mapped plan type and never the payer name (locked 2026-06-23); (2) fixed `merge_quantities` to sum across distinct component configs instead of taking a blanket max.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -58,7 +59,8 @@ class MergedShipment:
|
|||
components: list[str] # combined set, deduped
|
||||
order_numbers: list[str] # collected, displayed only
|
||||
hcpcs_codes: list[str] # collected, displayed only
|
||||
payer: str # taken from the LATEST row in the group
|
||||
payer: str # raw payer NAME from the LATEST row — DISPLAY ONLY
|
||||
plan_type: str # REQUIRED client-mapped plan type (medicare|medicare_advantage|medicaid|commercial|unknown); NEVER derived from payer name; used for CoverageLine keying + grading
|
||||
# CSV doc fields — merged from all rows, latest non-null wins
|
||||
csv_visit_date: date | None
|
||||
csv_swo_status: str | None
|
||||
|
|
@ -91,7 +93,7 @@ When multiple CSV rows have the same `(patient_id, device_type, DOS)`:
|
|||
| Normal split | Row A qty=3, Row B qty=1 | total_qty = 4 | Sensor + transmitter = 4 components |
|
||||
| Duplicate | Row A qty=3, Row B qty=3 (same components) | total_qty = 6 | ⚠️ This IS the risk of summing: a data-entry duplicate doubles the quantity. Mitigation: flag rows where components are identical across rows in the same group. If all fields except order_number match exactly, it is a likely duplicate → take max, not sum. |
|
||||
| Zero row | Row A qty=3, Row B qty=0 | total_qty = 3 | Zero-quantity rows are informational (e.g., a cancelation) and do not add to quantity |
|
||||
| Wildly different | Row A qty=3, Row B qty=100 | total_qty = 103 | This is almost certainly an error. Flag for review. For the pilot, take max (100) and log a warning. |
|
||||
| Wildly different | Row A qty=3, Row B qty=100 (same component) | total_qty = 100 | Almost certainly an error. Same component config, so take max (100, not sum); flag for review and log a warning. |
|
||||
|
||||
### Recommended default for quantity ambiguity
|
||||
|
||||
|
|
@ -99,18 +101,25 @@ When multiple CSV rows have the same `(patient_id, device_type, DOS)`:
|
|||
|
||||
```python
|
||||
def merge_quantities(rows_in_group: list[CSVRow]) -> int:
|
||||
"""Sum quantities within a dedup group.
|
||||
If two rows have identical components, take max instead of sum
|
||||
(likely a data-entry duplicate, not a real split)."""
|
||||
component_sets = [set(row.components.split(",")) for row in rows_in_group]
|
||||
unique_component_configs = set(frozenset(cs) for cs in component_sets)
|
||||
"""Sum quantities across DISTINCT component configs within a dedup group,
|
||||
while collapsing exact-duplicate rows.
|
||||
|
||||
if len(unique_component_configs) < len(rows_in_group):
|
||||
# At least one component config appears more than once
|
||||
# Take the maximum quantity, not the sum (defense against dupes)
|
||||
return max(row.quantity for row in rows_in_group)
|
||||
Group rows by their component set. Within one config, identical rows are
|
||||
likely data-entry duplicates, so take the MAX for that config (not the sum).
|
||||
Then SUM across distinct configs (a real component split, e.g. sensor + transmitter).
|
||||
|
||||
return sum(row.quantity for row in rows_in_group)
|
||||
Fixes the mixed case: two duplicate [sensor] rows + one [transmitter] row
|
||||
-> max(sensor)=3 + max(transmitter)=1 = 4, not a blanket global max of 3.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
by_config: dict[frozenset, list[int]] = defaultdict(list)
|
||||
for row in rows_in_group:
|
||||
config = frozenset(row.components.split(","))
|
||||
by_config[config].append(row.quantity)
|
||||
|
||||
# Per distinct config: max (defend against duplicate rows). Across configs: sum.
|
||||
return sum(max(qtys) for qtys in by_config.values())
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -125,11 +134,11 @@ def assign_to_coverage_lines(merged_shipments: list[MergedShipment]) -> dict[Cov
|
|||
lines: dict[CoverageLineKey, list[MergedShipment]] = {}
|
||||
|
||||
for shipment in merged_shipments:
|
||||
# Capline: plan_type comes from client's CSV mapping, not from guessing
|
||||
# plan_type is the client-mapped REQUIRED field, never the payer name (locked 2026-06-23)
|
||||
key = CoverageLineKey(
|
||||
patient_id=shipment.patient_id,
|
||||
device_type=shipment.device_type,
|
||||
plan_type_id=shipment.payer, # The client-mapped plan type, not the raw payer name
|
||||
plan_type_id=shipment.plan_type, # mapped plan type; payer name is display-only
|
||||
)
|
||||
lines.setdefault(key, []).append(shipment)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,3 +51,11 @@ Canonical, git-tracked record of **what shipped, what didn't, and the reason or
|
|||
## Maintenance
|
||||
|
||||
Update this whenever an item ships or a blocker changes. Move shipped items to a `## Shipped (archive)` section when the table grows. Both agents reason from this; Kisa validates and redirects.
|
||||
|
||||
## Design Verification (2026-06-24, Claude)
|
||||
|
||||
Pi's dedup design (item 9) and Convex-spike design (item 6) were verified line-by-line. Both are build-ready; fixes applied directly to the design docs:
|
||||
|
||||
- **dedup-design-2026-06-24.md**: (1) added a separate client-mapped `plan_type` field, distinct from display `payer`, so CoverageLine keys on the mapped plan type, never the payer name — the original draft reintroduced the killed plan-type-guessing bug. (2) Fixed `merge_quantities` to sum across distinct component configs; the old blanket-max dropped quantity in mixed duplicate+split groups (e.g. two [sensor] + one [transmitter] returned 3 instead of 4).
|
||||
- **convex-signal-spike-design-2026-06-24.md**: `shipments.orderNumber`/`hcpcs` changed to arrays (post-dedup multi-value); §11 dedup note de-staled to LOCKED; carry-forward note to grade on the mapped `planType`.
|
||||
- Noted, not a blocker: "Convex auth -> Clerk = config-only swap" is optimistic; treat as low-but-nonzero effort (Convex Auth still maturing).
|
||||
|
|
|
|||
Loading…
Reference in a new issue