-
Notifications
You must be signed in to change notification settings - Fork 0
feat(reconciliation): add maker-checker exception resolution command #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0fa5534
f9d5df5
e1ebb9f
42640b6
aea0bbc
b9472e2
ac0a3ca
4aff0dc
8a80142
2345eb3
523b6fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # ADR 0062: Reconciliation exception resolution uses named immutable maker-checker commands | ||
|
|
||
| - Status: Proposed | ||
| - Date: 2026-09-02 | ||
| - Bounded context: Bank Reconciliation / Evidence and Audit | ||
| - Depends on: ADR 0058 reconciliation-run command evidence and the stacked reconciliation lifecycle transition | ||
| - Follow-ups: #34/#9 for authenticated HTTP command authorization; #44 for least-privilege database capability | ||
|
|
||
| ## Context | ||
|
|
||
| A reconciliation exception is an accounting control fact. The existing `reconciliation_exception.resolution_status_code` records `open`, `resolved`, or `superseded`, but a mutable status value by itself does not prove who reviewed the exception, why the reviewer accepted the outcome, which retained evidence supported it, when the decision became effective, or whether an idempotent command and its integration event committed together. | ||
|
|
||
| The reconciliation-run lifecycle therefore correctly treated every exception as a blocker. The next bounded buyer-visible gap is to make terminal exception state authoritative without weakening that fail-closed boundary. This decision does not create or post journals, alter ledger balances, close periods, or change accounting policy. | ||
|
|
||
| IFRS Accounting Standards do not prescribe an application-level bank-reconciliation command protocol. IAS 7 remains relevant to cash and cash-equivalent reporting, and the IASB is actively researching Statement of Cash Flows improvements in 2026; this ADR does not characterize the implementation as IFRS compliance. The control design instead treats maker-checker separation, retained evidence, transaction atomicity, tenant isolation, and replay safety as system control requirements. | ||
|
|
||
| ## Decision | ||
|
|
||
| Introduce `accounting_core.reconciliation_exception_resolution_command` as immutable command evidence. A command is tenant/run/exception scoped and contains a shared reconciliation idempotency key, terminal target (`resolved` or `superseded`), retained evidence reference plus exact SHA-256 digest, distinct reviewer actor, purpose, effective time, database recording time, and a database-derived command hash. | ||
|
|
||
| The command uses the existing tenant-wide `reconciliation_command_identity` namespace with the new `exception_resolution` family. Opening, run-finalization, and exception-resolution commands therefore cannot silently reuse the same idempotency key for different meanings. | ||
|
|
||
| Exception resolution follows the reconciliation-run lifecycle serialization lock. The database verifies that the run is still `evaluating` or `review_required`, the exception is still `open`, the reviewer is not the exception owner, and the decision does not predate the exception. One exception may have only one terminal resolution command. Exact retries replay the retained command receipt; changed retries require a new idempotency key. | ||
|
|
||
| A raw `UPDATE reconciliation_exception SET resolution_status_code = ...` is not authority. The database permits the `open` to terminal transition only when exactly one matching resolution command already exists in the same transaction. A deferred constraint requires the command and terminal status to commit as a pair, and terminal exception evidence becomes immutable afterward. Tenant row-level security is forced and `PUBLIC` has no table privilege. | ||
|
|
||
| The application command writes the resolution command, terminal exception state, and transactional outbox event in one PostgreSQL transaction. `reconciliation_exception_resolved` and `reconciliation_exception_superseded` carry the exception aggregate reference and immutable command hash. The eventual restricted database capability in #44 must expose the named command boundary rather than grant raw insert/update/outbox privileges. | ||
|
|
||
| Run finalization is changed from the interim “any exception blocks” rule to: every exception must be terminal and must have exactly one durable resolution command whose target agrees with the terminal status. The application additionally includes the stable ordered resolution-command state—exception id, target, retained evidence reference/hash, and command hash—in the reconciliation transition snapshot. Changing resolution evidence therefore changes the run-finalization digest. | ||
|
|
||
| No statement line, imported bank evidence, probabilistic suggestion, or LLM output can invoke this command automatically. It is a reviewed accounting-control decision. A resolved exception also does not post a journal; any correcting entry remains a separate authorized accounting command with its own evidence and idempotency boundary. | ||
|
|
||
| ## Consequences | ||
|
|
||
| The run lifecycle can now progress after exceptions are explicitly reviewed without treating mutable status as evidence. Auditors can trace a terminal exception to a named reviewer decision, retained evidence digest, exact command hash, and emitted event. Concurrent finalization and exception resolution serialize on the same aggregate lock, while PostgreSQL `REPEATABLE READ` keeps the lifecycle evidence reads on one transaction snapshot. | ||
|
|
||
| The forward migration is unreleased and stacked. It must pass real PostgreSQL installation, rollback/restore rehearsal, direct-SQL bypass tests, concurrency tests, exact replay/conflict tests, forced-RLS tests, 100% owned production statement/branch coverage, security scans, package/SBOM/provenance gates, and independent review before integration. Databases containing pre-migration terminal exception rows require explicit review because those rows predate the named-command authority; they must not be silently grandfathered as valid resolution evidence. | ||
|
|
||
| ## Alternatives rejected | ||
|
|
||
| **Keep `resolution_status_code` as the only fact.** Rejected because a privileged or accidental update could manufacture terminal control state without durable reviewer provenance. | ||
|
|
||
| **Store free-form resolution notes on the exception row.** Rejected because notes do not provide immutable command identity, exact replay, maker-checker enforcement, or an atomic integration event. | ||
|
|
||
| **Let reconciliation finalization infer that every non-`open` status is approved.** Rejected because it promotes historical mutable state into authority and makes the finalization snapshot blind to the evidence that justified the exception decision. | ||
|
|
||
| **Post a correcting journal as part of exception resolution.** Rejected because bank reconciliation review and General Ledger posting are distinct bounded-context commands. Resolution may identify that a correcting journal is needed, but posting must remain a separate authorized double-entry operation. | ||
|
|
||
| ## Verification and traceability | ||
|
|
||
| Acceptance evidence must demonstrate at least these cases on real PostgreSQL: raw terminal status update fails; owner-as-reviewer fails; decision before exception time fails; valid command changes status and emits the matching outbox event atomically; exact replay returns the same receipt; changed replay conflicts; terminal exception evidence cannot be rewritten; a terminal status without matching command still blocks run reconciliation; a terminal status with matching command can proceed when all other bridge/review controls pass; tenant RLS and shared command idempotency remain enforced. | ||
|
Comment on lines
+49
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This commit changes reconciliation authority from “every exception blocks finalization” to maker-checker terminal commands, but none of AGENTS.md reference: AGENTS.md:L30-L30 Useful? React with 👍 / 👎. |
||
|
|
||
| Primary-source alignment used for this decision: | ||
|
|
||
| - International Accounting Standards Board. (2026). *Statement of Cash Flows and Related Matters*. IFRS Foundation. https://www.ifrs.org/projects/work-plan/statement-of-cash-flows-and-related-matters/ | ||
| - International Organization for Standardization. (2022). *ISO/IEC 27001:2022 Information security, cybersecurity and privacy protection—Information security management systems—Requirements*. ISO. https://www.iso.org/standard/27001 | ||
| - National Institute of Standards and Technology. (2020, updated 2025). *Security and Privacy Controls for Information Systems and Organizations (NIST SP 800-53 Rev. 5), AC-5 Separation of Duties*. U.S. Department of Commerce. https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final | ||
| - PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SET TRANSACTION*. https://www.postgresql.org/docs/18/sql-set-transaction.html | ||
| - PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: Row Security Policies*. https://www.postgresql.org/docs/18/ddl-rowsecurity.html | ||
|
|
||
| These references support reporting context, segregation-of-duty intent, transactional snapshot semantics, and tenant-access enforcement. They are design inputs, not certification or compliance claims. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,37 @@ | |
|
|
||
| from pathlib import Path | ||
|
|
||
| from .persistence import apply_foundation_migration as _apply_foundation_migration | ||
| from .core import AccountingValidationError | ||
| from .persistence import ( | ||
| _import_psycopg, | ||
| apply_foundation_migration as _apply_foundation_migration, | ||
| ) | ||
|
|
||
|
|
||
| def apply_foundation_migration(database_url: str, migration_path: Path) -> None: | ||
| """Apply the complete checked-in foundation chain through the canonical loader.""" | ||
| resolution_migration_path = ( | ||
| migration_path.parent / "0020_reconciliation_exception_resolution_command.sql" | ||
| ) | ||
| if not resolution_migration_path.is_file(): | ||
| raise AccountingValidationError( | ||
| "Reconciliation exception-resolution command migration is missing at " | ||
| f"{resolution_migration_path}. Restore " | ||
| "database/migrations/0020_reconciliation_exception_resolution_command.sql, " | ||
| "then retry." | ||
| ) | ||
| _apply_foundation_migration(database_url, migration_path) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Callers that use the existing Useful? React with 👍 / 👎. |
||
| psycopg = _import_psycopg() | ||
| try: | ||
| with psycopg.connect( | ||
| database_url, autocommit=True, cursor_factory=psycopg.ClientCursor | ||
| ) as connection: | ||
| connection.execute(resolution_migration_path.read_text(encoding="utf-8")) | ||
|
Comment on lines
14
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| except Exception as error: | ||
| raise AccountingValidationError( | ||
| "Reconciliation exception-resolution migration failed. Inspect the PostgreSQL " | ||
| "error, restore a clean database, then retry the complete foundation migration." | ||
| ) from error | ||
|
|
||
|
|
||
| __all__ = ["apply_foundation_migration"] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Required release records are missing
The new public command changes reconciliation authority, but
CHANGELOG.mdandSTANDARD_TRACEABILITY.mdomit it. Repository rules require both updates for authority changes.Was this helpful? React with 👍 or 👎 to provide feedback.