Initial import: eCW Charge Validation bot and results Portal

Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+324
View File
@@ -0,0 +1,324 @@
# eCW Charge Validation bot: plan before the client demo and live production
I merged 60 verified findings into 38 items: 11 demo blockers, 16 production items, 5 logic items, 6 structure changes and 7 performance items (2 of the performance items are new; the rest point back to items above). Every severity below is the verifier's. When findings were merged, the item takes the highest verifier severity. Effort: **S** = under half a day, **M** = 1–3 days, **L** = more than 3 days. All paths are under `src/ecw_bot/` unless shown otherwise.
---
## 1. Demo blockers
**D1 · Test mode is on, so the demo Charge Summary is mostly blank and replaces the live file's name.** Severity: high. Effort: S
`flows/export_medcor_ready.py:552, 572-574, 600-613, 684-698` · `core/charge_summary.py:138-157, 181-191`
- **Problem:** `TEST_CLAIM_IDS_FILE` is set to a 2-column hand sheet. The summary is built from that sheet, so 16 of the 24 columns come out blank, Group Name included. The file is saved as `Charge Summary_<date>.xlsx`, which pushes a live same-day file aside to `__HHMMSS`. An unreadable sheet still exits 0, and with `SHOW_CLAIM_IDS=0` the operator gets no sign that this is a test run.
- **Fix:** For the demo, either clear `TEST_CLAIM_IDS_FILE` or point it at `Medcor Ready Claim Sheet.xlsx` (it has all 15 report columns). In code:
- write test runs to `Charge Summary_TEST_<date>.xlsx`;
- print a "TEST MODE" banner that always shows;
- return a non-zero exit code when the test sheet cannot be read;
- on demo morning, check that every test claim is still in the 108ME queue.
**D2 · The Charge Summary marks claims "Clean / Pass / Ready to Submit" even when they were never actually checked.** Severity: high. Effort: M (a demo-only version is S)
`core/charge_summary.py:85-116` · `conditions/base.py:69-90` · `flows/export_medcor_ready.py:426-442, 498-507` · `conditions/telehealth.py:112-136` · `conditions/vaccine_admin_codes.py:336-360, 635-639` · `pages/dental_notes.py:47-52`
- **Problem:** `claim_outcome` knows only two states: something fired, or the claim is clean. All of the following fall through to CLEAN/Pass:
- a condition's `detect()` or `apply()` raised, because `run_all` drops it;
- a condition detected a problem but declined to act (unmapped payer, CPT tab not confirmed, admin code could not be added);
- `APPLY_CORRECTIONS=0`;
- the progress note was not read (`opened` is nothing, absent, disabled, or an error);
- Dental `high_risk` is None, or the sliding fee verdict is "unknown".
A form-read error leaves a blank row that looks exactly like "not found". The per-claim box also says "none matched" right after the box that said "about to apply X".
- **Fix:**
- Give `ConditionResult` a `status` (changed / no_change / declined / failed) and a `reason`, and have `run_all` return every condition that was detected or failed.
- Store `ran`, `fired`, `problems` and `note_read` in `data["_conditions"]`.
- Map any problem, an unread note, `APPLY=0`, or an incomplete Dental check to Category "Review" / "Needs manual review" with the reason in Comments.
- Write an explicit "Bot could not read claim" row for read errors.
- Demo-only version: change just `claim_outcome` so a missing `_conditions`, an unread note, and Dental None/unknown become Review.
- Add tests for each of these paths.
**D3 · The telehealth conditions report a change on claims that are already correct (POS '2'→'2'), so correct claims are marked Fail.** Severity: high. Effort: S
`pages/claim_form.py:799-806` · `conditions/telehealth.py:131-144` · `conditions/medicare_telehealth.py:73-96` · `tests/test_telehealth_fix.py:127-128`
- **Problem:** `set_cpt_pos_modifier` always fills POS and M1 and always records a change, so every correctly coded telehealth claim gets both review boxes and goes to the summary as "Fail / Corrections pending".
- **Fix:** Skip rows that already match. Normalize POS by stripping leading zeros so that "02" equals "2", and compare M1 in upper case. Record a change only when the value after differs from the value before, return `none` when nothing moved, and add an apply-twice test.
**D4 · Audio-only detection searches the whole note for "audio", so "audio and video" visits get modifier 93.** Severity: high. Effort: M
`pages/claim_form.py:719-727, 754` · `pages/progress_note.py:106, 129` · `conditions/telehealth.py:67, 120-123` · `conditions/medicare_telehealth.py:43, 69-71`
- **Problem:** The bare substring tests on `document.body` cause two errors:
- "real-time audio and video", "Audiology" and "audiometry" all classify the visit as audio-only, giving Commercial and Medicare visits 93 instead of 95;
- a "telehealth" or "telephone" mention in the Plan or header makes an in-person visit count as telehealth.
- **Fix:** Classify only from the structured fields: the Session Setting (PoS), Mode of Treatment, the TELEHEALTH section, and the procedure-code (TH) tags and modifiers.
- Explicit "audio only" or "no video" means audio. "audio and video" or "video" means video. Anything else means unknown, and the claim is left for review.
- Use word boundaries, and show the matched phrase in `describe()`.
**D5 · telehealth writes modifier 93 for unmapped, blank, Program and Dentaquest payers on audio visits.** Severity: high. Effort: S
`conditions/telehealth.py:77-101 (99-100)` · `tests/test_conditions_telehealth.py:69`
- **Problem:** The `mode == "audio"` check runs before the group lookup. As a result:
- an unclassified Medicare payer gets rule #21 (POS 2 + 93) where rule #22 applies;
- an unmapped Medicaid MCO gets 93 instead of GT.
This contradicts the module's own "leave as read" rule.
- **Fix:** Return None unless the group is Commercial before the audio branch runs. Update the test at line 69, and report "skipped: unmapped payer X" as a declined result (see D2).
**D6 · medicare_telehealth hides a failed G2025 add; the box and the summary both read as success.** Severity: high. Effort: S
`conditions/medicare_telehealth.py:80-121` · `core/charge_summary.py:56-58` · `pages/claim_form.py:1543`
- **Problem:** When G2025 is not confirmed, the condition still returns changed=True and `describe()` never mentions G2025. The fixed summary text then says "G2025 added … other CPTs unchecked", which is false on the UB screen, where the Add flow has not been verified.
- **Fix:**
- Add a `{"code": "G2025", "failed": True}` record and have `describe()` print "G2025 NOT added — add by hand; other rows NOT unchecked".
- Build the summary comment from the actual changes, and mark the claim Review so Save is not offered.
- Read M1 back after `_set_row_modifier`.
**D7 · HCFA and Dental claims are read before Angular has filled the form.** Severity: high. Effort: S
`pages/claim_form.py:384-397, 618-625` · `pages/dental_notes.py:34-60`
- **Problem:** `open_claim` returns as soon as the HCFA or Dental Cancel button is visible. `extract()` then reads 0 CPT rows, Prog. Notes reads as "disabled", and blank Dental notes read as "not high risk". This was seen live on a claim-1 read (run_20260930_163006). Because no condition fires, the claim goes to the summary as Pass.
- **Fix:**
- HCFA: wait until `[ng-bind="ClaimData.InvId"]` equals the claim id (this also gives HCFA the identity check UB already has), then until the count of CPT rows with a code is at least 1 and stable for about 500 ms.
- Dental: wait on an equivalent Dental marker.
- Re-check the Prog. Notes enabled state for up to about 5 s.
- If any wait times out, mark the claim "not evaluated" (see D2).
**D8 · Progress Notes uses a fixed 3 s sleep and one probe, so slow notes become "nothing" and telehealth is never checked.** Severity: high. Effort: S
`pages/claim_form.py:629-639` · `pages/progress_note.py:148-161`
- **Problem:** Logs show 3 misses in about 115 note reads (2 of them on claim 1). In all 3 the viewer had opened but the document had not loaded yet. The claim is then reported Clean and Ready to Submit.
- **Fix:**
- Poll `find()` every 250 ms for up to about 15–25 s, accepting a frame only when it is visible, has `[prisma-section]`, and its section count is the same on two polls in a row. Retry the click once if nothing appears.
- Mark the claim `note_unread` for D2.
- Optional: check that the note's DOS matches the claim's CPT SDOS.
**D9 · The per-claim box shows a page of '?' for Dental claims, untranslated Tamil words, and up to three boxes per corrected claim.** Severity: medium. Effort: S
`flows/export_medcor_ready.py:357-381, 496-508, 244, 283` · `pages/claim_form.py:1622-1666 (1654)` · `pages/progress_note.py:221`
- **Problem:** For Dental data, `claim_form.summary()` prints "Patient : ? … Charges : ?" right after the HIGH RISK box. Other boxes show "… innum N" and "Chart locked: Aamaa/Illa". A corrected claim needs three OK clicks, each with a 180 s timeout.
- **Fix:**
- Fold the Dental verdict into the SHOW_EACH_CLAIM box instead of calling `claim_form.summary`. This also fixes the ungated Dental box (P6).
- Change the strings to "… and N more" and "Yes"/"No", and skip fields that came back empty or '?'.
- For the demo, merge the before-apply note into the review box and use a shorter `MSGBOX_TIMEOUT_SECONDS`.
**D10 · A second run on the same day loses its whole summary if the earlier file is open in Excel.** Severity: high. Effort: S
`core/charge_summary.py:181-192` · `flows/export_medcor_ready.py:172-186, 551-558`
- **Problem:** `out_path.rename(backup)` fails with WinError 32 when the file is open. The error is caught and a box is shown, but that run's outcomes are discarded. The Medcor Ready categorize step uses the same rename-aside pattern.
- **Fix:** If the rename or the write fails, write to `…__HHMMSS.xlsx` instead (write to a temp file, then move it into place) and say so in the log and the box. Apply the same fix to `_categorize_by_payer`, and add a test that holds the file open.
**D11 · (known; new data) Each failed claim click waits the full 30 s timeout, and the fallback check does not wait at all.** Severity: high. Effort: S
`pages/claim_form.py:349-376` · `core/page_utils.py:43-47`
- **Problem:** The "already open" fallback only runs after Playwright's full 30 s click timeout. It hit 43 of 92 opens (and 15/15 in the 16-Sep run), and the form sits open for about 30 s, which looks hung. The `visible()` check at line 372 is instant because Playwright 1.62 ignores its timeout, so a form that opens slowly gets Escaped by attempt 2. On HCFA that bypasses `closeClaimScreen()`.
- **Fix:** Use timeouts `(5_000, 15_000)`, replace line 372 with `locator.filter(visible=True).first.wait_for(timeout=5_000)`, and log `str(exc).splitlines()[0]` plus the elapsed time for the separate root-cause task.
---
## 2. Before live production
**P1 · Saving to eCW is on by default, and the .env reader can silently turn it on.** Severity: high. Effort: S
`settings.py:46-80, 367, 380, 442-456` · `.env.example:144, 152` · `flows/export_medcor_ready.py:670-671` · `core/msgbox.py:61-62`
- **Problem:** `SAVE_CORRECTIONS` and `APPLY_CORRECTIONS` both default to True, and the template ships them as 1. Values like `SAVE_CORRECTIONS=0 # note`, a blank value, or `O` all read as True; `CLAIM_PROCESS_LIMIT=5 # demo` reads as 1. No banner shows which mode is running, and the save box's default button is OK, so pressing Enter saves.
- **Fix:**
- Default both settings to False and set them to 0 in `.env.example`.
- Strip unquoted `\s+#…` comments when reading `.env`.
- Make unrecognised values a `validate()` error (exit 2).
- Print a `[MODE] VERIFY / LIVE-WRITE | limit | test file` banner, and make write mode require OK/Cancel at start.
- Add `MB_DEFBUTTON2` to the save confirmation so Enter means Cancel.
**P2 · A condition that fails partway through `apply()` leaves its edits on the form, and the next confirmed Save writes them unreviewed.** Severity: high. Effort: S (on top of the D2 `run_all` change)
`conditions/base.py:82-87` · `flows/export_medcor_ready.py:315-331` · `pages/claim_form.py:801-806, 868-869, 1074-1076, 1443-1456` · `docs/superpowers/specs/2026-09-16-condition-framework-design.md:174`
- **Problem:** The write helpers edit fields one at a time with no rollback. When one raises, the condition is dropped from `results`, the save box lists only the other conditions, and `saveAllData()` saves the whole form. The spec's statement that "only what's in results" gets saved is wrong. (Reproduced with fakes.)
- **Fix:** Mark the claim tainted whenever any `apply()` raised. `_confirm_and_save` then never offers Save: it shows "X failed mid-edit, not saved" and closes with Cancel. Correct the spec row, and add a gate test.
**P3 · One stuck form or an expired session ruins the rest of the run: no recovery, no stop, and errors are reported as "Not found".** Severity: high. Effort: M
`flows/export_medcor_ready.py:419-420, 444-446, 478-484, 510-526` · `pages/claim_form.py:372-376, 412-417, 494, 536-539` · `pages/claim_filters.py:343` · `pages/claim_results.py:246-247`
- **Problem:** Several gaps combine:
- `close_claim()`'s False return is ignored.
- If `open_claim` raises after the form appears, close is skipped entirely.
- A leftover form blocks the Lookup click (30 s per claim), and a lost session costs up to 300 s per claim.
- All of these become "Not found" with blank rows in the summary, and nothing counts consecutive failures.
- **Fix:**
- Close whenever `any_claim_form_open()`, inside try/except.
- Run a `_recover()` before each search: close the form, then re-run `open_claims` and re-apply the four filters.
- Record error separately from not_found everywhere.
- Stop with its own exit code after 5 consecutive errors, or when the login page is detected.
**P4 · Run results live only in memory: a crash or Ctrl+C loses the whole Charge Summary, and there is no resume.** Severity: high. Effort: M
`flows/export_medcor_ready.py:468-469, 475, 535, 540, 687-689, 334` · `pages/claim_form.py:1613-1618`
- **Problem:**
- The summary is written only after the last claim. A dead page in the `finally` close, or a KeyboardInterrupt, skips it and also skips `clear_claim_no`.
- A re-run starts again at claim 1.
- `<id>.json` is overwritten on every read.
- With saving on, only condition names are logged, never the changed fields.
- **Fix:**
- Append one flushed JSONL line per claim to `Output\progress_<date>.jsonl`.
- Wrap the loop in try/finally so the summary and `clear_claim_no` always run, with unreached claims marked "Not processed".
- Add `RESUME_TODAY=1`.
- Keep snapshots per run as `claims\<date>\<id>_<HHMMSS>.json`, and log the full save body.
**P5 · `save_claim` and `close_claim` count success as "#UBClaimDiv hidden", which is always true on HCFA and Dental.** Severity: high. Effort: S
`pages/claim_form.py:532-533, 1574-1582` · `selectors/claim_form.py:40, 229` · `flows/export_medcor_ready.py:444` · `core/charge_summary.py:113-116`
- **Problem:** On HCFA both functions return True immediately, even while the form is still up; this was reproduced. A save blocked by eCW validation is then logged as "Saved", skips Cancel, writes "Ready to Submit", and leaves the form blocking the next claims. `SEL_OK_HCFA` also matches the UB OK button, so a stuck UB save gets OK clicked a second time.
- **Fix:** Add `_wait_form_closed()` that polls `any_claim_form_open()`, and use it in both functions. Pick the OK button per screen type (Dental refused), click once, never use a cross-screen fallback, and return False if the form is still up. Add fixture tests with a no-op OK and a no-op Cancel.
**P6 · There is no unattended mode: the Dental box cannot be turned off, the end-of-run Enter prompt can hang or crash, and exit codes hide failures.** Severity: high for the Dental box, medium for the rest. Effort: M
`flows/export_medcor_ready.py:61-62, 375, 556, 578-581, 604-609, 693-697, 716-718` · `settings.py:311-439` · `core/msgbox.py:83-101`
- **Problem:**
- The Dental box costs 180 s per Dental claim even with every review flag off.
- Seven prompt flags must each be set to 0, and `ECW_KEEP_OPEN` is not in `.env.example`.
- With no stdin, `input()` raises RuntimeError, so a successful run exits 1, which means "login failed".
- No export, an unreadable sheet, or every claim failing all exit 0.
- **Fix:** Add one `RUN_MODE=attended|unattended` setting that switches all boxes and KEEP_OPEN together. Gate the Dental box (see D9). Catch `(EOFError, RuntimeError, OSError)` and skip the prompt when there is no TTY. Add distinct exit codes for "nothing processed" and "some claims errored", and update the README exit-code table.
**P7 · Write-adjacent buttons are clicked by numbered ids with no check of what they do.** Severity: medium. Effort: S
`selectors/claim_form.py:47-49, 55, 275` · `pages/claim_form.py:505-508, 1574` · `pages/sliding_fee.py:174-186`
- **Problem:** The UB Cancel/OK/X (`#UBClaimBtn17/18/3`) and Don't Save (`#savePrompt-tplBtn4`) are found only by their numbered id. If an eCW template renumbering moved those ids, Cancel would become a silent `saveAllData(true)`, and Don't Save would become Save, on a read-only check.
- **Fix:** Anchor on the handler instead:
- Cancel: `[ng-click*="closeUBClaimScreen"]`.
- Don't Save: `button[ng-click="saveData(2)"]` plus a check of its text. Do not scope it to `#savePrompt`, which is not in the real DOM.
- Before clicking, refuse any close target whose ng-click contains `saveAllData`.
- Add fixture ng-clicks and a swapped-id negative test.
**P8 · `visible(timeout=…)` never waits, so every caller that expects a grace period gets an instant check.** Severity: medium. Effort: S
`core/page_utils.py:43-47` · callers: `pages/claim_form.py:372, 436, 468, 589, 618` · `pages/progress_note.py:189` · `pages/sliding_fee.py:133-135` (already has a working `_appears`)
- **Problem:** Playwright 1.62 ignores `is_visible(timeout)`. As a result:
- a slow CPT tab gets read short;
- a slow Prog. Notes button reads as "absent";
- `.first` reports False when a hidden leftover element comes first.
- **Fix:**
- Change `visible()` to `filter(visible=True).count() > 0`.
- Add `appears(page, sel, timeout)` using `wait_for` and move only the callers that really mean "wait" onto it.
- Keep `is_ub_claim_form` and `is_dental_claim_form` as instant checks; a wait there would add 5–10 s to every claim.
**P9 · A bad `payer_groups.json` silently switches off 3 conditions for the whole run.** Severity: medium. Effort: S to validate at startup, M to centralize
`conditions/telehealth.py:88-91` · `conditions/medicare_telehealth.py:48-51` · `conditions/vaccine_admin_codes.py:312-316` · `core/payer_groups.py:27` · `core/charge_summary.py:179` · `settings.py:442-456`
- **Problem:** With 301 payers waiting to be added by hand, one trailing comma means telehealth, medicare_telehealth and vaccine_admin_codes return "not covered" on every claim, the log blames "unclassified" payers, and the summary write fails. Test mode skips the only warning box.
- **Fix:** Load the mapping in `validate()` and exit 2 if it is invalid, before login. Make the log messages say "mapping unavailable" rather than "unclassified". Centralizing the lookup is St6.
**P10 · TLS certificate checks are off for the session that types the password, opens the 2FA link and loads PHI.** Severity: medium. Effort: S
`core/browser.py:53-57` · `auth/mailbox.py:102-103`
- **Problem:** `ignore_https_errors=True` on the only browser context means a man-in-the-middle at or after the HTTP proxy is accepted silently. `tools/check_net.py` already passes with the default Windows certificate store, so the flag is not needed.
- **Fix:** Remove the flag, or put it behind `IGNORE_HTTPS_ERRORS` (default 0) with a loud warning. Accept the 2FA link only over https and only on an allow-listed host.
**P11 · Full clinical notes, sliding-fee screens and patient screenshots pile up with no minimisation and no deletion.** Severity: medium. Effort: M
`pages/progress_note.py:106-130` · `pages/claim_form.py:659, 664, 1601-1619` · `pages/sliding_fee.py:206` · `auth/login.py:197` · `core/page_utils.py:33-40` · `auth/mailbox.py:276` · `pages/dental_notes.py:56-59`
- **Problem:**
- Each claim's JSON stores the whole note: `text`, `sections` and `patient_header`.
- A success screenshot is taken on every claim, about 230–350 MB per full run, with no claim id in the name.
- Nothing is ever purged, the unit tests write into the same production folder, and the OTP code is printed.
- **Fix:**
- Save a redacted copy of the JSON (keep the in-memory `text`, which the conditions need).
- Add `DEBUG_SCREENSHOTS` (default 0) and put the claim id in failure-shot tags.
- Purge old Screenshots and Logs at start.
- Point `cfg.SCREENSHOTS` at a temp folder in tests.
- Stop logging the OTP value and the Billing Notes text.
- Ops: BitLocker plus a restricted folder ACL.
**P12 · Files likely holding PHI in the repo root are not gitignored.** Severity: medium. Effort: S
`.gitignore:1-35`
- **Problem:** `Dom.txt` (a live claim DOM capture), the two client workbooks and `~$…xlsx` are untracked but not ignored, sitting next to the uncommitted charge-summary work. One `git add -A` puts them in history.
- **Fix:** Move them to `Reference/` and update the comments that point to them. Add `Reference/`, `Dom*.txt`, `*.xlsx` and `~$*` to `.gitignore`. Stage files by name, and ship releases from `git archive`, not from a zip of the working folder.
**P13 · Every native `confirm()` from eCW is accepted automatically.** Severity: low. Effort: S
`core/page_utils.py:144-149`
- **Problem:** An eCW "save anyway?" or "save changes?" prompt on OK, Cancel or X would be answered Yes with no operator involved. None have been seen in 138 logs.
- **Fix:** Accept only `alert`. Dismiss `confirm` and `prompt` unless the message matches an allow-list, record what was seen per claim, and flag that claim Review.
**P14 · Claim No is cleared only at the end of a successful run, never before the bulk Lookup.** Severity: low. Effort: S
`flows/export_medcor_ready.py:540, 563-581` · `pages/claim_results.py:30-57`
- **Problem:** If eCW keeps the last Claim No (unproven), an aborted run would shrink the next export to 1 row. The confirm box does not show Claim No.
- **Fix:** Call `clear_claim_no` before `run_lookup` and raise if it fails. Add Claim No to `_filter_summary`, and warn when Total Counts is 0 or 1.
**P15 · The README, CHANGELOG and docstrings describe an older, read-only bot.** Severity: medium. Effort: S
`README.md:3, 60-89, 97-140, 384-386` · `CHANGELOG.md:299-304` · `pages/claim_form.py:2-7` · `conditions/vaccine_admin_codes.py:342-346` · `settings.py:360-361, 385` · `flows/export_medcor_ready.py:13, 351-352, 384` · `pages/dental_notes.py:9-11` · `pages/sliding_fee.py:10-11`
- **Problem:** The README says the form "is always closed with Cancel", while the code ships with writes on. The docs omit the 8 conditions, Dental, HCFA and the Charge Summary, and the CHANGELOG still lists telehealth as POS 11 on every row.
- **Fix:** Rewrite the Overview, Layout, Tests and claim-screen sections, including the write gates. Add one CHANGELOG entry per feature, and correct the stale comments.
**P16 · Nobody has proved that verification mode is really read-only.** Severity: low (unverified). Effort: S
`selectors/claim_form.py:204` (`addClaimFSCPTCode('save');addCptClaimEdit()`) · checkbox `updateBillToIns()`
- **Problem:** If these handlers save on the eCW server before OK is pressed, closing with Cancel does not undo an added G2025 row or a checkbox change, so `SAVE_CORRECTIONS=0` would not protect live claims, the demo included.
- **Fix:** On one test claim, add a row and toggle a checkbox, Cancel, reopen, and confirm that nothing persisted. Do this before the demo.
---
## 3. Logic improvements
**L1 · depression_dx_pointer can strip a billed line's only diagnosis pointer.** Severity: high. Effort: S
`pages/claim_form.py:860-864` · `conditions/depression_dx_pointer.py:108-130` · `tests/test_telehealth_fix.py:186-189`
- **Problem:** A checked 96127, 99213 or 90791 whose only pointer is Z13.31 ends up with ICD1–4 blank. That breaks the 837P SV107 rule and gets the claim rejected; this was reproduced on the HCFA fixture.
- **Fix:** If removing the pointer would leave nothing, leave the row alone and flag "only diagnosis is Z13.31 — needs review". Ask the client whether the primary pointer should be substituted instead, and update the pinned test.
**L2 · telehealth's "first real row" ignores whether the line is billed or excluded.** Severity: medium. Effort: S
`conditions/telehealth.py:131-132` · `conditions/medicare_telehealth.py:73-74` · `pages/claim_form.py:786-808`
- **Problem:** With BHT3 in row 1 (reproduced), POS 2/GT goes onto BHT3. dummy_codes then excludes that row, and the real billed service stays POS 11 with no modifier.
- **Fix:** Choose the first row that is checked, priced, and not in one shared exclusion set (BHT3/4, J2001/J2930, G8510/G8431, admin codes). If no row qualifies, decline with the reason. Add fixture tests.
**L3 · Exclusion conditions say "excluded" when no Assign-To-Patient checkbox was found (UB screen).** Severity: medium (plausible). Effort: S
`pages/claim_form.py:1443-1456` · `conditions/dummy_codes.py:54` · `conditions/j2930_j2001_update.py:62` · `selectors/claim_form.py:164-172`
- **Problem:** If the UB grid lacks `[ng-click*="updateBillToIns("]`, the fee is zeroed but the line stays checked and billed at $0. On those claims `billed=None`, so zero_fee_uncheck and priced_line_check never run.
- **Fix:** Spend two minutes in DevTools on one live UB claim to settle whether the selector exists. Record `checkbox_found`, and have `describe()` print "fee zeroed, NOT unchecked" when it is False. Flag the claim Review when every row has `billed=None`.
**L4 · A Sliding Fee screen that never fills in is reported as "NONE: no sliding fee dates assigned".** Severity: medium. Effort: S
`pages/sliding_fee.py:104-116, 143-153, 198-201`
- **Problem:** `_wait_filled` returns `{}` on a timeout or null, and the button's enabled state is checked once after 1 s. A slow modal therefore becomes a stated fact ("NONE") and a Pass (reproduced, 15.3 s).
- **Fix:** Return `{"error": …}` on a timeout or null, which makes the verdict unknown and therefore Review under D2. Poll the button's enabled state for about 5 s, and add a fixture test with `values=None`.
**L5 · The high-risk regex flags negations ("not high risk", "High Risk: No") and matches inside words.** Severity: low (plausible). Effort: S
`pages/dental_notes.py:31, 42-60` · `core/charge_summary.py:95-98`
- **Problem:** Any substring match routes the claim to the DentaQuest portal as Fail. The summary drops the matched text, so no reviewer can check it.
- **Fix:**
- Confirm with billing the exact marker they type.
- Add word boundaries and treat a negated match as "CHECK".
- Put the matched text in Comments, and add negation tests.
---
## 4. Structure changes
| # | Change | Where | Cost | Benefit |
|---|---|---|---|---|
| St1 | **One outcome model per claim.** `ConditionResult.status` + `reason`; `run_all` returns a RunReport (changed / declined / failed / tainted) that drives the box, the save gate, the summary and the log. | `conditions/base.py`, `flows/export_medcor_ready.py:426-442`, `core/charge_summary.py:71-116` | M | Implements D2, P2 and D6 together. Removes "none matched" lies and fixed-string comments. |
| St2 | **Run journal.** Per-claim JSONL checkpoint; summary built from the journal inside try/finally; resume; per-run snapshots. | `flows/export_medcor_ready.py:468-540` | M | Implements P3 and P4. A crash costs about 1 claim instead of the whole run; gives an audit trail of saves. |
| St3 | **Config profile.** `RUN_MODE` plus strict bool/int parsing plus a startup mode banner. | `settings.py:46-80, 311-439` | S–M | Implements P1 and P6. One switch replaces 7 flags; typos cannot silently enable writes. |
| St4 | **Production approval model** (decide before go-live): (a) `AUTO_SAVE_CONDITIONS` allow-list of client-signed conditions, or (b) two passes: read-only pass → client approves the summary → save pass on the approved ids, with no prompts. | `flows/export_medcor_ready.py:295-338` | M + client sign-off | Without it, about 270–570 corrected claims each need a human click, and every timeout means "not saved". |
| St5 | **Split claim_form.py (1,666 lines).** One `real_cpt_rows()` generator replaces 9 copied row loops; fee and telehealth rules move to a DOM-free module; rewrite the "read-only" docstring. | `pages/claim_form.py:2-7, 787-795 … 1430-1437` | L (docstring S) | One place to fix if eCW changes the grid. Do it after the demo; fix the docstring and do P16 now. |
| St6 | **Resolve payer and group once per claim** (report PAYER first, form name as fallback, log mismatches); conditions read `data["payer_group"]`. | 3 copies in P9 + `core/charge_summary.py:154` | M | The summary's Group Name and the condition decisions agree; removes 3 per-claim disk reads. |
| St7 | **Test runner.** `tests/run_all.py` (subprocess per file, totals, non-zero exit); AST-based layer check (catches the `core/charge_summary.py:174` violation and `from .. import X`); `check_root` skips when there is no .env. | `tests/test_settings.py:33, 53-56, 129-136`, `README.md:60-89` | S | One pre-demo/pre-deploy gate (397 cases, about 4.5 min) instead of 7 of the 28 files. |
---
## 5. Performance
**Expected time per claim (UB/HCFA, boxes off).** About 13–15 s of real work: search 3–8 s, open 1–2 s, CPT tab about 1.5 s, extract under 0.5 s, Prog. Notes about 4.5 s, close about 1 s.
- **Today:** about 43–45 s whenever the click fallback hits (43 of 92 opens, up to 15/15 in one run).
- **Dental:** the sliding-fee check takes 10–16 s; claim-to-claim cycles were seen at 59–61 s.
- **Not found:** 180 s or more each.
- **Boxes:** each unanswered box adds up to 180 s.
**Full run** (about 540 unique ids, with some queues of 1,145 or more):
- after the cuts below: about 2.3–4.8 h;
- today: about 4.5–14 h, before box time.
| # | Cut | Where | Saving | Effort |
|---|---|---|---|---|
| Pf1 | Click timeout 30 s → 5 s with a real wait (= D11, known) | `pages/claim_form.py:349-376` | about 15–30 s per claim | S |
| Pf2 | A not-found claim exits early once Total Counts reads a stable "0", and the single-claim search gets its own `CLAIM_SEARCH_TIMEOUT_MS` | `pages/claim_results.py:249-272`, `settings.py:263` | about 165 s per missed claim; removes the silent 3-min "hang" | S |
| Pf3 | Progress-note poll instead of a 3 s sleep (= D8); drop the success screenshot (= P11) | `pages/claim_form.py:631, 664` | about 2–3 s per claim plus fewer false Cleans | S |
| Pf4 | Single-claim search polls every 500 ms (`SEARCH_POLL_MS`), accepting only after the loader was seen and cleared; the bulk lookup keeps 3 s | `pages/claim_results.py:254, 262`, `settings.py:264` | about 1.5–3 s per claim; stops 0 s stale-grid matches | S |
| Pf5 | Gate the Dental box and add unattended mode (= P6, D9) | `flows/export_medcor_ready.py:375` | up to 180 s per Dental claim unattended | S |
| Pf6 | Circuit breaker (= P3) | `flows/export_medcor_ready.py:478-484` | caps a session-loss cascade (about 300 s × remaining claims) | M |
| Pf7 | Close leaked note tabs in a `finally`; soak test of 150+ claims while logging `context.pages`, DOM size and heap; reset the page every N claims only if the soak test shows growth | `pages/claim_form.py:629-662` | Unknown. No run has gone past 17 claims, so long-run behaviour is untested. | M |
---
## 6. Known items still open
- **SAVE_CORRECTIONS=0 verification mode** (known). New: the code default and the template still enable writes (see P1).
- **G2025 / admin code priced at 0.00 after being added** (known). Still waiting on an operator ruling.
- **medicare_telehealth leaves other rows when G2025 is unconfirmed; priced_line_check may then check them** (known). New: the failure is also hidden from the box and the summary (see D6).
- **"Click reported failure, but the form is already open"** (known). New: it costs about 30 s per hit, and its fallback check does not wait (see D11).
- **301 unmapped payers; AARP Medicare Advantage grouping disputed** (known). New: telehealth writes 93 for these payers (D5), declines are reported Clean (D2), and a JSON typo disables 3 conditions (P9).
- **Condition #9 (Medicaid D codes) removed** (known). Nothing new.
---
## Recommended order
1. **Today, before any commit:** gitignore and move the PHI files (P12). Default writes to off, strip inline comments, add the mode banner, and make Cancel the default button (P1). Run the Cancel-really-discards check on one test claim (P16).
2. **Demo setup:** clear or replace `TEST_CLAIM_IDS_FILE`, add the `_TEST` filename, check the demo claims are still in the queue (D1), and add the unique-name fallback for the summary write (D10).
3. **What the client sees on screen:** Dental summary box, "and N more", Yes/No, one box per claim (D9). Do the 2-minute UB checkbox selector check (L3).
4. **Telehealth correctness:** skip rows that are already correct (D3), decline unmapped payers (D5), classify audio/video from structured fields (D4), and report a failed G2025 (D6).
5. **Read completeness:** HCFA/Dental data wait (D7), progress-note polling (D8), fixed `visible()`/`appears()` (P8), click timeout cut (D11).
6. **Truthful summary:** minimal `claim_outcome` "Review" mapping before the demo (D2). Then the full St1 outcome model with the taint-blocks-save rule (P2).
7. **Write safety for production:** close/save success by `any_claim_form_open()` with no cross-screen OK (P5), handler-anchored selectors (P7), dismiss confirms by default (P13), depression pointer guard (L1).
8. **Run resilience:** JSONL journal plus try/finally summary plus resume (P4/St2), recovery plus error≠not-found plus circuit breaker (P3), early not-found exit (Pf2).
9. **Production operating model:** `RUN_MODE` unattended plus exit codes (P6/St3), approval-model decision with the client (St4), validate `payer_groups.json` at startup (P9), 150-claim soak test (Pf7).
10. **Hardening and hygiene:** remove the TLS flag (P10), PHI minimisation and retention (P11), L2/L4/L5 logic fixes, docs (P15), `tests/run_all.py` (St7). Split claim_form.py (St5) after the demo.