monthly-close-control-plane 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. monthly_close_control_plane-0.1.1/CONTRIBUTING.md +28 -0
  2. monthly_close_control_plane-0.1.1/LICENSE +21 -0
  3. monthly_close_control_plane-0.1.1/MANIFEST.in +7 -0
  4. monthly_close_control_plane-0.1.1/PKG-INFO +162 -0
  5. monthly_close_control_plane-0.1.1/README.md +143 -0
  6. monthly_close_control_plane-0.1.1/SECURITY.md +22 -0
  7. monthly_close_control_plane-0.1.1/closecontrol/__init__.py +5 -0
  8. monthly_close_control_plane-0.1.1/closecontrol/cli.py +104 -0
  9. monthly_close_control_plane-0.1.1/closecontrol/engine.py +329 -0
  10. monthly_close_control_plane-0.1.1/closecontrol/errors.py +2 -0
  11. monthly_close_control_plane-0.1.1/closecontrol/loader.py +292 -0
  12. monthly_close_control_plane-0.1.1/closecontrol/models.py +59 -0
  13. monthly_close_control_plane-0.1.1/closecontrol/report.py +350 -0
  14. monthly_close_control_plane-0.1.1/docs/architecture.md +53 -0
  15. monthly_close_control_plane-0.1.1/docs/follow-on-safety-layers.md +48 -0
  16. monthly_close_control_plane-0.1.1/examples/account_mapping.csv +7 -0
  17. monthly_close_control_plane-0.1.1/examples/current_trial_balance.csv +8 -0
  18. monthly_close_control_plane-0.1.1/examples/prior_trial_balance.csv +8 -0
  19. monthly_close_control_plane-0.1.1/examples/review_note.json +5 -0
  20. monthly_close_control_plane-0.1.1/examples/subledger_balances.csv +3 -0
  21. monthly_close_control_plane-0.1.1/monthly_close_control_plane.egg-info/PKG-INFO +162 -0
  22. monthly_close_control_plane-0.1.1/monthly_close_control_plane.egg-info/SOURCES.txt +32 -0
  23. monthly_close_control_plane-0.1.1/monthly_close_control_plane.egg-info/dependency_links.txt +1 -0
  24. monthly_close_control_plane-0.1.1/monthly_close_control_plane.egg-info/entry_points.txt +2 -0
  25. monthly_close_control_plane-0.1.1/monthly_close_control_plane.egg-info/requires.txt +4 -0
  26. monthly_close_control_plane-0.1.1/monthly_close_control_plane.egg-info/top_level.txt +1 -0
  27. monthly_close_control_plane-0.1.1/pyproject.toml +34 -0
  28. monthly_close_control_plane-0.1.1/schemas/account_mapping.csv +1 -0
  29. monthly_close_control_plane-0.1.1/schemas/canonical_trial_balance.csv +1 -0
  30. monthly_close_control_plane-0.1.1/schemas/subledger_balances.csv +1 -0
  31. monthly_close_control_plane-0.1.1/setup.cfg +4 -0
  32. monthly_close_control_plane-0.1.1/tests/test_engine.py +400 -0
  33. monthly_close_control_plane-0.1.1/tests/test_loader.py +176 -0
  34. monthly_close_control_plane-0.1.1/tests/test_report_and_cli.py +699 -0
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ Keep this project in its narrow role: a local, deterministic review-pack generator. No contribution should give it authority to post journals, make payments, lodge returns, lock periods, send reports, or approve a close.
4
+
5
+ ## Data boundary
6
+
7
+ - Use fabricated fixtures. Keep client trial balances, subledgers, workpapers, review packs, credentials, `.env` files, tokens and screenshots from a live accounting system out of the repository.
8
+ - Put fabricated CSV fixtures under `examples/` and header-only schema references under `schemas/`. The `.gitignore` blocks ordinary CSV files outside those two directories.
9
+ - Treat source CSV content and review notes as untrusted input. Keep the fail-closed validation and the spreadsheet-formula safeguards in place.
10
+
11
+ ## Local verification
12
+
13
+ Python 3.10 or newer. The repository uses `uv` and commits its lock file.
14
+
15
+ ```bash
16
+ uv lock --check
17
+ uv sync --locked --all-extras
18
+ uv run pytest
19
+ uv build
20
+ ```
21
+
22
+ For a behaviour change, add or update a focused test under `tests/`. Keep the output deterministic: no wall-clock timestamps, client identifiers or hidden state in a review pack.
23
+
24
+ ## Pull requests
25
+
26
+ Explain which control or boundary your change affects, include the test result, and name any operational limitation that remains. Never present a review acknowledgement as an approved or completed close.
27
+
28
+ For a potential security vulnerability, follow [SECURITY.md](SECURITY.md), and keep credentials, client data and exploit details out of the issue tracker.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Duguid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE
2
+ include README.md
3
+ include SECURITY.md
4
+ include CONTRIBUTING.md
5
+ recursive-include docs *.md
6
+ recursive-include examples *.csv *.json
7
+ recursive-include schemas *.csv
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: monthly-close-control-plane
3
+ Version: 0.1.1
4
+ Summary: A review-first monthly close control pack for validated trial-balance exports
5
+ Author: Ryan Duguid
6
+ License-Expression: MIT
7
+ Keywords: accounting,month-end-close,reconciliation,xero,controls
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Financial and Insurance Industry
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: dev
16
+ Requires-Dist: build>=1.2; extra == "dev"
17
+ Requires-Dist: pytest>=8; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # Monthly Close Control Plane
21
+
22
+ [![tests](https://github.com/ryanduguid/monthly-close-control-plane/actions/workflows/ci.yml/badge.svg)](https://github.com/ryanduguid/monthly-close-control-plane/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
23
+
24
+ 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.
25
+
26
+ It is intentionally narrow:
27
+
28
+ ```text
29
+ Validated trial-balance export
30
+ |
31
+ v
32
+ Exact control gates and variance checks
33
+ |
34
+ v
35
+ Explicit exception queue
36
+ |
37
+ v
38
+ Human review and workpaper acknowledgement
39
+ ```
40
+
41
+ The first MVP accepts the canonical CSV written by [xero-trial-balance-export](https://github.com/ryanduguid/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.
42
+
43
+ ## Why this exists
44
+
45
+ A close can be technically balanced and still need review. This tool keeps the evidence visible:
46
+
47
+ - Exact `Decimal` arithmetic for money controls, never binary floating point.
48
+ - Schema, duplicate-key, date, and numeric gates fail closed.
49
+ - Current-period and YTD debits must exactly equal credits.
50
+ - Material YTD variances, new/missing accounts, account metadata changes, unmapped accounts, and supplied subledger differences become explicit exceptions.
51
+ - 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_change` blank, rather than reporting that a percentage test passed that never ran.
52
+ - Output has only `PASS`, `REVIEW`, and `BLOCKED` states. A reviewer, not the tool, decides whether a close is acceptable.
53
+ - 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.
54
+ - 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 `-1000` or an ID like `@123` stays joinable in Excel and Power BI, while `-A1` is quoted. That pass-through is narrow by construction, not a test for everything a spreadsheet can evaluate: `+unsafe` still 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 into `close-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.
55
+ - `exceptions.csv` is written with a UTF-8 byte-order mark, matching the canonical input files, so a spreadsheet reads non-ASCII entity and account names correctly.
56
+ - 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.csv` open 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 `--output` directory; concurrent runs are not serialised.
57
+ - 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`.
58
+
59
+ ## Quick demo
60
+
61
+ The repository contains fabricated data only. Do not commit client trial balances, workpapers, exports, or credentials.
62
+
63
+ ```bash
64
+ python -m pip install -e ".[dev]"
65
+
66
+ close-control review \
67
+ --current examples/current_trial_balance.csv \
68
+ --prior examples/prior_trial_balance.csv \
69
+ --mapping examples/account_mapping.csv \
70
+ --subledger examples/subledger_balances.csv \
71
+ --absolute-threshold 10000 \
72
+ --percentage-threshold 0.10 \
73
+ --reconciliation-tolerance 0.01 \
74
+ --review-note examples/review_note.json \
75
+ --output outputs/demo
76
+ ```
77
+
78
+ The demo exits `2` because its deliberately fabricated exceptions need human review. It writes:
79
+
80
+ - `close-summary.md`: a concise, deterministic review pack.
81
+ - `exceptions.csv`: filterable exception detail for Excel or Power BI.
82
+ - `close-review-pack.json`: structured evidence, thresholds, source hashes, and any supplied review acknowledgement.
83
+
84
+ 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.
85
+
86
+ ## Canonical trial-balance contract
87
+
88
+ The initial input is the ten-column, normalised trial-balance schema from `xero-trial-balance-export`:
89
+
90
+ ```text
91
+ ReportDate,Tenant,Section,AccountID,AccountName,AccountCode,Debit,Credit,YTDDebit,YTDCredit
92
+ ```
93
+
94
+ `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.
95
+
96
+ The current-period `Debit`/`Credit` pair represents movement. `YTDDebit`/`YTDCredit` represents the position used for variance comparison. All values are read as exact decimals.
97
+
98
+ ## Optional mapping and reconciliation inputs
99
+
100
+ An account mapping is a two-column CSV:
101
+
102
+ ```text
103
+ AccountID,ReviewGroup
104
+ ```
105
+
106
+ 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.
107
+
108
+ An optional subledger CSV must have:
109
+
110
+ ```text
111
+ Tenant,AccountID,SubledgerBalance
112
+ ```
113
+
114
+ `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.
115
+
116
+ ## Human acknowledgement
117
+
118
+ If a reviewer wants the pack to record that it was read, supply a separate JSON file:
119
+
120
+ ```json
121
+ {
122
+ "reviewer_initials": "RD",
123
+ "reviewed_on": "2026-08-08",
124
+ "comment": "Reviewed demo exceptions; no client close was approved by this example."
125
+ }
126
+ ```
127
+
128
+ `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.
129
+
130
+ 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.
131
+
132
+ ## Data and operational boundaries
133
+
134
+ - Use a separate, access-controlled working directory for client source files and outputs.
135
+ - Keep this checkout limited to fabricated fixtures. Its `.gitignore` blocks CSVs outside `examples/` and `schemas/`, and blocks all three generated pack files by name wherever `--output` points them, including inside those two fixture directories.
136
+ - Produce the source CSV through a read-only export workflow. Live Xero OAuth, token storage, and client authorisation are deliberately outside this MVP.
137
+ - Do not use this as tax, financial, audit, or legal advice. It is a configurable review aid that requires professional judgement.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ python -m pip install -e ".[dev]"
143
+ pytest
144
+ python -m build
145
+ ```
146
+
147
+ 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.
148
+
149
+ 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](CONTRIBUTING.md) for the local verification and data-handling requirements.
150
+
151
+ ## Roadmap
152
+
153
+ The next layers are deliberately separated from the control engine:
154
+
155
+ 1. A safe, read-only Xero AI review gateway with a defined query allowlist, redaction boundary, source evidence, and no mutation tools.
156
+ 2. 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.
157
+
158
+ See [docs/follow-on-safety-layers.md](docs/follow-on-safety-layers.md) for the intended boundary contracts.
159
+
160
+ Built with AI assistance (Claude); design, review, and testing by the author.
161
+
162
+ MIT licensed.
@@ -0,0 +1,143 @@
1
+ # Monthly Close Control Plane
2
+
3
+ [![tests](https://github.com/ryanduguid/monthly-close-control-plane/actions/workflows/ci.yml/badge.svg)](https://github.com/ryanduguid/monthly-close-control-plane/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
4
+
5
+ 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.
6
+
7
+ It is intentionally narrow:
8
+
9
+ ```text
10
+ Validated trial-balance export
11
+ |
12
+ v
13
+ Exact control gates and variance checks
14
+ |
15
+ v
16
+ Explicit exception queue
17
+ |
18
+ v
19
+ Human review and workpaper acknowledgement
20
+ ```
21
+
22
+ The first MVP accepts the canonical CSV written by [xero-trial-balance-export](https://github.com/ryanduguid/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.
23
+
24
+ ## Why this exists
25
+
26
+ A close can be technically balanced and still need review. This tool keeps the evidence visible:
27
+
28
+ - Exact `Decimal` arithmetic for money controls, never binary floating point.
29
+ - Schema, duplicate-key, date, and numeric gates fail closed.
30
+ - Current-period and YTD debits must exactly equal credits.
31
+ - Material YTD variances, new/missing accounts, account metadata changes, unmapped accounts, and supplied subledger differences become explicit exceptions.
32
+ - 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_change` blank, rather than reporting that a percentage test passed that never ran.
33
+ - Output has only `PASS`, `REVIEW`, and `BLOCKED` states. A reviewer, not the tool, decides whether a close is acceptable.
34
+ - 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.
35
+ - 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 `-1000` or an ID like `@123` stays joinable in Excel and Power BI, while `-A1` is quoted. That pass-through is narrow by construction, not a test for everything a spreadsheet can evaluate: `+unsafe` still 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 into `close-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.
36
+ - `exceptions.csv` is written with a UTF-8 byte-order mark, matching the canonical input files, so a spreadsheet reads non-ASCII entity and account names correctly.
37
+ - 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.csv` open 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 `--output` directory; concurrent runs are not serialised.
38
+ - 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`.
39
+
40
+ ## Quick demo
41
+
42
+ The repository contains fabricated data only. Do not commit client trial balances, workpapers, exports, or credentials.
43
+
44
+ ```bash
45
+ python -m pip install -e ".[dev]"
46
+
47
+ close-control review \
48
+ --current examples/current_trial_balance.csv \
49
+ --prior examples/prior_trial_balance.csv \
50
+ --mapping examples/account_mapping.csv \
51
+ --subledger examples/subledger_balances.csv \
52
+ --absolute-threshold 10000 \
53
+ --percentage-threshold 0.10 \
54
+ --reconciliation-tolerance 0.01 \
55
+ --review-note examples/review_note.json \
56
+ --output outputs/demo
57
+ ```
58
+
59
+ The demo exits `2` because its deliberately fabricated exceptions need human review. It writes:
60
+
61
+ - `close-summary.md`: a concise, deterministic review pack.
62
+ - `exceptions.csv`: filterable exception detail for Excel or Power BI.
63
+ - `close-review-pack.json`: structured evidence, thresholds, source hashes, and any supplied review acknowledgement.
64
+
65
+ 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.
66
+
67
+ ## Canonical trial-balance contract
68
+
69
+ The initial input is the ten-column, normalised trial-balance schema from `xero-trial-balance-export`:
70
+
71
+ ```text
72
+ ReportDate,Tenant,Section,AccountID,AccountName,AccountCode,Debit,Credit,YTDDebit,YTDCredit
73
+ ```
74
+
75
+ `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.
76
+
77
+ The current-period `Debit`/`Credit` pair represents movement. `YTDDebit`/`YTDCredit` represents the position used for variance comparison. All values are read as exact decimals.
78
+
79
+ ## Optional mapping and reconciliation inputs
80
+
81
+ An account mapping is a two-column CSV:
82
+
83
+ ```text
84
+ AccountID,ReviewGroup
85
+ ```
86
+
87
+ 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.
88
+
89
+ An optional subledger CSV must have:
90
+
91
+ ```text
92
+ Tenant,AccountID,SubledgerBalance
93
+ ```
94
+
95
+ `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.
96
+
97
+ ## Human acknowledgement
98
+
99
+ If a reviewer wants the pack to record that it was read, supply a separate JSON file:
100
+
101
+ ```json
102
+ {
103
+ "reviewer_initials": "RD",
104
+ "reviewed_on": "2026-08-08",
105
+ "comment": "Reviewed demo exceptions; no client close was approved by this example."
106
+ }
107
+ ```
108
+
109
+ `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.
110
+
111
+ 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.
112
+
113
+ ## Data and operational boundaries
114
+
115
+ - Use a separate, access-controlled working directory for client source files and outputs.
116
+ - Keep this checkout limited to fabricated fixtures. Its `.gitignore` blocks CSVs outside `examples/` and `schemas/`, and blocks all three generated pack files by name wherever `--output` points them, including inside those two fixture directories.
117
+ - Produce the source CSV through a read-only export workflow. Live Xero OAuth, token storage, and client authorisation are deliberately outside this MVP.
118
+ - Do not use this as tax, financial, audit, or legal advice. It is a configurable review aid that requires professional judgement.
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ python -m pip install -e ".[dev]"
124
+ pytest
125
+ python -m build
126
+ ```
127
+
128
+ 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.
129
+
130
+ 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](CONTRIBUTING.md) for the local verification and data-handling requirements.
131
+
132
+ ## Roadmap
133
+
134
+ The next layers are deliberately separated from the control engine:
135
+
136
+ 1. A safe, read-only Xero AI review gateway with a defined query allowlist, redaction boundary, source evidence, and no mutation tools.
137
+ 2. 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.
138
+
139
+ See [docs/follow-on-safety-layers.md](docs/follow-on-safety-layers.md) for the intended boundary contracts.
140
+
141
+ Built with AI assistance (Claude); design, review, and testing by the author.
142
+
143
+ MIT licensed.
@@ -0,0 +1,22 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are applied to the latest version on the default branch.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please use this repository's private vulnerability-reporting feature. Do not
10
+ open a public issue for a suspected security vulnerability. Include a clear
11
+ description, reproduction steps, impact, and any suggested mitigation.
12
+
13
+ We will acknowledge a valid report within seven days and will coordinate a fix
14
+ and disclosure timeline with the reporter.
15
+
16
+ ## Data boundary
17
+
18
+ This repository ships fabricated fixtures only. It deliberately has no Xero credential, API client, MCP server, LLM client, journal-posting path, payment path, BAS/lodgment path, email path, or period-locking path.
19
+
20
+ Do not commit client trial balances, subledgers, workpapers, credentials, `.env` files, or generated review packs. The `examples/` folder (fabricated fixtures) and the `schemas/` folder (header-only schema references) are the only places CSVs belong in this source tree. Use a separate access-controlled working location for real data.
21
+
22
+ The optional review note records a human acknowledgement only. It does not approve a close or replace professional judgement.
@@ -0,0 +1,5 @@
1
+ """Review-first controls for a monthly trial-balance close."""
2
+
3
+ from .engine import CloseReviewPack, review_close
4
+
5
+ __all__ = ["CloseReviewPack", "review_close"]
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from decimal import Decimal, InvalidOperation
6
+ from pathlib import Path
7
+
8
+ from .engine import review_close
9
+ from .errors import ControlInputError
10
+ from .report import PACK_FILE_NAMES as _PACK_FILE_NAMES, write_review_pack
11
+
12
+
13
+ def _non_negative_decimal(value: str) -> Decimal:
14
+ try:
15
+ result = Decimal(value)
16
+ except InvalidOperation as exc:
17
+ raise argparse.ArgumentTypeError(f"not a decimal: {value!r}") from exc
18
+ if not result.is_finite() or result < 0:
19
+ raise argparse.ArgumentTypeError("must be a finite non-negative decimal")
20
+ return result
21
+
22
+
23
+ def build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(description="Create a review-first monthly close control pack from validated trial-balance exports.")
25
+ commands = parser.add_subparsers(dest="command", required=True)
26
+ review = commands.add_parser("review", help="run integrity, variance, mapping, and optional reconciliation controls")
27
+ review.add_argument("--current", required=True, type=Path, help="current-period canonical trial-balance CSV")
28
+ review.add_argument("--prior", required=True, type=Path, help="prior-period canonical trial-balance CSV")
29
+ review.add_argument("--mapping", type=Path, help="optional AccountID,ReviewGroup mapping CSV")
30
+ review.add_argument("--subledger", type=Path, help="optional Tenant,AccountID,SubledgerBalance CSV")
31
+ review.add_argument("--review-note", type=Path, help="optional human acknowledgement JSON")
32
+ review.add_argument("--output", required=True, type=Path, help="directory for the generated review pack")
33
+ review.add_argument("--absolute-threshold", type=_non_negative_decimal, default=Decimal("1000"), help="minimum absolute YTD variance for review")
34
+ review.add_argument("--percentage-threshold", type=_non_negative_decimal, default=Decimal("0.10"), help="minimum proportional YTD variance for review, e.g. 0.10")
35
+ review.add_argument("--reconciliation-tolerance", type=_non_negative_decimal, default=Decimal("0.01"), help="maximum permitted GL/subledger difference")
36
+ return parser
37
+
38
+
39
+ def main(argv: list[str] | None = None) -> int:
40
+ parser = build_parser()
41
+ try:
42
+ args = parser.parse_args(argv)
43
+ except SystemExit as exc:
44
+ # argparse exits with 2 on usage errors, but this tool's exit contract
45
+ # reserves 2 for a pack needing review; remap usage errors to 1.
46
+ # --help and -h exit with 0 and must keep doing so, so re-raise
47
+ # anything that is not a usage error.
48
+ if exc.code == 2:
49
+ return 1
50
+ raise
51
+ if args.command != "review": # pragma: no cover - argparse currently has one subcommand.
52
+ parser.error("unknown command")
53
+ # write_review_pack replaces its three destinations and deletes what it
54
+ # parked aside. If a source file IS one of those destinations, that source
55
+ # is destroyed and the pack still records a source_sha256 for it, so the
56
+ # provenance chain points at evidence that no longer exists. Refuse before
57
+ # anything is read or written. resolve() so case, "." segments and symlinks
58
+ # normalise to one key; the destinations need not exist yet.
59
+ destinations = {(args.output / name).resolve() for name in _PACK_FILE_NAMES}
60
+ for flag, source in (
61
+ ("--current", args.current),
62
+ ("--prior", args.prior),
63
+ ("--mapping", args.mapping),
64
+ ("--subledger", args.subledger),
65
+ ("--review-note", args.review_note),
66
+ ):
67
+ if source is not None and source.resolve() in destinations:
68
+ print(
69
+ f"close-control: output error: {flag} {source} is inside --output and "
70
+ f"shares a generated pack file name; the run would destroy it.",
71
+ file=sys.stderr,
72
+ )
73
+ return 1
74
+ try:
75
+ pack = review_close(
76
+ current_path=args.current,
77
+ prior_path=args.prior,
78
+ mapping_path=args.mapping,
79
+ subledger_path=args.subledger,
80
+ acknowledgement_path=args.review_note,
81
+ absolute_threshold=args.absolute_threshold,
82
+ percentage_threshold=args.percentage_threshold,
83
+ reconciliation_tolerance=args.reconciliation_tolerance,
84
+ )
85
+ except (ControlInputError, ValueError) as exc:
86
+ print(f"close-control: input error: {exc}", file=sys.stderr)
87
+ return 1
88
+ try:
89
+ outputs = write_review_pack(pack, args.output)
90
+ except (OSError, ValueError) as exc:
91
+ # An unusable --output is a caller error, not a crash: report it on the
92
+ # same failure path as a malformed input instead of a raw traceback.
93
+ # ValueError covers a render that cannot be encoded, which is not an
94
+ # OSError and used to escape as a traceback.
95
+ print(f"close-control: output error: {exc}", file=sys.stderr)
96
+ return 1
97
+ print(f"close-control: {pack.status}; {len(pack.exceptions)} exception(s)")
98
+ for name, path in outputs.items():
99
+ print(f" {name}: {path}")
100
+ return 0 if pack.status == "PASS" else 2
101
+
102
+
103
+ if __name__ == "__main__":
104
+ sys.exit(main())