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>
34 KiB
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_FILEis 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 asCharge Summary_<date>.xlsx, which pushes a live same-day file aside to__HHMMSS. An unreadable sheet still exits 0, and withSHOW_CLAIM_IDS=0the operator gets no sign that this is a test run. - Fix: For the demo, either clear
TEST_CLAIM_IDS_FILEor point it atMedcor 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.
- write test runs to
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_outcomeknows only two states: something fired, or the claim is clean. All of the following fall through to CLEAN/Pass:- a condition's
detect()orapply()raised, becauserun_alldrops 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 (
openedis nothing, absent, disabled, or an error); - Dental
high_riskis 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".
- a condition's
-
Fix:
- Give
ConditionResultastatus(changed / no_change / declined / failed) and areason, and haverun_allreturn every condition that was detected or failed. - Store
ran,fired,problemsandnote_readindata["_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_outcomeso a missing_conditions, an unread note, and Dental None/unknown become Review. - Add tests for each of these paths.
- Give
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_modifieralways 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
nonewhen 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.bodycause 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 havedescribe()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.
- Add a
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_claimreturns 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).
- HCFA: wait until
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_unreadfor D2. - Optional: check that the note's DOS matches the claim's CPT SDOS.
- Poll
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.
- Fold the Dental verdict into the SHOW_EACH_CLAIM box instead of calling
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.xlsxinstead (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 bypassescloseClaimScreen(). - Fix: Use timeouts
(5_000, 15_000), replace line 372 withlocator.filter(visible=True).first.wait_for(timeout=5_000), and logstr(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_CORRECTIONSandAPPLY_CORRECTIONSboth default to True, and the template ships them as 1. Values likeSAVE_CORRECTIONS=0 # note, a blank value, orOall read as True;CLAIM_PROCESS_LIMIT=5 # demoreads 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 filebanner, and make write mode require OK/Cancel at start. - Add
MB_DEFBUTTON2to the save confirmation so Enter means Cancel.
- Default both settings to False and set them to 0 in
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, andsaveAllData()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_savethen 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_claimraises 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-runopen_claimsand 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.
- Close whenever
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
finallyclose, or a KeyboardInterrupt, skips it and also skipsclear_claim_no. - A re-run starts again at claim 1.
<id>.jsonis overwritten on every read.- With saving on, only condition names are logged, never the changed fields.
- The summary is written only after the last claim. A dead page in the
- 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_noalways 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.
- Append one flushed JSONL line per claim to
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_HCFAalso matches the UB OK button, so a stuck UB save gets OK clicked a second time. - Fix: Add
_wait_form_closed()that pollsany_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_OPENis 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|unattendedsetting 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 silentsaveAllData(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.
- Cancel:
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";
.firstreports False when a hidden leftover element comes first.
- Fix:
- Change
visible()tofilter(visible=True).count() > 0. - Add
appears(page, sel, timeout)usingwait_forand move only the callers that really mean "wait" onto it. - Keep
is_ub_claim_formandis_dental_claim_formas instant checks; a wait there would add 5–10 s to every claim.
- Change
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=Trueon the only browser context means a man-in-the-middle at or after the HTTP proxy is accepted silently.tools/check_net.pyalready 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,sectionsandpatient_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.
- Each claim's JSON stores the whole note:
- 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.SCREENSHOTSat a temp folder in tests. - Stop logging the OTP value and the Billing Notes text.
- Ops: BitLocker plus a restricted folder ACL.
- Save a redacted copy of the JSON (keep the in-memory
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~$…xlsxare untracked but not ignored, sitting next to the uncommitted charge-summary work. Onegit add -Aputs them in history. - Fix: Move them to
Reference/and update the comments that point to them. AddReference/,Dom*.txt,*.xlsxand~$*to.gitignore. Stage files by name, and ship releases fromgit 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. Dismissconfirmandpromptunless 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_nobeforerun_lookupand 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=0would 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 claimsbilled=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 havedescribe()print "fee zeroed, NOT unchecked" when it is False. Flag the claim Review when every row hasbilled=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_filledreturns{}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 withvalues=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
- 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).
- Demo setup: clear or replace
TEST_CLAIM_IDS_FILE, add the_TESTfilename, check the demo claims are still in the queue (D1), and add the unique-name fallback for the summary write (D10). - 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).
- 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).
- Read completeness: HCFA/Dental data wait (D7), progress-note polling (D8), fixed
visible()/appears()(P8), click timeout cut (D11). - Truthful summary: minimal
claim_outcome"Review" mapping before the demo (D2). Then the full St1 outcome model with the taint-blocks-save rule (P2). - 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). - 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).
- Production operating model:
RUN_MODEunattended plus exit codes (P6/St3), approval-model decision with the client (St4), validatepayer_groups.jsonat startup (P9), 150-claim soak test (Pf7). - 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.