Monthly Close Control Plane
A small, review-first monthly-close control pack for a validated trial-balance export. It turns current and prior period trial balances into deterministic integrity checks, variance exceptions, reconciliation exceptions, and a human-review pack.
It is intentionally narrow:
Validated trial-balance export
|
v
Exact control gates and variance checks
|
v
Explicit exception queue
|
v
Human review and workpaper acknowledgement
The first MVP accepts the canonical CSV written by xero-trial-balance-export. Each file must contain exactly one tenant and one report date; current and prior files must name the same tenant, and the prior date must be earlier. It does not connect to Xero, store OAuth tokens, write journals, make payments, lodge BAS, lock a period, distribute a client report, or claim that a close has been approved.
Why this exists
A close can be technically balanced and still need review. This tool keeps the evidence visible:
- Exact
Decimalarithmetic for money controls, never binary floating point. - Schema, duplicate-key, date, and numeric gates fail closed.
- Current-period and YTD debits must exactly equal credits.
- Material YTD variances, new/missing accounts, account metadata changes, unmapped accounts, and supplied subledger differences become explicit exceptions.
- A YTD variance is raised only when it clears both the absolute and the percentage threshold, with one carve-out: an account whose prior YTD balance is nil has no percentage change to compute, so the absolute threshold decides alone. Those exceptions name the absolute threshold only and leave
percentage_changeblank, rather than reporting that a percentage test passed that never ran. - Output has only
PASS,REVIEW, andBLOCKEDstates. A reviewer, not the tool, decides whether a close is acceptable. - Source SHA-256 digests travel with the generated review pack so its source files can be identified later. Each digest is calculated from the same immutable byte snapshot the loader parses, so a file replaced during a run cannot be misidentified as the source of the calculations.
- Spreadsheet-facing CSV text beginning with
=is always neutralised with a leading apostrophe.+,-and@are neutralised unless the rest of the value is a plain identifier (word characters and dots, and not an A1-style cell reference), so an account code like-1000or an ID like@123stays joinable in Excel and Power BI, while-A1is quoted. That pass-through is narrow by construction, not a test for everything a spreadsheet can evaluate:+unsafestill passes through and reads as a defined name rather than as text, which costs display fidelity in that cell and calls nothing. Every payload that reaches outside the sheet carries a character the identifier test rejects. Text rendered intoclose-summary.md(reviewer-note text and every exception table cell) is flattened onto one line, and its backslashes are escaped before its pipes so that neither a pipe nor a backslash shielding one can add a cell and shift the columns a reviewer reads. exceptions.csvis written with a UTF-8 byte-order mark, matching the canonical input files, so a spreadsheet reads non-ASCII entity and account names correctly.- The three pack files are staged beside their destinations and moved into place only once all three have been written. If one cannot be replaced (a reviewer holding
exceptions.csvopen is the usual cause), the files already moved are rolled back to the content they replaced, so the previous pack survives whole instead of half describing one trial balance and half describing another. A failed run never deletes a pack file it did not write. Run one export at a time into a given--outputdirectory; concurrent runs are not serialised. - Amounts are rendered with at least two decimal places and never fewer than the value carries. A percentage is rendered with at least two places and always enough to show its leading significant digit, so neither a tolerance finer than one cent nor a threshold finer than a hundredth of a percent is flattened to
0.00.
Quick demo
The repository contains fabricated data only. Do not commit client trial balances, workpapers, exports, or credentials.
python -m pip install -e ".[dev]"
close-control review \
--current examples/current_trial_balance.csv \
--prior examples/prior_trial_balance.csv \
--mapping examples/account_mapping.csv \
--subledger examples/subledger_balances.csv \
--absolute-threshold 10000 \
--percentage-threshold 0.10 \
--reconciliation-tolerance 0.01 \
--review-note examples/review_note.json \
--output outputs/demo
The demo exits 2 because its deliberately fabricated exceptions need human review. It writes:
close-summary.md: a concise, deterministic review pack.exceptions.csv: filterable exception detail for Excel or Power BI.close-review-pack.json: structured evidence, thresholds, source hashes, and any supplied review acknowledgement.
Use exit code 0 only for an all-PASS pack, 2 for REVIEW or BLOCKED, and 1 for a malformed file, an invalid command configuration, or an --output path that cannot be written.
Canonical trial-balance contract
The initial input is the ten-column, normalised trial-balance schema from xero-trial-balance-export:
ReportDate,Tenant,Section,AccountID,AccountName,AccountCode,Debit,Credit,YTDDebit,YTDCredit
Tenant plus AccountID is the control key. AccountCode and AccountName are display attributes, not stable identifiers. The loader rejects unknown/missing columns, duplicate control keys, malformed ISO dates, empty identifiers, and malformed monetary values.
The current-period Debit/Credit pair represents movement. YTDDebit/YTDCredit represents the position used for variance comparison. All values are read as exact decimals.
Optional mapping and reconciliation inputs
An account mapping is a two-column CSV:
AccountID,ReviewGroup
Any current TB account that is missing from a supplied mapping remains in the pack as a REVIEW exception. The mapping is a review label; it does not transform source numbers.
An optional subledger CSV must have:
Tenant,AccountID,SubledgerBalance
SubledgerBalance must use the same signed convention as YTDDebit - YTDCredit: debit balances positive; credit balances negative. Each supplied subledger row is compared only with the matching current TB account. A missing GL account, or a difference beyond --reconciliation-tolerance, requires review.
Human acknowledgement
If a reviewer wants the pack to record that it was read, supply a separate JSON file:
{
"reviewer_initials": "RD",
"reviewed_on": "2026-08-08",
"comment": "Reviewed demo exceptions; no client close was approved by this example."
}
reviewed_on must not be earlier than the current ReportDate. A note dated before the period it claims to review is rejected as a malformed input: the run stops with exit 1 and writes no pack.
An acknowledgement is evidence of a human action only. It never changes REVIEW or BLOCKED to PASS, and it never asserts that a period has been closed.
Data and operational boundaries
- Use a separate, access-controlled working directory for client source files and outputs.
- Keep this checkout limited to fabricated fixtures. Its
.gitignoreblocks CSVs outsideexamples/andschemas/, and blocks all three generated pack files by name wherever--outputpoints them, including inside those two fixture directories. - Produce the source CSV through a read-only export workflow. Live Xero OAuth, token storage, and client authorisation are deliberately outside this MVP.
- Do not use this as tax, financial, audit, or legal advice. It is a configurable review aid that requires professional judgement.
Development
python -m pip install -e ".[dev]"
pytest
python -m build
The test suite covers schema gates, exact balancing, variance and metadata exceptions, mapping and subledger checks, deterministic pack generation, acknowledgement parsing, and the command-line exit contract.
Continuous integration verifies the committed uv.lock, runs the test suite on Python 3.10, 3.11, 3.12, and 3.13, then builds and smoke-tests the wheel with the fabricated demo. CodeQL scans the Python source, and Dependabot is configured to propose updates for uv dependencies and pinned GitHub Actions. See CONTRIBUTING.md for the local verification and data-handling requirements.
Roadmap
The next layers are deliberately separated from the control engine:
- A safe, read-only Xero AI review gateway with a defined query allowlist, redaction boundary, source evidence, and no mutation tools.
- A tax-change impact monitor that records authoritative source versions, produces drafts for human review, and never turns legislation changes into an automatic client conclusion.
See docs/follow-on-safety-layers.md for the intended boundary contracts.
Built with AI assistance (Claude); design, review, and testing by the author.
MIT licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file monthly_close_control_plane-0.1.1.tar.gz.
File metadata
- Download URL: monthly_close_control_plane-0.1.1.tar.gz
- Upload date:
- Size: 40.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b3f0ad555d2192989e10ab4433035fb0e45530643d941dc5a3cf403ac946e59
|
|
| MD5 |
dabc69adcc573a2306d6bbe95271d4c7
|
|
| BLAKE2b-256 |
8d0668988e1493be8e9a1683f68d48dacd3eeac1ca7c100e71b6cd57ed51304b
|
Provenance
The following attestation bundles were made for monthly_close_control_plane-0.1.1.tar.gz:
Publisher:
release.yml on ryanduguid/monthly-close-control-plane
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
monthly_close_control_plane-0.1.1.tar.gz -
Subject digest:
0b3f0ad555d2192989e10ab4433035fb0e45530643d941dc5a3cf403ac946e59 - Sigstore transparency entry: 2580028977
- Sigstore integration time:
-
Permalink:
ryanduguid/monthly-close-control-plane@4fc4f1cae7f552e101f718901ead078ee53df820 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ryanduguid
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4fc4f1cae7f552e101f718901ead078ee53df820 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file monthly_close_control_plane-0.1.1-py3-none-any.whl.
File metadata
- Download URL: monthly_close_control_plane-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4ca2bce708a3e28c8a6316eae68095848a116a04a99f24bc1d7325d92a449d9
|
|
| MD5 |
36ad641940bd3ae54582779049c51478
|
|
| BLAKE2b-256 |
31a690a678c8a7622fbe7e5d2d78171d465887d6c843a4efa650f33710ef828f
|
Provenance
The following attestation bundles were made for monthly_close_control_plane-0.1.1-py3-none-any.whl:
Publisher:
release.yml on ryanduguid/monthly-close-control-plane
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
monthly_close_control_plane-0.1.1-py3-none-any.whl -
Subject digest:
e4ca2bce708a3e28c8a6316eae68095848a116a04a99f24bc1d7325d92a449d9 - Sigstore transparency entry: 2580028981
- Sigstore integration time:
-
Permalink:
ryanduguid/monthly-close-control-plane@4fc4f1cae7f552e101f718901ead078ee53df820 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ryanduguid
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4fc4f1cae7f552e101f718901ead078ee53df820 -
Trigger Event:
workflow_dispatch
-
Statement type: