COEO Billing (coeo-bill)
Builds/updates the monthly COEO (Huawei Cloud) billing workbook for Sivapol (CTO, Vonosis). This skill grows in phases as Sivapol approves each one — append new phases to this file, don't replace the working logic already validated.
Workflow order for a brand-new month: Phase 1 builds the raw Table1 data, Phase 2 tags Enterprise Project, THEN a single Finalize pass recalculates/validates/writes history once, THEN Phase 3 (when asked) produces the customer export. Earlier versions of this skill validated and wrote history right after Phase 1, then again after Phase 2 if tagging changed anything — Sivapol pointed out that's redundant double work since tagging often changes the numbers anyway (confirmed on a real month — see Status log), so the standard flow now runs Phase 2 immediately after building Table1 and only validates/writes history once, at the end, in Finalize. Multiple consecutive months can be run as one batch — chain each month off the previous month's just-built Partner file, run Phase 1→2→Finalize→Phase 3 for every month in the batch, then present one consolidated report and get a single confirmation before saving all of them (validated on a real 5-month batch — see Status log).
File & location
- Source of truth lives on Sivapol's computer (OneDrive), connected folder root:
.../COEO/00-Billing report/. - Per month subfolder:
YYYY-MM/(e.g.2026-08/). - Working file naming (as of Phase 1): new months are built as
YYYY-MM-Billing-COEO-Partner.xlsx(e.g.2026-09-Billing-COEO-Partner.xlsx) — this is the name Phase 1 gives the file it copies/builds, and the one Phase 2 should look for on any month built after this convention started. Months built before this change (e.g.2026-08) instead used the olderx-YYYY-MM-Billing-COEO_2.xlsxnaming — that file still exists and is still valid for that month; don't rename it retroactively unless Sivapol asks. When looking for "the working file" for a given month, check forYYYY-MM-Billing-COEO-Partner.xlsxfirst, fall back tox-YYYY-MM-Billing-COEO_2.xlsxif that's what exists, and ask Sivapol if neither is present and it's unclear which convention applies. - Phase 3 output naming:
YYYY-MM-Billing-COEO-Customer.xlsx, in the same month subfolder as the working file it was built from. - Other files that can appear in the same subfolder and are NOT the working file: a non-prefixed raw
.xlsx, a_BK.xlsxbackup copy, and the raw Huawei CSV exports (see below). The 2026-08 folder in particular has accumulated several stray files over testing (2026-08-Billing-COEO.xlsx— a raw pivot-only workbook, wrong structure;2026-08-Billing-COEO_2.xlsx— corrupted/not a valid zip) — the only valid template file for 2026-08 isx-2026-08-Billing-COEO_2.xlsx, confirmed by its sheet names (2026-08-Billing-COEO,Control Sheet,Project name,Project name-customer). When picking a template file, always open it and checkwb.sheetnamesmatches this expected set before trusting it — don't assume by filename alone. - Do the work with
device_bashdirectly on the connected folder (the files live on the user's computer, not the cloud container) — never stage/download unless a step genuinely needs the cloud workspace or a tool the local shell lacks. - Each month's raw Huawei billing exports land directly in that month's subfolder (Sivapol/Huawei console places them there — no separate "copy into folder" step needed). Two variants can appear:
pricefactor_bill_detail_..._0.csv— the full daily-granularity export (has aDatecolumn, many more rows). Not what Table1 is built from.- the monthly-cycle-aggregated export, one row per resource for the month, 57 columns matching Table1 columns 1–57 exactly (no
Datecolumn). This is the file Phase 1 uses. Its filename usually (but not always) has a_cyclesuffix — identify it by reading the CSV header and comparing to Table1's 57 column names, not by filename alone (a real month was found where the only CSV present had no_cyclesuffix in its name but was in fact the correct cycle-aggregated format). If a month's folder genuinely has only the daily/full export (has aDatecolumn, far more rows than resources), stop and tell Sivapol to export the cycle-aggregated version from the Huawei console — don't try to aggregate the full export yourself. - A CSV can be a OneDrive cloud-only placeholder that isn't actually downloaded to the device yet — reading it fails with an error like "Resource deadlock avoided". If that happens, don't keep retrying blindly; ask Sivapol to open the file locally once (Finder/Excel, or "Always keep on this device") to force the real download, then retry.
Workbook structure
- Main data sheet is named
YYYY-MM-Billing-COEOand contains an Excel structured table named Table1 (58 columns:A:BF, row count grows/shrinks monthly). - Table1 columns: 1–57 come straight from the Huawei cycle CSV (
Billing Cycle...Hourly Commitment (USD)). Column 58,Environment, is a formula column, not static data — every row carries the exact same formula (uses[#This Row]structured references, so the text is identical row to row):
Never delete or skip this formula on any row, old or newly added — Sivapol has flagged this explicitly.=IF(Table1[[#This Row],[Account Name]]="coeo-workload-prod","Production",IF(Table1[[#This Row],[Account Name]]="coeo-workload-nonprod","Non-Production",IF(Table1[[#This Row],[Account Name]]="coeo-network-share-prod","Share-Production",IF(Table1[[#This Row],[Account Name]]="coeo-network-share-nonprod","Share-Non-Production","Share-Production")))) - Column 2,
Enterprise Project, is the plain-value field Phase 2 fills — it is NOT a formula. Important: the raw Huawei CSV itself can already carry real values here, not justdefault.HIS,ERPandNon-projectin particular showed up already tagged straight from the CSV on a real month (Huawei Cloud's own native "Enterprise Project" console feature, applied before export) — these are not something Phase 2 assigns. Phase 2's four rules only ever act on rows still sitting atdefault, and only ever produceWeb,PHR,Enterprise RouterorDirect Connect. Don't assume every fresh month starts 100%default; check the actual distribution before reporting. Also don't assume a month must have hits for all 4 rules, or that hits stay flat month to month — real months ranged from 0 tagging hits up to 9, withWebandPHRhit counts changing as the underlying resource set changed — always inspect the actual Resource Name/Service Type values rather than assuming. - Other sheets:
Control Sheet(named rangesCustomer_discount,INNOCO_Cost,Exchange_rate),Project nameandProject name-customer(cost-distribution + multi-month history — see Phase 1/Finalize),Sheet3if present (a stray pivot, informational only). Project name/Project name-customerlayout:A2:D8(Project namealso has C/D cost columns;Project name-customerdoesn't):B2= literal textList Price - YYYY-MMfor the CURRENT month this workbook represents.B3:B7= formulas computing that month's List Price by category (Production, Non-Production, PHR, Direct Connect, Enterprise Router).B8=SUM(B3:B7)— this must equal the Table1Amount (USD)grand total.A11:F25: the underlying SUMIFS/allocation model. Rows 12–19 = ERP/HIS/PHR/Web split by Production/Non-Production, each getting aC(direct SUMIFS keyed on theEnterprise Projectvalue, e.g.C18/C19only count rows already taggedWeb),D(% of its production/non-production base, e.g.D12=IFERROR(C12/SUM(C$12,C$14,C$16,C$18),0)— see IFERROR note below),E(allocated share of pooled "default"/unclassified cost —=D*Share_productionor=D*Share_nonProduction),F=C+E. Rows 20–21 = Direct Connect / Enterprise Router (no share allocation). Rows 22–25 = thedefault-tagged cost pool split by Environment (Share-Production, Share-Non-Production, Production, Non-Production) — this is what makes the whole sheet reconcile to the Table1 grand total with zero leakage. Do not re-derive this model from scratch or "fix" it — Sivapol built and validated it himself; the pipeline only needs to feed it fresh Table1 data and read back the results (the one exception is theB3orphaned-pool patch below, made at Sivapol's explicit direction).- Named ranges
Share_production/Share_nonProduction— important dependency for Phase 3: originally defined as='Project name'!$B$29/$B$30(=SUM(C22,C24)/=SUM(C23,C25)). Sivapol has since repointed both to'Project name-customer'!$B$29/$B$30instead —Project name-customeralready computes the identicalB29/B30/C22:C25locally, so this makesProject name-customerself-sufficient and safe to keep onceProject nameis deleted (see Phase 3). Confirmed done on the2026-08template and2026-01's Partner file, and inherited automatically by every month copy-forward-built since then. Before running Phase 3 on any month, verify (don't assume) these two named ranges resolve toProject name-customer, notProject name— if a template still points atProject name, deletingProject namein Phase 3 would breakProject name-customerwith#REF!. If found still pointing atProject name, stop and ask Sivapol to repoint it himself in Excel's Name Manager (he prefers to make this specific edit himself) before proceeding. #DIV/0!fix (IFERROR): if a month has zero rows in the Production (or Share-Production) environment at all,SUM(C$12,C$14,C$16,C$18)is 0 andD12/D14/D16/D18used to divide by zero, cascading#DIV/0!intoB3/B5/B8(andC3/C5/C8,D3/D5/D8INNOCO/Customer THB columns onProject name). Fixed at Sivapol's request:D12:D19in bothProject nameandProject name-customerare wrapped=IFERROR(<original formula>,0). Applied to2026-01's Partner file; the Finalize step below makes this self-healing going forward regardless of which template a month was copied from.B3orphaned-pool patch — a follow-on edge case, also fixed at Sivapol's request: the IFERROR fix above stops the error from showing, but when the Production-side base (SUM(C12,C14,C16,C18)) is 0 while the Production/Share-Production default pool (Share_production=C22+C24) is NOT 0, the money used to vanish silently (no ERP/HIS/Web/PHR row exists to proportion it against, soD12/D14/D16/D18all resolve to 0 via IFERROR and the pool goes nowhere — caught by the validation gate as a real mismatch, not a rounding issue). Confirmed fix (Sivapol's instruction): when this happens, put the orphaned amount straight intoB3(Production). Implemented by changingB3in bothProject nameandProject name-customerfrom=SUM(F12,F14,F18)to:
This is a no-op whenever the base is nonzero (the normal proportional model already fully distributes=SUM(F12,F14,F18)+IF(SUM(C$12,C$14,C$16,C$18)=0,Share_production,0)Share_productionacrossF12,F14,F16,F18in that case) — confirmed both on the month that first surfaced it (base became nonzero after Phase 2 tagging, so the patch stopped firing and the money landed in the more accurate category via the normal model instead) and on 5 subsequent real months where Phase 2 tagging always supplied a nonzero base before Finalize ran, so the patch never needed to fire again. Apply this sameB3patch as a self-heal check (like the IFERROR one) on every month's Finalize pass, on whichever template a month was built from.G2:T8: a 12-month history table.G=project label,H..S= Jan..Dec of the current year (fixed mapping: H=01, I=02, J=03, K=04, L=05, M=06, N=07, O=08, P=09, Q=10, R=11, S=12),T=SUM(H:S)grand total per row. Each month's column holds hardcoded values, not formulas — pasted once when that month's workbook is finalized (see Finalize), because Table1 only ever holds one month's data at a time.
Phase 1 — Build a new month's Table1 from the raw CSV
When a new month's cycle-aggregated CSV is ready and the workbook for that month doesn't exist yet (or Sivapol asks to rebuild it fresh). This phase only rebuilds Table1's rows — it does not recalculate, validate, or write history (that happens once, in Finalize, after Phase 2 has also run):
- Copy the starting point: copy the most recently finished working file for the prior month into the new month's subfolder, named
YYYY-MM-Billing-COEO-Partner.xlsxfor the new month (e.g.2026-09-Billing-COEO-Partner.xlsx) — this is the naming convention Sivapol wants for every month built by this pipeline from here on, regardless of what the prior month's file was named. This carries forward all sheet structure, named ranges, and the allocation model — don't build a workbook from scratch. Rename the main data sheet from<prev-YYYY-MM>-Billing-COEOto<new-YYYY-MM>-Billing-COEO(safe:Table1[...]structured references are table-scoped, not sheet-name-scoped, so nothing else breaks). Verify the chosen template file'swb.sheetnamesmatches the expected set (see "File & location" — some folders accumulate stray/corrupted files with similar names). For a batch of several consecutive months, chain them: month N+1 is built from month N's just-finished Partner file (in scratch, before it's saved to the real folder), not from an older template.- If the target month has no chronologically-prior month at all (e.g. building the very first month in the series, like
2026-01when there's no2025-12), there's nothing to copy forward from by definition. Confirmed approach with Sivapol: use the most recently finished/validated month's workbook as the structural template instead (e.g.2026-08's), and clear all 12 history columnsH:S(rows 3–7, bothProject nameandProject name-customer) before writing anything — the template's own history doesn't belong on the new base month. Then proceed with steps 2 onward as normal, writing this month's own value into its own column later in Finalize.
- If the target month has no chronologically-prior month at all (e.g. building the very first month in the series, like
- Read the new cycle CSV, and hard-fail with a clear message if its header row doesn't match Table1 columns 1–57 exactly (column order and names) — don't guess a mapping. Identify the right CSV by header content, not filename (see "File & location").
- Rebuild Table1's data rows to match the CSV exactly:
- If the CSV has fewer rows than Table1's current row count (e.g. new month CSV = 200 rows, but the copied-forward Table1 still has 380 rows from the previous month), delete the excess rows from the bottom (
ws.delete_rows, not just clearing values — no leftover phantom rows). - If the CSV has more rows, insert new rows (
ws.insert_rows), copying cell style from the row immediately above into every new cell so formatting/number formats carry over. - Write the CSV's 57 columns into columns 1–57 for every row. Column 1 (
Billing Cycle) parses from e.g."Aug 2026"to a real date (first of month). Numeric-looking columns convert to float; the literal placeholder--is kept as the literal string--(matches the existing convention) rather than blanked or zeroed. Column 2 (Enterprise Project) just takes whatever the CSV has — which can be a mix ofdefaultand already-tagged values (see "Workbook structure"), not necessarily alldefault. - Column 58 (
Environment) gets the exact formula text above written into every single row, old or new — never leave it blank, never delete it. - Update Table1's
refandautoFilter.refto the newA1:BF<n+1>range.
- If the CSV has fewer rows than Table1's current row count (e.g. new month CSV = 200 rows, but the copied-forward Table1 still has 380 rows from the previous month), delete the excess rows from the bottom (
- Report the mechanical result to Sivapol (old/new row counts, deleted/inserted) and move straight into Phase 2 tagging — no recalc or validation yet at this point. For a batch, gather this per-month rather than reporting immediately (see the combined batch report format under Finalize).
Column-type parsing reference (Python): try datetime.strptime(s, "%b %Y") for Billing Cycle; for every other column try float(s.replace(',', '')), falling back to the raw string (this also correctly preserves -- placeholders and text fields like Resource Name/Region/etc.).
Phase 2 — Enterprise Project classification rules
Run immediately after Phase 1 (Table1 rebuilt), before any recalculation/validation. Apply only to rows where Enterprise Project is currently default (case-insensitive, trimmed). Check in this order (first match wins; conditions are on different fields so overlap is not expected, but keep the order anyway):
Resource Namecontainsweb_(case-insensitive substring) → setEnterprise Project=WebResource NamecontainsRDS_PHR(case-insensitive substring) → setEnterprise Project=PHRService Typeexactly equalsEnterprise Router→ setEnterprise Project=Enterprise RouterService Typeexactly equalsDirect Connect As A Service→ setEnterprise Project=Direct Connect
If a row matches one of these rules but its current Enterprise Project is not default (i.e. someone/something already tagged it differently — including HIS/ERP/Non-project coming straight from Huawei console, see "Workbook structure" above), do not change it — keep the existing value, but collect it into a "skipped/conflict" list to report at the end (row number, Resource Name, Service Type, current value, which rule matched).
Use openpyxl.load_workbook(path, data_only=False) to preserve formulas elsewhere, mutate only the Enterprise Project column values. Locate columns by header name, not fixed letter. Report the tagging tally (rows changed per label, total changed, rows still default, full skipped/conflict list — state it even when zero, and if it's zero, sanity-check the actual Resource Name/Service Type values for that month before reporting it, since a genuinely-zero month is possible but should be confirmed, not assumed) alongside the Phase 1 mechanical report, then move straight into Finalize — don't save or validate yet.
Finalize — recalculate, validate, write history (runs once, after Phase 1 + Phase 2)
This replaces what used to be two separate validate/history passes (once after Phase 1, again after Phase 2 if tagging changed anything) — Sivapol pointed out that's redundant since tagging usually does change the numbers, so now this runs exactly once, after both building and tagging are done, on an in-memory/scratch copy before anything touches the real working file:
- Self-heal checks on both
Project nameandProject name-customer: ifD12:D19isn't already wrappedIFERROR(...,0), wrap it; ifB3isn't already=SUM(F12,F14,F18)+IF(SUM(C$12,C$14,C$16,C$18)=0,Share_production,0), patch it. (Both fixes are already baked into2026-01's Partner file, so a month built from it inherits them automatically — this step is the safety net for templates that predate the fixes, e.g.2026-08.) - Recalculate with LibreOffice — openpyxl cannot evaluate formulas, and the SUMIFS/share-allocation model must actually run on the new data:
(thesoffice --headless --norestore -env:UserInstallation=file:///tmp/lo_profile --convert-to xlsx:"Calc MS Excel 2007 XML" --outdir <dir> <file.xlsx>-env:UserInstallationwith a scratch profile dir avoids profile-lock issues on repeated runs). Read back the recalculated file to harvestB3:B7from bothProject nameandProject name-customer. - Validation gate: sum Table1's
Amount (USD)column directly in Python. Compare it to the sum of the harvestedB3:B7values (map any remaining error value to 0 as a last-resort defensive fallback — with the IFERROR + B3 patches this shouldn't occur anymore). They must match (within float rounding). If they don't match, stop — do not save over the real file — and report the discrepancy to Sivapol (the model should reconcile to zero by construction; a mismatch means something in the CSV, the rebuild, or a new edge case not yet covered is off — this is exactly how theB3orphaned-pool case was first caught). - Update the history: set
B2toList Price - <new-YYYY-MM>and write the harvestedB3:B7values into the matching month column (H..S mapping, see "Workbook structure") rows 3–7, in bothProject nameandProject name-customer, as plain numbers (not formulas). Never touch other months' columns. - Recalculate once more so the
Tcolumn (12-month grand total) picks up the newly written month. - Report to Sivapol as one combined summary: old/new Table1 row counts, Phase 2 tagging tally + conflicts, any self-heal patches applied, the validation-gate result, the month column written and its values, the new grand total, and the exact output filename — before saving over the real file. Treat this as a single STOP-gate action: show the full dry tally, execute (copy/save into the real month subfolder) only after Sivapol confirms. If rebuilding a month that already has a working file, make a
_BK.xlsxcopy of whatever's already there first if no backup sibling exists yet. For a batch of several months: run Phase 1→2→Finalize (build/tag/validate in scratch, no saving) for every month in the batch first, then present ONE consolidated table (one row per month: row counts, tagging tally, validation result, grand total) and ask ONE confirmation before saving all of them to their real folders — validated on a real 5-month batch (see Status log). If any single month in the batch fails validation, stop and report just that month's discrepancy — don't silently skip it or save the others without flagging it first.
Phase 3 — Customer-facing export
Once that month's workbook has been through Phase 1 + Phase 2 + Finalize, produce the version that goes out to the customer. Not run automatically — only when Sivapol asks for it (he has asked for every month so far, but always as a separate explicit instruction, sometimes in the same turn as confirming Finalize and sometimes later):
- Prerequisite check: open the month's working file and confirm the
Share_production/Share_nonProductiondefined names resolve to'Project name-customer'!$B$29/$B$30(not'Project name'!...) — see "Workbook structure" above. If they still point atProject name, stop and ask Sivapol to repoint them himself in Excel's Name Manager before continuing (this is a deliberate one-time fix he makes per-file/per-template, not something this skill does automatically — he explicitly wants to keep control of editing defined names). - Copy the completed working file to a new file named
YYYY-MM-Billing-COEO-Customer.xlsxin the same month subfolder. - Delete the
Control SheetandProject namesheets from the copy (del wb[sheet_name]). Keep the mainYYYY-MM-Billing-COEOdata sheet (with Table1) andProject name-customer. Keep all defined names — Sivapol explicitly wants them preserved even thoughCustomer_discount/INNOCO_Cost/Exchange_rate(pointing at the now-deletedControl Sheet) become unresolvable; nothing in the surviving sheets uses them, so this causes no visible error, and don't clean them up unless Sivapol asks. - Recalculate with LibreOffice (same command as Finalize) and verify: no error-value cells remain anywhere in
Project name-customerrows 3–30, andB8still equals Table1'sAmount (USD)sum exactly. If either check fails, stop and report — don't hand over a broken customer file. - Report to Sivapol: source file used, sheets removed, validation result, output filename. Treat as a STOP-gate action like the other phases. For a batch of several months, do this for every month after all their Finalize saves are confirmed, then report all of them together (source/sheets removed/validation/filename per month) rather than one at a time — validated on a real 5-month batch.
Status log
- Phase 2 (Enterprise Project fill: Web / PHR / Enterprise Router / Direct Connect via Resource Name & Service Type) — implemented and validated on
2026-08/x-2026-08-Billing-COEO_2.xlsx(31 rows changed, 0 conflicts). Re-run is idempotent. - Diagnostic note (historical) — traced a 186.16218007 USD gap in Sivapol's
Project nameSUMIFS totals to (a) an over-subtraction bug and (b)default-tagged rows outside Share-* environments having no bucket at all. Sivapol redesigned the sheet himself (rows 22–25 +Share_production/Share_nonProductionnamed ranges) — this fully closes the gap; do not reintroduce the old subtraction-based formula. - Phase 1 mechanics — validated in a sandbox (row insert/delete, Environment formula on 100% of rows, LibreOffice recalculation genuine, grand total reconciled exactly in both directions).
- Real run:
2026-01(first build, then rebuilt from scratch on request) — 80 rows, zero Production-environment rows that month,#DIV/0!→ 0 handled, grand total 1366.42380896. Phase 2: 0 rows changed (verified not a bug — no resources matching any of the 4 rules that month), 0 conflicts. - Naming convention: output file naming is
YYYY-MM-Billing-COEO-Partner.xlsxfor every month built from here on; existing months already built under the oldx-YYYY-MM-Billing-COEO_2.xlsxconvention (2026-08) are left as-is. - Phase numbering: build/tag order swapped to match real workflow — content unchanged, only labels swapped.
#DIV/0!→ IFERROR fix (2026-01) — Sivapol asked to make the zero-Production-base edge case show 0 instead of#DIV/0!directly in the live formulas. WrappedD12:D19in bothProject nameandProject name-customerwithIFERROR(...,0)on2026-01-Billing-COEO-Partner.xlsx. Added as a Finalize self-heal check so every future month gets this regardless of template. Not yet applied to the2026-08template file itself — the self-heal check catches it automatically whenever a month is next built from it.Share_production/Share_nonProductionnamed-range repoint (2026-01) — these named ranges pointed at'Project name'!$B$29/$B$30, which would breakProject name-customerwith#REF!onceProject nameis deleted in Phase 3. Sivapol repointed both names himself (Excel Name Manager) to'Project name-customer'!$B$29/$B$30on both2026-08/x-2026-08-Billing-COEO_2.xlsxand2026-01/2026-01-Billing-COEO-Partner.xlsx— confirmed viawb.defined_names. Prerequisite for Phase 3 (see Phase 3 step 1); check, don't assume, on any file that predates this fix.- Phase 3 (Customer export) — implemented and validated on
2026-01: copied the fixed/tagged Partner file to2026-01-Billing-COEO-Customer.xlsx, deletedControl Sheet+Project name, kept all 5 defined names, recalculated — zero errors,B8= 1366.42380896 matching Table1 exactly. - Real run:
2026-02(build → tag → finalize, single-pass order) — 80→225 rows (+145). First validation attempt (Phase 1 alone, before tagging) failed the gate by exactly 24.26038457 USD: this month had real Production/Share-Productiondefault-tagged cost ($23.50322 + $0.75716) but zero rows tagged ERP/HIS/PHR/Web in the Production environment, soD12/D14/D18(via IFERROR) all resolved to 0 and the money vanished instead of erroring — a new edge case (nonzero pool, zero base) distinct from the original all-zero case. Sivapol's fix: route it intoB3(Production) directly — implemented as theB3orphaned-pool patch documented above. Sivapol then pointed out that Phase 2 tagging should run before the validation/history pass, not after with a possible rewrite, since tagging often changes the numbers anyway — adopted as the new standard order (this Status log entry is what prompted restructuring Phase 1/Phase 2/Finalize into their current separate sections). Re-running with Phase 2 first: 3 rows tagged PHR, which made the Production base nonzero, so the normal proportional model took over on its own (theB3patch'sIFcondition no longer fired) and correctly routed the $24.26 into PHR instead of generic Production — validation passed,B8= 1290.0295992 matching Table1 exactly. Saved as2026-02-Billing-COEO-Partner.xlsx. Phase 3 also run for2026-02: zero errors,B8= 1290.0295992 matching Table1. - Batch real run:
2026-03through2026-07(5 consecutive months, chained: each month built from the previous month's just-saved Partner file, Phase 1 → Phase 2 → Finalize → Phase 3 for all 5 before any single confirmation) — Sivapol confirmed this batch/chained approach works and is the expected way to run several months in one go, not just one month at a time. Row counts: 225→215→213→197→220→256. Phase 2 tagging kept finding real hits as the months progressed (PHR +3 every month, Web +6 in2026-05, +4 in2026-06and2026-07) — confirms Phase 2 rules stay relevant, not just a one-off from2026-08. All 5 months validated cleanly on the first pass (Phase 2 always run before Finalize, per the2026-02lesson) — noB3-patch firing needed since Phase 2 tagging consistently supplied a nonzero Production base before Finalize ran. All 5 self-heal checks were no-ops (both fixes already inherited from2026-02through the copy-forward chain). Phase 3 ran cleanly on all 5 with zero errors and exactB8/Table1 matches. Grand totals:2026-03=1790.60449485,2026-04=2226.56057435,2026-05=3005.4661607,2026-06=2651.19627612,2026-07=3191.95690991. This leaves2026-01–2026-08all fully built (Phase 1+2+Finalize+Phase 3);2026-09–2026-12still need their CSVs before they can be built.