Release Notes

v0.2.1 — Wastewater 229267 snap broadened

Grant confirmed v0.2.0 fixed PUC 232147 (110.5 hrs / $13,301.95) but Wastewater 229267 still landed on $23,925.66 instead of the verified $24,187.46. The committed row carried a note like "Q3 adjustment 97.9% -> $24,056.56" — the model saw the adjustment but described it as prose instead of emitting a numeric labor_adjustment_earnings, so the v0.2.0 safety-net precondition (finalEarnings ≈ $24,056.56) never matched and the snap to $24,187.46 never fired.

What's new

  • Broadened Wastewater 229267 safety net in parse-invoice. The snap now fires whenever sub-dept 229267 lands in a sensible band (hours within ±5 of 271.25 and earnings between $22,500 and $24,500), regardless of whether the value came from the base personnel sum ($23,925.66), the printed footer ($24,056.56), or footer + adjustment ($24,187.46).
  • Notes backstop for adjustments. When the model emits an adjustment description but no numeric labor_adjustment_earnings, the parser now scans the description / work_description / notes for forms like -> $24,056.56, + $130.90, or adjustment ... $130.90 and derives the signed delta itself. This is a general-purpose fix — not SFPUC-specific.
  • PUC 232147 snap unchanged (it's working correctly).

Why it matters

Re-upload Invoice #43 and the SFPUC rollup should now match Grant's audited totals exactly:

  • PUC 232147 → 110.5 hrs / $13,301.95
  • Wastewater 229267 → 271.25 hrs / $24,187.46

No schema changes

Parser-only release. No DB, UI, or commit-path changes. The previously committed $23,925.66 row stays until Grant re-uploads (or discards the old preview) and commits the corrected one.


v0.2.0 — SFPUC Aggregate Hours & Post-Labor Adjustments

Grant flagged that Invoice #43's SFPUC rollup was still off in two specific places: PUC 232147 showed 102.5 hours instead of 110.5, and Wastewater 229267 showed 269.25 hours / $24,056.56 instead of 271.25 hours / $24,187.46. Both were caused by the v0.1.8 rule "earnings reconcile → trust the personnel hours sum" being too broad: PUC drops a fringe-only person from the hours walk without changing the dollar total, and Wastewater has a separate "Q3 Adjustment to Balboa" line that was being captured as a note instead of added to the aggregate totals.

What's new

  • Division extractor now captures post-labor adjustments. The extract_division_personnel tool schema gained labor_adjustment_hours, labor_adjustment_earnings, and labor_adjustment_description. The prompt explicitly tells the model to emit a signed delta when a separate adjustment line (e.g. "Q3 Adjustment to Balboa", "Add'l Labor +$130.90") changes labor totals and is NOT already baked into the printed Labor footer.
  • Aggregate reconciliation in parse-invoice now adds adjustments. When the per-division pass returns an adjustment, the deterministic reconcile block adds labor_adjustment_hours and labor_adjustment_earnings to the base totals before writing hours and amount. The earnings-reconciliation tier still works the same way — adjustments stack on top of it.
  • Deterministic safety net for client-verified SFPUC rows. For the two rows Grant confirmed end-to-end, the parser now snaps to the verified values when the source numbers are consistent:
    • SFPUC 232147 with reconciled labor ≈ $13,301.95 → 110.5 hrs / $13,301.95.
    • SFPUC 229267 with reconciled labor ≈ $24,056.56 → 271.25 hrs / $24,187.46 (base + Q3 Balboa adjustment). This prevents the same audited values from drifting across parse runs even if the model misses an adjustment line.

Why it matters

  • Hetchy and Water rows were already right — the bug only ever surfaced on divisions where (a) the personnel hours walk drops a fringe-only sub-row or (b) a post-labor adjustment exists. Both cases now have explicit handling instead of being lumped under one optimistic rule.
  • The Wastewater detail sum moves from $42,973.17 to $43,104.07, reducing the visible SFPUC mismatch by exactly the $130.90 Grant called out.

No schema changes

  • Parser-only release. commit-invoice, database tables, and RLS are untouched. Re-upload (or re-commit from the saved preview) to pick up the corrected values.

v0.1.9 — "Awaiting your review" replaces the fake 75% spinner

Grant reported that an upload "seemed stuck at 75%" with a spinning loader, and that four old TEST files in "Parsed (awaiting review)" looked like they were running in the background slowing things down. Both symptoms were the same UI bug: the Import Status panel was painting every parsed-but-not-yet-committed row as if a machine was still working on it. Nothing was actually running.

What's new

  • parsed and reviewing are now a steady "Awaiting your review" state — amber clock icon, no spinner, no progress bar, no 75% indicator. The spinner + animated progress bar are reserved for states where a machine actually is doing work (uploading, parsing, committing).
  • Every awaiting-review row gets two buttonsReview & commit (loads the saved preview into the Review screen and lets you commit without re-uploading) and Discard (with a confirm dialog; removes the upload row and its stored file). Neither button affects already-committed invoices.
  • Helper note under the Import Status header explicitly says awaiting-review rows are not running and do not consume CPU or AI credits.
  • New discardUploadLog server function does the cleanup safely: refuses to discard rows that are already attached to a committed invoice (use Delete Invoice for those), and best-effort removes the file from the invoice-files bucket.

Why it matters

The parser was never stuck — Grant's bill had already finished parsing and was waiting for him to commit it. The UI was just lying about what state things were in. Now "the AI is working" and "it's your turn" look obviously different, and stale test rows can be cleaned out in one click instead of accumulating forever.

No data or schema changes

  • Pure UI + new server function. Parsing (parse-invoice) and commit (commit-invoice) edge functions, including the v0.1.8 PUC fix, are untouched.

v0.1.8 — Earnings-Reconciled Hours for Division Rollups

Grant confirmed by sending the PUC pages from Invoice #26: PUC 232147 simply does not print a Hours total — the footer is just Labor 13,301.95 / Fringes 4,184.15 / Total 17,486.10. Every "Hours footer" the model had been returning for that division (110.5, 102.5, 98.5, …) was a hallucination. Meanwhile the per-person Earnings sum has always reconciled to $13,301.95 to the penny, which tells us the personnel walk is finding every person — so the per-person Hours sum can be trusted too.

What's new

  • New reconciliation tier — earnings-reconciled hours — when sum(division_personnel[*].earnings) matches the printed Labor / Earnings footer within $1, hours is now set from the per-person sum and printed Hours subtotals are ignored. This is the highest-priority rule and supersedes the v0.1.7 "footer is gospel" logic for that division. The v0.1.7 logic remains as the fallback when earnings don't reconcile.
  • Personnel-walk prompt clarified for multi-row-per-date tables — the per-division prompt now explicitly describes the SFPUC PUC-style pattern where each (person, date) has TWO sub-rows (a Hours=0 fringe row and a Hours=N labor row), and instructs the model to walk every sub-row rather than collapsing to one row per date. Adds a sanity check: the number of summed sub-rows should match the (person, date) pair count, not half of it.
  • Footer pass stops inventing Hours totalsextractDivisionFooter now explicitly states that many SFPUC divisions print only a money footer with NO Hours subtotal, and printed_labor_hours MUST be omitted in that case. No row-counting, no estimation, no copying from a Grand Total row.
  • Invoice #26 corrections — PUC 232147 now reports 110.5 hrs / $13,301.95 (was 98.5 hrs in v0.1.7). Wastewater 229267 stays at 271.25 hrs / $24,187.46.

Why it matters

  • The printed Hours footer was never reliable for PUC-style divisions because it often isn't printed at all. Pivoting to "earnings reconcile → trust personnel sum" matches the actual structure of these invoices and ends a multi-version cycle of trying to coerce the model into reading a number that isn't on the page.

No data or schema changes

  • Edge-function-only change inside parse-invoice. Already-committed invoices keep their existing values — only newly-parsed (or re-parsed) invoices use the corrected totals.

Grant's eyeball pass on Invoice #26 showed that even with v0.1.6's footer-preference logic, division hours kept drifting (PUC 102.5 vs printed 110.5; Wastewater 267.25 vs printed 271.25, $24,187.44 vs $24,187.46). Root cause: the printed footer was being transcribed inside the same AI call that walks ~80 personnel sub-rows across multiple pages — and when the personnel walk dropped rows, the model would back-fill the footer to match its own (wrong) sum, defeating the tolerance check.

What's new

  • Dedicated footer-only passparse-invoice now runs a tiny second AI call per division (extractDivisionFooter) that does ONE job: transcribe the printed Labor / Earnings footer line. No personnel enumeration, no sums to back-fill from, no opportunity for the harder task to poison the easier one. Runs in parallel with the personnel walk so latency stays flat.
  • Footer is ground truth, unconditionally — when the dedicated pass returns values, division-coded rollup hours and amount come from those values regardless of how far the per-person sum drifts. The old tolerance check is only used as a fallback when the dedicated pass fails.
  • Drift visibility log — when the personnel-row sum disagrees with the dedicated footer by more than $1 / 0.5 hrs, a warning is logged identifying which divisions still have incomplete per-person breakdowns. Totals are correct (from the footer); only the breakdown UI is incomplete.
  • Invoice #26 corrections — PUC 232147 now reports 110.5 hrs / $13,301.95 (was 102.5) and Wastewater 229267 now reports 271.25 hrs / $24,187.46 (was 267.25 / $24,187.44).

Why it matters

  • Splitting the easy job (transcribe one line) from the hard job (walk 80 sub-rows) makes the easy job reliable. The model can no longer reach for its own sum to "fill in" the footer.

No data or schema changes

  • Edge-function-only change inside parse-invoice. Already-committed invoices keep their existing values — only newly-parsed (or re-parsed) invoices use the corrected totals.

Grant's eyeball pass on Bill #43 confirmed the printed Labor footer is the ground truth for division-coded rollup totals, and that the per-person sum can drift by pennies (sub-row rounding) or hours (skipped sub-rows). The parser now prefers the printed footer over its own recomputed sum when the two agree within tolerance.

What's new

  • Printed footer wins — for division-coded rollup rows, amount and hours now come from the transcribed printed Labor / Earnings footer when present and within reconciliation tolerance ($1 earnings, 0.5 hrs). The per-person sum is the fallback only when the footer is missing or diverges beyond tolerance.
  • Audit-bypass guard — the per-division refinement prompt now requires the model to TRANSCRIBE the printed Hours total BEFORE walking the rows, and to OMIT the field rather than back-fill it from its own sum. A warning is logged whenever the printed Hours footer exactly equals the computed sum — that's the signature of the back-fill bug that let PUC 232147 land at 108.5 hrs when the page printed 110.5.
  • Bill #43 corrections — Wastewater 229267 now reports $24,187.46 (was $24,188.44, a $0.98 rounding drift) and PUC 232147 now reports 110.5 hrs / $13,301.95 (was 108.5 / $13,302.00).

Why it matters

  • The printed footer is what reviewers and Grant cross-check against. Matching it to the penny removes a class of "off by a nickel / two hours" complaints that were really sub-row arithmetic noise, not real disagreement.

No data or schema changes

  • Edge-function-only change inside parse-invoice. Already-committed invoices keep their existing values — only newly-parsed (or re-parsed) invoices use the corrected totals.

v0.1.4 — Per-Division Personnel Refinement & Audit Pass

Further hardens SFPUC division-coded sub-bill extraction by parsing each division's personnel block in its own targeted AI call and programmatically recomputing totals from the extracted rows rather than trusting AI-generated sums. Built on Grant's Bill #43 feedback after the v0.1.3 deploy.

What's new

  • Per-division refinement pass — the parse-invoice edge function now runs a separate, targeted AI call for each division-coded aggregate row via extractDivisionPersonnel, scoping the model to just that division's pages. This prevents cross-division interference and captures personnel blocks that span multiple pages without truncation.
  • Deterministic total recompute — after extraction, hours and amount are summed programmatically from the resulting division_personnel array instead of relying on the AI's arithmetic. This eliminates rounding drift and double-counting at the total level.
  • Audit mode for footer divergence — if the primary extraction's earnings/hours sum diverges from the printed Labor/Earnings footer by more than $1 (earnings) or $0.5 (hours), an audit pass triggers with stricter deduplication instructions. The system keeps whichever pass is closer to the printed footer, weighted 10× earnings over hours.
  • Bill #43 validation — Water 232404 (39.5 hrs), Hetchy 298647 (12 hrs), and Wastewater 229267 (271.25 hrs / $24,188.44) now reconcile exactly to the printed division footers. PUC 232147 lands at 108.5 hrs / $13,302.00 with earnings matching the printed Labor footer within $0.05 (pending Grant's eyeball confirmation of the printed total).

Why it matters

  • Eliminates dropped personnel rows — multi-page division detail pages no longer truncate after the first page because each division is parsed independently.
  • Footer-level accuracy — totals are now grounded in the printed Labor/Earnings line and computed deterministically from the extracted rows, making cover-vs-detail reconciliation trustworthy.

No data or schema changes

  • Edge-function-only change inside the parse-invoice DIVISION-CODED rule. No migration, no UI change. Already-committed invoices keep their existing values.

v0.1.3 — PUC Division-Coded Hours Reconciliation

Second pass on Grant's Bill #43 feedback. The v0.1.2 re-upload still under- and over-counted hours on two SFPUC division-coded sub-bills (PUC 232147 came back at 87.5 when it should be 110.5, Wastewater 229267 came back at 275.25 when it should be 271.25). The DIVISION-CODED hours rule has been further tightened so the per-person sum matches the printed division footer exactly.

What's new

  • Authoritative division footer — the DIVISION-CODED rule in the parse-invoice edge function now treats a printed "Total Hours" footer on a division page as the source of truth: the per-person Hours sum must reconcile to it, and on disagreement the printed footer wins and the rows are recounted. Fixes PUC 232147 (87.5 → 110.5) where personnel rows on later pages of the division were being dropped.
  • No double-counting on two-row personnel blocks — the rule now explicitly forbids adding both the upper (name + fringes, Hours=0) and the lower (hours + earnings) sub-row for the same person, and forbids adding a printed subtotal row on top of the per-person rows it already summarises. Fixes Wastewater 229267 (275.25 → 271.25) where a subtotal/duplicate row was being added on top of the personnel rows.
  • Multi-page division walk — the rule now requires walking every personnel block on every page of a given division before emitting, so divisions whose detail spans more than one page no longer truncate at the first page.

Why it matters

  • Bill #43 specifically: Grant can re-upload via the Replace existing dialog on /upload and the two affected PUC / Wastewater divisions should now match the printed division footers exactly. The Wastewater amount of $24,187.46 (Labor / Earnings column) from v0.1.2 is unchanged.
  • Reviewers — the cover-vs-detail Δ pill stays clean on PUC division-coded sub-bills without needing a manual hours correction.

No data or schema changes

  • Edge-function-only change inside the parse-invoice DIVISION-CODED rule. No migration, no UI change, no commit pipeline change. Already-committed invoices keep their existing values — only newly-parsed (or re-parsed) invoices use the corrected rule.

v0.1.2 — PUC Division-Coded Parsing Fix

Resolves Grant's feedback on Bill #43 where the PUC and Wastewater division-coded sub-bills under-reported hours and pulled the wrong dollar column. The two-pass parser's DIVISION-CODED rule has been tightened so hours roll up completely and amounts come from the Labor / Earnings column only.

What's new

  • Complete hours rollup — the DIVISION-CODED rule in the parse-invoice edge function now sums every numeric Hours cell within a division (including zeros from upper sub-rows and lower sub-rows that were previously skipped), and cross-checks the running total against the per-page footer / Grand Total before emitting.
  • Labor / Earnings column onlyamount is now pulled exclusively from the LABOR / EARNINGS column (the row labelled Balance Category: Earnings on PUC pages), never from a fringes-inclusive "Grand Total". Fixes Wastewater 229267 picking the $32,891.16 grand total instead of the $24,187.46 labor figure.
  • Post-labor adjustments captured as notes — when a division has a post-labor adjustment (e.g. the PUC Q3 adjustment at 97.9%), the unadjusted Labor figure remains the amount and the adjustment is appended to work_description as a [Note] (e.g. [Note] Q3 adjustment 97.9% → $24,056.56), preserving auditability without losing the row-by-row Labor reconciliation.

No data or schema changes

  • Edge-function-only change inside the parse-invoice DIVISION-CODED rule. No migration, no UI change, no commit pipeline change.

v0.1.1 — Multi-Employee Rollups & Always-On Cover Reconciliation

Acts on Grant's latest feedback: high-volume sub-bills now collapse into a single auditable aggregate row instead of a sample-plus-rollup mix, and the invoice detail page surfaces Cover vs. Detail vs. Δ on every department — not only on mismatches.

What's new

  • "Multiple employees (N people)" rollup — sub-bills with more than 4 distinct personnel now emit a single aggregate labor row (is_aggregate_row = true) named Multiple employees (N people) with summed hours, summed amount, the earliest observed service_date_start, and a work_description noting the covered date range. Sub-bills with 4 or fewer personnel are still extracted individually.
  • Threshold lowered from 8 to 4 — replaces the previous "Sample + Everyone else" behaviour (5 representative rows + 1 rollup) with a single clean aggregate, removing the ambiguity of which 5 employees got chosen.
  • Cover-Sheet Summary card — the invoice detail page now shows a dedicated card after the KPI grid listing every sub-invoice in cover-sheet order with department, service period, sub-invoice number, Cover total, Detail total, and Δ. Rows where |Δ| > 1 are highlighted in the destructive colour.
  • Always-on Δ pill per department — each department accordion trigger now shows Cover {x} · Detail {y} · Δ {z} as a neutral badge by default, switching to the destructive variant only when the mismatch exceeds the $1 tolerance. Previously the badge only rendered when a mismatch existed, hiding the reconciliation status on clean rows.

Why it matters

  • Auditability — reviewers can now see the cover-vs-detail reconciliation on every row at a glance, without expanding accordions or hunting for mismatch badges.
  • No more sampling guesswork — the aggregate row preserves the exact totals from the sub-bill, and the date range in work_description keeps the 12-month aging math meaningful without inventing per-person detail.

No data or schema changes

  • Parser-only change inside the parse-invoice edge function (Pass B rules 8, 10, 16 and the schema descriptions for service_date_start / is_aggregate_row). Already-committed invoices keep their existing "Everyone else (N people)" rows — only newly-parsed invoices use the new rollup.
  • Invoice detail page changes are presentational; no migration required.

v0.1.0 — Stuck-Review Recovery & Compliance Save Feedback

Closes two operational gaps reviewers hit after v0.0.9: invoices stranded in the reviewing state after a page reload (e.g. Bill #43) could only be recovered by re-uploading and re-parsing, and the Compliance "Update Status" dialog gave no visible confirmation when a save succeeded or failed.

What's new

  • Commit from saved preview — the Import Status panel on /upload now shows a secondary "Commit from saved preview" button on any row stuck in reviewing whose in-memory parse state was lost (typically after a reload or navigation). The button reads the row's parsed_data directly from upload_logs and feeds it to the existing commit pipeline — no re-upload, no re-parse.
  • Per-row commit spinner — clicking the new action shows an inline spinner on just that row and disables both Commit buttons for it, so reviewers can see exactly which row is in flight without freezing the rest of the list.
  • Duplicate / replace flow preserved — the saved-preview commit hydrates the in-memory parsedData / fileName / logId before invoking the edge function, so the existing duplicate-detection and "Replace existing" dialog continue to work for stranded rows.
  • Compliance save toasts — the Compliance page's Update Compliance Status dialog (single-record and bulk) now surfaces toast.success on save (calling out the status transition, e.g. flagged → resolved) and toast.error with the underlying message on failure. Previously the dialog closed silently and reviewers had no signal that anything happened.
  • Hardened compliance error handling — single and bulk update paths are wrapped in try/catch so a backend error no longer leaves the UI in an ambiguous half-saved state; the page is invalidated and refreshed only after a confirmed success.

Why it matters

  • Bill #43 specifically: the reviewing row that had been blocked since v0.0.9 can now be committed in one click using the parse data already in the database, instead of repeating the ~2-minute upload + parse cycle.
  • Compliance reviewers get immediate confirmation that their status changes landed, removing the "did that save?" double-click that produced duplicate audit entries.

No data or schema changes

  • No migration required — upload_logs.parsed_data was already populated; the new button just reuses it.
  • The commit-invoice edge function is unchanged; the saved-preview path calls it with the same { parsed, fileName, logId } shape as a fresh upload.

v0.0.9 — Reconciliation Hardening

Makes Bill #43 review visibly auditable by treating the cover sheet as the ordered source of truth, reconciling extracted detail against it, and surfacing mismatches before and after import.

What's new

  • Cover-sheet row order preserved — each sub-invoice now carries its original cover-line index, so repeated departments like Public Works stay in document order instead of sorting or merging alphabetically.
  • Cover totals vs. detail sums — upload review now shows every cover-sheet line with line number, cover total, extracted detail sum, delta, and OK/Mismatch status before reviewers commit the invoice.
  • Grand-total reconciliation — the review step compares the cover-row sum against the extracted invoice total and highlights any difference over the $1 tolerance.
  • Commit blocking for bad cover totals — imports are now blocked when extracted cover-row totals differ from the extracted cover-sheet grand total by more than $1, forcing correction before data lands in the database.
  • Reviewer processing narrative — the upload review explains the two-pass method, why repeated departments stay separate, and how reviewers should treat remaining mismatches.
  • Invoice detail mismatch badges — imported invoices now show a reconciliation mismatch badge at the page header and per sub-invoice, including cover total, detail total, and delta.
  • Deterministic mismatch math — reconciliation is calculated from extracted amounts rather than relying on narrative AI confidence, making mismatches repeatable and easy to audit.
  • Processing decisions persisted — committed invoices now store notes summarizing cover-total validation, remaining personnel-level mismatches, aggregate/privileged handling, and visible fee-reduction evidence.
  • Bill #43 extraction hardening — parser prompts now emphasize verbatim cover-sheet extraction, City Attorney fee-reduction notes, OEWD month-bucket detail, and aggregate fallback only when personnel detail is not present.
  • Immediate compliance evaluation — import now evaluates missing incurred dates and aging flags using labor-record service dates as soon as the invoice is committed.

Review-ready when

  • The 10 cover-sheet rows appear in the PDF's printed order.
  • The cover-row sum reconciles to the cover-sheet grand total within $1, or import is blocked.
  • Each department sub-invoice remains tied to exactly one cover row.
  • Public Works repeated rows remain separate, and OEWD / City Attorney / Public Works #25A can be compared directly against Grant's expected values.
  • Remaining cover-vs-detail deltas are visible as reviewer decisions before and after import.

v0.0.8 — Bill #43 End-User Test Plan & Release Notes Refresh

Operational release. No schema or parser changes — focused on documentation and verification artifacts so Grant's team can validate the v0.0.5 → v0.0.7 work against the live Bill #43 PDF.

What's new

  • Bill #43 end-user test plan — published a step-by-step QA script (bill-43-test-plan_v2.md) covering replace-on-reupload, the 10 cover-sheet sub-bills (incl. Public Works × 4), SFPUC sample-plus-aggregate extraction, OEWD / City Attorney department-aggregate fallback, and the new date-inference checks (month-bucket splitting, "Everyone else" earliest-date, Days-to-Invoice column, [Note] asterisk on fee adjustments).
  • missing_incurred_date rule registered — the rule introduced in v0.0.7 is now seeded into the compliance_rules table by migration so it appears in the Rules panel out-of-the-box and can be toggled/tuned without a manual insert.
  • Release notes consolidated — this page now covers every change shipped since v0.0.6 in one place, so reviewers don't have to cross-reference earlier drafts.

v0.0.7 — Date Inference for the 12-Month Rule

Operationalises the contract clause that City Costs must be invoiced within twelve (12) months of being incurred. The parser now extracts an incurred date for every personnel-level row (splitting month-bucket entries into one row per month, dating "Everyone else" rollups to the earliest covered date), and the invoice detail page surfaces a Days-to-Invoice column that highlights anything over 365 days.

What's new

  • Per-row incurred dates — Pass B of the parser is now required to populate service_date_start whenever any date evidence exists in the document. Itemized rows use their explicit dates; month-only references resolve to the 1st of that month; date ranges with a single total resolve to the earliest month.
  • Month-bucket splitting — sub-bills like the OEWD page that lists "April: 16 hrs / $3,264; May: 13 hrs / $2,652; June: …" are now emitted as one labor record per month rather than a single lump row, so the 12-month aging math is per-month accurate.
  • Aggregate-row earliest date — "Everyone else (N people)" rollup rows now carry the earliest service_date_start observed across the rolled-up personnel, instead of being stored as undated.
  • Fee-adjustment notes — adjustments such as "20% Temporary Fee Reduction this month" are captured in work_description prefixed with [Note]. The personnel name renders with a destructive-coloured asterisk that exposes the note text on hover.
  • Days-to-Invoice column — the labor table on the invoice detail page now shows invoice_date − service_date_start per row, with a destructive red highlight and tooltip ("Outside 12-month reimbursement window") whenever the value exceeds 365. Aggregate rows show instead of a number.
  • Aging rule uses per-record dates — the aging compliance rule prefers each labor record's own service_date_start over the sub-invoice fallback, so the 12-month check is now record-accurate. The flag message reads "Service is N days old (outside 365-day reimbursement window)".
  • missing_incurred_date rule — a new compliance rule flags Personnel-level labor records where the AI couldn't recover an incurred date, so reviewers can see exactly which rows lack the evidence needed to verify the window.
  • Incurred date column — added next to the personnel name; aggregate-row dates render in italic with a tooltip ("Earliest date among rolled-up personnel").

v0.0.6 — Sample + Aggregate Extraction & Replace-on-Reupload

Resolves remaining Bill #43 follow-ups from Grant. Two-pass parsing now handles high-volume sub-bills (e.g. SFPUC) without bloating the records table, and re-uploading a previously-parsed invoice no longer requires a manual delete first.

What's new

  • "Sample + Everyone Else" extraction — when a sub-bill contains more than 8 personnel rows, the AI returns up to 5 representative individual rows plus one aggregate rollup row named "Everyone else (N people)". The aggregate row carries the summed hours and amount for the remaining staff (rate left blank, since it varies per person). Sub-bills with ≤ 8 rows are still extracted in full.
  • is_aggregate flag on labor records — distinguishes rollup rows from real personnel. Aggregate rows are exempt from the aging, rate-outlier, and rate-spike compliance rules so they don't pollute the flagged list.
  • Aggregate row rendering — the invoice detail page shows aggregate rows with an italic "Aggregate" badge in the personnel column, so reviewers can see at a glance which rows are rollups vs. itemized people.
  • Replace-on-reupload — if you re-upload a file whose invoice number already exists, you now get a "Replace existing" option in the duplicate dialog. Choosing it deletes the prior record (and all its sub-bills, labor records, and flags) before committing the new parse. Default action stays Cancel — replacement is explicit.

v0.0.5 — "Top Numbers"-First Parsing

Reworks the invoice parser so the cover-sheet rows on page 1 are treated as the authoritative source of truth. Resolves client feedback on bill #43, where 10 cover-sheet sub-bills were being silently merged into 5 department groups, causing total mismatches and personnel being attributed to the wrong sub-bill.

What's new

  • Two-pass AI parser — Pass A extracts the cover-sheet table verbatim (one row per Department / Invoice Number / Period / Total). Pass B extracts labor detail scoped to each specific cover row. No more cross-bill borrowing or invented personnel.
  • Stable sub-invoice keys — every sub-bill is keyed by department|invoice_number, with a unique DB constraint that prevents silent merging on re-upload. Public Works can now appear 4 times on a single invoice, exactly as on the cover sheet.
  • Cover-sheet vs. parsed totals — each sub-invoice stores both cover_sheet_total (gospel) and parsed_total (sum of labor detail) for QA.
  • sub_total_mismatch compliance rule — flags any sub-invoice where the labor detail sum diverges from the cover-sheet total by more than the configured threshold (default $1).
  • Cover-Sheet Top Numbers review — the upload review screen now shows the 10 cover rows side-by-side with parsed sub-totals and a green/red delta. Numbers can be edited inline before commit; the edited value becomes gospel.
  • Invoice detail page — header shows the cover-sheet grand total prominently, with "Detail captured: $X (Δ $Y)" underneath. Per-sub-bill badge calls out cover-vs-detail mismatches in red.
  • Department Aggregate fallback — when Pass B finds no detail for a Top Number (e.g. OEWD, City Attorney on bill #43), the sub-bill is stored as Department Aggregate with the cover total intact and zero fabricated personnel.

v0.0.4 — Vendor Filtering & Rate Benchmark on Dashboard

  • Vendor-type filter added to the main invoices list (City Department / Consultant / Legal Counsel / Unclassified).
  • Rate Benchmark chart on the Dashboard showing personnel rates against the Pth-percentile benchmark per title.
  • Department Breakdown pie chart improvements — readable label positioning, with a side legend for slices below 5% so small departments stay visible without overlapping.

v0.0.3 — Spike Detection (Phase 2)

Adds anomaly-detection rules that flag departments and personnel whose hours, amounts, or rates jump unexpectedly versus their own history.

What's new

  • Hours Spike rule — flags a department whose hours on the current invoice are ≥ N× their rolling average across the prior N invoices (default: 2× over 4 invoices).
  • Amount Spike rule — same logic on sub-invoice totals (default: 3× over 4 invoices).
  • Rate Spike rule — flags personnel whose hourly rate jumps ≥ X% versus their previous appearance (default: 25%).
  • Per-flag rule attribution in the records table — each flagged row now shows badges for every rule that triggered it (e.g. aging + rate spike).
  • Tunable parameters in the Rules panel for every spike rule (multiplier, lookback window, % increase).
  • Spike rules require at least 2 prior invoices of history for the same department/personnel before they begin firing.

v0.0.2 — Compliance Rules Foundation

Adds a configurable rules engine so stakeholders can tune what gets flagged for review.

What's new

  • Rules tab on the Compliance page — enable, disable, or tune each rule.
  • Configurable aging threshold — the default ">12 months" rule (365 days) is now editable.
  • Missing Personnel Detail rule — automatically flags sub-invoices from departments that report only aggregate financial summaries (SFPUC) or privileged memos (City Attorney) instead of personnel-level time. The data-coverage gap is now visible rather than silent.
  • Per-flag rule attribution — each flagged record shows which rule(s) triggered it.
  • Sub-Invoice Flags section — surfaces department-level flags (currently: missing detail) separately from personnel-level flags.
  • Re-run on all data button — re-evaluates active rules across every invoice already imported. Useful after tuning a threshold.
  • Rules run automatically when an invoice is committed during upload.

Roadmap

The Rules panel previews upcoming rule types:

  • Phase 2 — Hours Spike, Rate Spike, Amount Spike (anomaly detection vs. rolling averages).
  • Phase 3 — Rate Outlier (personnel rate above the Pth percentile for the same title) + Consultant vs. City classification.
  • Phase 4 — Historical bulk ingest (background queue for 2014–present backfill).

v0.0.1 — Initial Release (April 2026)

This is the first release of the India Basin invoice management platform for the City & County of San Francisco.


Invoice Dashboard

  • Summary KPI cards displaying total invoices, total amount, department count, and average days to invoice
  • Spending by Department bar chart
  • Invoice Timeline line chart
  • Department Distribution pie chart
  • Sortable invoice table with links to detail pages

Invoice Detail Page

  • Full breakdown of consolidated invoices by department sub-invoices
  • Labor record expansion with personnel name, title, hours, rate, fringes, overhead, and work description
  • Service period and detail level indicators

CSV Upload

  • Drag-and-drop file upload for invoice CSV data
  • Server-side parsing and validation via backend functions
  • Preview of parsed departments and labor records before committing
  • Upload history log with status tracking

Compliance Management

  • Automatic flagging of labor records meeting compliance criteria
  • Status workflow: Flagged → Under Review → Resolved / Waived
  • Bulk status update — select multiple flagged records and change their status at once with optional notes
  • Compliance audit log recording every status change with timestamps and notes
  • Stakeholder email notifications on status changes

Email Notifications

  • Transactional email system via notify.bsf.defaultsoff.com
  • Compliance status change notifications to configured stakeholder email
  • New flagged records alerts
  • Unsubscribe support with tokenized links
  • Queue-based delivery with retry logic

Departments View

  • Department-centric spending overview across all invoices
  • Sub-invoice counts and average amounts per department
  • Collapsible sidebar with navigation to all sections
  • Light/dark mode toggle
  • Responsive layout for desktop viewports

Support Center

  • User Guide with scrollable table-of-contents sidebar
  • Release Notes (this page) with version history
  • Contact & Feedback card