Changelog

Every feature, fix, and improvement — documented as we ship.

LINES OF CODE
124,927
RELEASES
135
API ENDPOINTS
198
API 56,686 Portal 37,163 Docs 11,115 Site 19,963 31 API resources

Most software ships updates without telling you what changed. You find out when something breaks or feels different.

Accounting software shouldn't work that way. When your books are involved, you deserve to know exactly what changed, when, and why.

Every entry here is written by a human, not generated. If we ship it, we document it.

Feature New capability
Fix Bug resolved
Improvement Existing feature enhanced
Breaking API or behaviour change
v5.26.0 9 March 2026 Latest Improvement

Pre-M6 Type Safety — Backend any Elimination

This release completes the pre-M6 type safety pass across the backend. Every any type in the route layer, service layer, and infrastructure files has been addressed — either eliminated with a concrete type, or suppressed with a documented justification where TypeScript cannot express the boundary.


What Changed

AuditLogger interface introduced. The request-scoped audit decorator previously used a null as any placeholder. A typed AuditLogger interface now gives the decorator a proper zero value — a no-op implementation that silently discards calls on unauthenticated routes and is overridden with the real RLS-aware logger for all tenant-scoped requests.

SQL parameter arrays typed throughout. Dynamic query builders that accumulated parameters into any[] now use (string | number | boolean | null)[], matching the actual values pushed into each array.

Stored JSON blob interfaces. The ODCE document import pipeline (bill imports and invoice imports) now has a typed StoredDocumentRow interface covering both the standalone upload path and the migration wizard nested path. The aging report contact maps now use a typed AgingInvoiceEntry interface.

Stripe SDK gaps handled correctly. Fields absent from the Stripe TypeScript definitions (current_period_end, trial_end, subscription on Invoice) are now accessed via targeted intersection types rather than as any casts.

Helper function signatures corrected. Standalone async helpers taking a database client now use PoolClient rather than ReturnType<typeof Object> with inline casts. The awkward (dbClient as { query: Function }) double-cast pattern is eliminated across the ODCE route files.

Catch clauses typed. All catch (err: any) clauses are now catch (err: unknown) with appropriate narrowing via instanceof Error or structural checks for database error codes.

Two justified suppressions documented. decimal-config.ts retains as any for the ESM/CJS interop boundary — no TypeScript construct can express “this import may carry a .default wrapper” without losing the constructable class type on one branch. invoice-service.ts retains any[] on the invoices return field — the caller performs a deliberate cast to an internal row type, and tightening the return breaks that boundary. Both are dual-suppressed with lint-ignore and eslint-disable comments explaining the rationale.


Files Affected

20 backend files across routes, services, and infrastructure. No SQL, no financial arithmetic, no transaction boundaries, no schema changes, no API response shape changes. Build passes clean. Lint reduces from 115 errors to 1 (suppressed, irreducible).


Kernel Integrity

No kernel invariants were modified. All changes are type annotations applied above the financial kernel. The compiled JavaScript output is identical to the previous release in all affected files.

v5.25.0 9 March 2026 Improvement

Kernel Milestone 5 — Schema-Derived Categorical Boundary

Kernel Milestone: 5 of 6 Previous: Milestone 4 — Provenance (v5.22.0) Next: Milestone 6 — Append-Only Cryptographic Ledger

System Impact
-------------
Ledger integrity:         strengthened — unvalidated row access eliminated
Ledger mutation:          unchanged
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       preserved
Invariant Change
----------------
Every database query result in every route file now passes through a
strict Zod schema before entering the domain layer. The raw untyped
boundary is closed system-wide.

Why This Matters

The database boundary is the most dangerous trust boundary in any financial system. When raw query results flow directly into domain logic without validation, two classes of failure become invisible: driver coercion bugs (where the database returns data in a form different from what the application assumes) and migration drift (where a column is added, renamed, or removed without the application noticing).

Milestone 4 established the boundary pattern in the invoice service and proved it mechanically. Milestone 5 propagates that pattern across every remaining route file in the system. The result is that no query result can reach the domain layer without passing through a declared schema with known shape, known types, and known nullability.

The secondary value of this milestone is diagnostic. Strict schemas make the application’s assumptions explicit. When those assumptions were wrong, as they were in three places in the dividend voucher endpoint, the compiler refused to build until they were corrected. Three pre-existing type defects that had been silently masked by untyped row access were surfaced and fixed as a direct consequence of this work. None of them were new bugs introduced by the milestone. All of them were real.


Boundary Propagation

Route File Coverage

AX-BND-001 boundary validation has been applied to all remaining route files. Each file received a full schema sweep: every distinct query shape was assigned a dedicated Zod schema declared .strict(), and every result set passes through .parse() before any field access.

  • Directors 4 schemas covering all query shapes. 5 parse sites. Zero bare row accesses remain.

  • Contacts 6 schemas including contact metadata, invoice summary, and count aggregates. 8 parse sites. Zero bare row accesses remain.

  • Accounts 6 schemas covering account detail, balance aggregates, and type lookups. 11 parse sites. Zero bare row accesses remain.

  • Categorisation Rules 14 schemas covering the full TMADD engine query surface: rule list, engine evaluation, account lookups, insert/update/delete signals, match log, and next-priority computation. 25 parse sites. Zero bare row accesses remain. The categorisation engine’s internal RuleRow interface is retained for TMADD matching logic; CatRuleEngineRow is the boundary gate that all DB results pass through before entering the engine.

  • Dividends 14 schemas covering board minutes, declared dividends, director queries, retained earnings, voucher data, and date/sequence lookups. 37 parse sites. Zero bare row accesses remain.

BIGINT Discipline

The dividend route handles amount_pence as a PostgreSQL BIGINT, which the database driver returns as a string rather than a number. This driver behaviour is now explicitly encoded in the schema boundary: amount_pence carries type z.string() at the schema layer, and parseInt(..., 10) conversion happens at each consumption site. Ordinary integer fields retain z.number().int() with no conversion. The boundary is now truthful to what the driver actually produces.

Pre-Existing Defects Surfaced

Three type defects in the dividend voucher endpoint were uncovered and fixed as a direct consequence of the boundary schema work. All three had been silently masked by untyped row access. None were introduced by this milestone.

  • Director name nullability The voucher data structure requires a non-null director name. The underlying join guarantees this at runtime, but the schema correctly declares the column nullable to reflect the actual database type. A null-coalescing default is applied at the consumption site, making the intent explicit where previously it was invisible.

  • Company number null-to-undefined coercion The voucher organisation type declares company number as string | undefined. The database column is nullable and the schema reflects this. The coercion from null to undefined is now explicit at the construction site.

  • Address field null-to-undefined coercion All six address fields carry the same pattern. Each is nullable in the schema, correctly reflecting the column definition, and each is coerced to undefined at the construction site to satisfy the voucher organisation contract.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Driver coercion bug in director routesOpenClosed (Class M)Runtime schema validation
Migration drift in contact queriesOpenClosed (Class M)Runtime schema validation
Unregistered column propagation in account routesOpenClosed (Class M).strict() schema parse
BIGINT string coercion invisible in dividend routesOpenClosed (Class M)Runtime schema + Compiler
Untyped voucher construction in dividend routesOpenClosed (Class M)Compiler type check

Kernel Closure Statement

Axiom: AX-BND-001 — Schema-Derived Runtime Validation

Status: CLOSED system-wide as of SpeyBooks v5.25.0

Enforcement layers verified:
  Runtime layer    — Zod .strict() parse on every query result set
                     across all route and service files
  Compiler layer   — Typed mapper functions reject shape mismatches
                     at build time; three pre-existing defects fixed
  CI layer         — check-db-boundary.ts scans all route and service
                     files; exits 1 on any unvalidated row access

CI verification:
  verify-axiom-coverage.sh      — PASS (21 delivered axioms, 0 violations)

Residual risk:
  None within the runtime boundary domain. Egress shape invariance
  (AX-BND-002) remains planned for post-launch.

Next dependent axiom:
  AX-LED-001 — Canonical Time Axis (Milestone 6)

Security Posture Change

The database boundary was previously a Class O boundary in the route layer: correct behaviour depended on developer discipline in accessing the right fields with the right types. That is now Class M. Every query result is validated by a declared schema before any field is accessed. A query returning an unexpected shape causes an immediate 500 rather than silent propagation. This eliminates an entire class of potential migration-related data corruption from the threat model.


Verification Record

Pre-flight:
  0 lint violations across all five route files
  tsc: clean build after three type defects corrected

Post-commit:
  V1  directors.ts    — 4 schemas, 5 parse sites, 0 bare row accesses
  V2  contacts.ts     — 6 schemas, 8 parse sites, 0 bare row accesses
  V3  accounts.ts     — 6 schemas, 11 parse sites, 0 bare row accesses
  V4  categorisation-rules.ts — 14 schemas, 25 parse sites, 0 bare row accesses
  V5  dividends.ts    — 14 schemas, 37 parse sites, 0 bare row accesses

Axiom coverage gate:
  verify-axiom-coverage.sh — PASS
  Delivered axioms: 21
  Violations: 0
  Execution time: 9s

Architectural Context

The six kernel milestones form a dependency chain. Milestones 1 and 2 established that the ledger cannot be corrupted from outside the system (tenant isolation, immutability, state machines). Milestones 3 and 4 established that the ledger cannot be corrupted from inside the system (monetary arithmetic, temporal determinism, identifier correctness, query ordering, typed error handling). Milestone 5 closes the final trust boundary between the database and the domain layer, ensuring that data entering the domain is always of known shape and type.

Milestone 6 completes the chain by making the ledger append-only and cryptographically verifiable, so that the integrity guarantees established in Milestones 1 through 5 can be independently audited over time.


Operational Impact

  • Zero unvalidated query results enter the domain layer across any route in the system
  • Three pre-existing type defects in the dividend voucher endpoint are resolved
  • Build-time type errors now surface schema mismatches that were previously silent
  • BIGINT columns are handled with explicit documented coercion, not silent assumption
  • CI boundary gate prevents any future unvalidated row access from reaching production

Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4ProvenanceComplete
M5Schema-Derived Categorical BoundaryComplete
M6Append-Only Cryptographic LedgerPending

Files Changed

Backend:

  • api/src/routes/directors.ts — 4 Zod schemas, 5 parse sites, accounting. prefix corrected throughout
  • api/src/routes/contacts.ts — 6 Zod schemas, 8 parse sites, accounting. prefix corrected throughout
  • api/src/routes/accounts.ts — 6 Zod schemas, 11 parse sites, accounting. prefix corrected throughout
  • api/src/routes/categorisation-rules.ts — 14 Zod schemas, 25 parse sites, ORDER BY determinism annotated
  • api/src/routes/dividends.ts — 14 Zod schemas, 37 parse sites, BIGINT discipline applied, 3 type defects corrected

Milestone 6 will close the final architectural gap: append-only cryptographic ledger integrity, making every guarantee established across Milestones 1 through 5 independently auditable over time.

v5.24.0 8 March 2026 Improvement

M4 Complete — Temporal Determinism, DB Boundary Validation & Unified Verification Harness

Kernel Milestone: 4 of 6 Previous: Milestone 3 — Monetary Domain Partition & Financial Kernel v2.2 (v5.18.0) Next: Milestone 5 — Boundary Propagation

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          unchanged
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       preserved
Invariant Change
----------------
Temporal values crossing the DB/domain boundary are now typed as strings
throughout the financial domain type system, eliminating the driver's
timezone-sensitive Date coercion path. DB query results are validated against
Zod schemas at the service boundary before any mapper executes, with compiler
and CI enforcement preventing regressions.

Why This Matters

M4 addresses two categories of silent failure that the original codebase tolerated: temporal ambiguity and unvalidated DB results. The database driver coerces date and timestamp columns to JavaScript Date objects using server local time — a source of timezone-dependent behaviour that is invisible in tests but surfaces in production. By declaring all temporal fields as string in the financial domain type system, the compiler rejects any mapper that accepts a driver-coerced object without explicit normalisation. This converts a class of silent runtime bugs into compile-time errors.

The DB boundary work closes a different gap: prior to M4, query results were accessed with no runtime verification of the column shapes returned. A schema migration adding or renaming a column would silently produce missing values in mapped objects, surfacing only as display errors or incorrect calculations. The Zod schema layer validates every result row before the mapper executes. Unexpected columns fail immediately rather than passing through. The satisfies keyword on the mapper return replaces an unsafe cast, allowing the compiler to verify mapper completeness against the domain type.

AX-BND-001 is closed in M4 at the standard and enforcement level: the runtime schema layer, compiler-checked mapper pattern, and CI boundary gate are all delivered, with the invoice service as the reference implementation. M5 propagates this closed pattern across the remaining services. The reference implementation demonstrates the three-layer enforcement model: Zod runtime validation, TypeScript compiler checking via satisfies, and a CI gate that scans service and route files for unvalidated boundary crossings.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Temporal values coerced to server-local Date by driverOpenClosed (Class M)Domain type temporal purge — compiler gate
Mapper accepting driver-coerced objects without normalisationOpenClosed (Class M)Compiler rejects type mismatch at build
Query results passing through with unmapped columnsOpenClosed (Class M)Zod .strict() schemas — unexpected columns fail immediately
Mapper incompleteness hidden by unsafe castOpenClosed (Class M)satisfies check — compiler verifies mapper completeness
Unvalidated boundary in route handlersOpenClosed (Class M)CI gate scans both services and routes
Scattered verification gates with no single entry pointOpenClosed (Class O)Unified harness — single command, consolidated output

Proof Anchors

Compiler layer — AX-TMP-001:
  Domain type system: kernel temporal fields now cross the DB/domain boundary
  as strings rather than Date, with compiler enforcement surfacing any mapper
  that attempts to pass driver-coerced temporal objects through unchanged.

Compiler layer — AX-BND-001:
  Invoice service mapper: `satisfies` domain type check — compiler verifies
  every field against the domain type; unsafe cast removed.

Runtime layer — AX-BND-001:
  Boundary schemas with .strict() validate all invoice query results at the
  service boundary before mapper execution. Unexpected columns fail
  immediately as P1.

CI layer — AX-BND-001:
  Boundary CI gate scans service and route files for query result accesses
  without a nearby schema parse call. Exits non-zero, blocking build.

Security Posture Change

Prior to this release, temporal field handling and DB result shapes were both Class O guarantees — dependent on developer discipline, driver behaviour, and TypeScript generics that the runtime did not enforce. A driver update, a schema migration, or a timezone misconfiguration could silently corrupt financial values with no observable error at the point of failure. Both are now Class M: the compiler enforces temporal typing, Zod enforces boundary shape, and the CI gate prevents regressions.


Unified Verification Harness

scripts/run-harness.sh is the single entry point for all kernel verification gates. Previously, the eight verification scripts had to be run individually with no consolidated output. The harness runs them in dependency order — compiler first, database gates last — and produces a summary table showing each gate’s pass/fail status and elapsed time. Exit 0 means all gates passed.

bash scripts/run-harness.sh              # all 8 gates
bash scripts/run-harness.sh --fast       # skip database gates (no DB required)
bash scripts/run-harness.sh --sql-only   # database gates only (post-migration)
GateAxiomType
TypeScript compilerPrerequisiteStatic
Ordering determinism gateAX-QRY-001Static
Boundary validation gateAX-BND-001Static
Constraint coverage checkAX-ERR-001Static
Axiom coverage verifierAll axiomsStatic
Double-entry conservationAX-ALG-001Database
Monetary constraintsAX-RND-001–005Database
RLS policy symmetryAX-TEN-002Database

Verification Record

Post-commit:
  V1  pnpm build — clean, 0 TypeScript errors
  V2  Boundary CI gate — PASS, 0 violations
  V3  Ordering determinism gate — PASS, 0 violations, 113 files scanned
  V4  Constraint coverage check — PASS, 74 constraints, 100% coverage
  V5  Axiom coverage verifier — PASS, 21 delivered axioms, 0 violations
  V6  Double-entry conservation — PASS, 6/6 checks
  V7  Monetary constraints — PASS, 12/12 checks
  V8  RLS policy symmetry — PASS, 5/5 checks
  Full harness: 8/8 gates passed — mathematically closed

M4 Axiom Delivery Record

AxiomNameVersion
AX-CON-001Sequential Identifier Correctnessv5.19.0
AX-TMP-001Temporal Determinism Standardv5.20.0
AX-CON-002Idempotency Contractv5.21.0
AX-QRY-001Ordering Determinismv5.22.0
AX-BND-001Schema-Derived Runtime Validationv5.23.0
M4 Closure Statement
--------------------
M4 is now closed. The milestone's five axioms establish deterministic identity,
temporal representation, retry semantics, query ordering, and validated DB
boundary entry into the financial domain. Together they remove the remaining
silent provenance and replay ambiguities left open after M3.

Operational Impact

Temporal safety. Every temporal value from the database is now a string at the type boundary. No mapper can silently accept a timezone-coerced object — the compiler flags it at build time.

Boundary failures are loud. A schema mismatch at the DB boundary produces an immediate 500 identifying the exact field that failed. There is no silent corruption path.

Single harness command. One command verifies the entire kernel — 8 gates covering compiler output, static analysis, constraint coverage, axiom registry integrity, double-entry conservation, monetary invariants, and RLS symmetry — with a consolidated pass/fail summary.

Ledger readiness. With M4 complete, the query layer is deterministic, temporal values are normalised, and service boundaries are validated. The provenance foundations required for the M6 cryptographic audit trail are in place.


Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4 — AX-CON-001Sequential Identifier CorrectnessComplete
M4 — AX-TMP-001Temporal DeterminismComplete
M4 — AX-CON-002Idempotency ContractComplete
M4 — AX-QRY-001Ordering DeterminismComplete
M4 — AX-BND-001Schema-Derived Runtime ValidationComplete
M5Boundary PropagationPending
M6Append-Only Cryptographic LedgerPending
v5.23.0 8 March 2026 Improvement

AX-BND-001 — Schema-Derived Runtime Validation at the DB Service Boundary

Kernel Milestone: 4 of 6 (Part 5 of 5) Previous: Milestone 4 (Part 4) — Ordering Determinism — AX-QRY-001 (v5.22.0) Next: Milestone 4 Complete — Temporal Determinism, DB Boundary Validation & Unified Verification Harness (v5.24.0)

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          unchanged
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       strengthened — boundary failures now fail loud
Invariant Change
----------------
DB query results at the invoice service boundary are now validated against
strict Zod schemas before any mapper executes. An unregistered column, a
type mismatch, or a missing required field causes immediate parse failure.
The boundary can no longer pass corrupted financial state silently into the
domain layer.

Why This Matters

The database driver returns database values as raw JavaScript primitives: integer columns as strings to preserve precision, decimal columns as strings for scale preservation, date columns as ISO strings. TypeScript generics on query calls impose no constraint on the actual runtime values returned. A schema migration changing a column type, a driver version update altering coercion behaviour, or a wildcard column selection after a schema addition would all compile cleanly, pass tests, and process incorrect data until a financial discrepancy surfaced downstream. There would be no error. The wrong values would simply flow through the mapper into domain objects and eventually into financial calculations.

This is the DB boundary problem. The mapper assumes the query result has the right shape. Nothing checks that assumption at runtime. The only defence was the TypeScript generic — and that generic is a lie, because the driver does not enforce it.

AX-BND-001 closes this gap. Every invoice query result now passes through a Zod schema before the mapper can access it. The schemas are declared .strict() — an unregistered column causes an immediate parse failure rather than passing through silently. A ZodError at this boundary is not caught and not handled: it propagates as a 500 and is treated as a P1 incident. The boundary fails loud rather than passing corrupted state to the domain layer.

The unsafe type cast on the invoice mapper — the clearest possible signal of an unvalidated boundary — has been replaced with a satisfies check. This requires the TypeScript compiler to verify mapper completeness at every build. If the domain type gains a required field and the mapper does not provide it, the build fails. The escape hatch is gone.


What Was Delivered

Boundary schemas define strict Zod schemas for each invoice query result shape. Each schema enforces the actual driver output types: integer-as-string monetary values are validated by format, date-as-string values are validated against the ISO date pattern in accordance with AX-TMP-001. Schemas are .strict() — an unregistered column in the result causes an immediate parse failure. Legacy computed columns from before the M3 pence migration are declared optional before .strict() to maintain compatibility with existing queries.

The CI boundary gate is a new static analysis script that scans all service and route files for query result accesses that lack a nearby schema parse call, and exits non-zero if any are found. Route handlers are included in the scan to close the bypass vector where a handler could access query results directly without going through a service. This gate runs on every build and rejects any new unvalidated boundary crossing before it can reach production.

The row type definitions complete the AX-TMP-001 temporal purge. All Date types across every row interface are now typed as string. The deprecated sequence columns that were superseded by the AX-CON-001 document sequencing migration are marked accordingly. The invoice line row interface has been corrected to include the canonical monetary pence columns introduced in M3 that were missing from the original definition.


Boundary Schema Design Decisions

.strict() on all schemas. A schema without .strict() silently accepts extra fields. This means a migration adding a column would pass through unnoticed — exactly the failure class AX-BND-001 is designed to prevent.

ZodError must not be caught at the boundary. A boundary parse failure means the production database schema has diverged from the application’s expectations. This is a P1 incident. Catching the error would mask the incident and allow subsequent requests to continue processing corrupted data. ZodError propagates directly to the error handler.

satisfies replaces the unsafe cast. The previous cast hid the gap between what the mapper produced and what the domain type required. The satisfies check inverts this: the compiler verifies the mapper output covers every required field. If the type changes, the build fails at the mapper, not silently at runtime.

Reference implementation scope. AX-BND-001 is closed at the standard and enforcement machinery level in M4. The invoice service is the reference implementation. M5 propagates the pattern across the remaining services. The CI gate is already active — any new unvalidated boundary crossing will be caught before it reaches production.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Driver coercion producing wrong runtime typesOpenClosed (Class M)Zod schema parse at boundary
Schema migration silently corrupting mapped objectsOpenClosed (Class M).strict() — unexpected columns fail immediately
Mapper incompleteness hidden by unsafe castOpenClosed (Class M)satisfies check — compiler gate
Unvalidated boundary in route handlersOpenClosed (Class M)CI gate scans routes as well as services
Date objects crossing the DB/domain boundaryOpenClosed (Class M)Row type temporal purge — AX-TMP-001

Proof Anchors

Runtime layer — AX-BND-001:
  Boundary schema file: three strict Zod schemas for invoice queries.
  ZodError propagates as 500 — not caught.

Compiler layer — AX-BND-001:
  Invoice service mapper: `satisfies` domain type check replaces unsafe cast.
  Compiler verifies mapper completeness at every build.

CI layer — AX-BND-001:
  Boundary CI gate: scans service and route files for query result accesses
  without a nearby schema parse call. Exits non-zero, blocking build.
  Runs on every commit.

Compiler layer — AX-TMP-001 (completed):
  Row type definitions: all Date types replaced with string.
  Compiler rejects any mapper accepting driver-coerced Date objects.

Security Posture Change

Prior to this release, DB query result shapes were a Class O guarantee — dependent on developer discipline and TypeScript generics that the runtime did not enforce. A driver update, a schema migration, or a misconfigured query could silently produce wrong financial data with no observable error. This release moves the DB service boundary to Class M: Zod schemas enforce the expected shape at runtime, the compiler enforces mapper completeness, and the CI gate prevents any new unvalidated boundary from reaching production.


Verification Record

Pre-flight:
  Invoice service identified as primary reference implementation
  Row type audit: all temporal fields confirmed as string candidates
  Invoice line row gap identified: M3 canonical monetary columns missing
  from original row type definition

Patch application:
  Boundary schemas — 3 schemas, 4 primitive types, .strict() on all
  Invoice service — schema parse at boundary; satisfies check applied;
  unsafe cast removed; enum column cast narrowed
  Row type definitions — Date purge complete; deprecated columns marked;
  invoice line row corrected
  Domain types — inline contact summary type introduced; invoice number
  and created-by nullability corrected; all Date fields replaced with string

Post-commit:
  V1  pnpm build — clean, 0 TypeScript errors
  V2  Boundary CI gate — PASS, 0 violations
  V3  Axiom coverage verifier — PASS, 21 delivered axioms, 0 violations
  V4  Full harness (run-harness.sh) — 8/8 gates passed

Operational Impact

Boundary failures are loud. A schema mismatch produces an immediate 500 identifying the exact field and value that failed. There is no silent corruption path.

Migration safety. Any future schema migration changing a column type or renaming a column will be caught at the first request to hit the affected query — not discovered later through financial discrepancy investigation.

Compiler as enforcer. Adding a required field to the domain type without updating the mapper produces a build error, not a runtime gap.

CI as enforcer. The boundary gate runs on every build. No new unvalidated boundary crossing can reach production without a deliberate exemption. Zero ongoing maintenance cost.


Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4 — AX-CON-001Sequential Identifier CorrectnessComplete
M4 — AX-TMP-001Temporal DeterminismComplete
M4 — AX-CON-002Idempotency ContractComplete
M4 — AX-QRY-001Ordering DeterminismComplete
M4 — AX-BND-001Schema-Derived Runtime ValidationComplete
M5Boundary PropagationPending
M6Append-Only Cryptographic LedgerPending

Files Changed

New:

  • Boundary schema definitions — strict Zod schemas for invoice query results
  • CI boundary gate — static analysis script for unvalidated boundary crossings

Modified:

  • Row type definitions — AX-TMP-001 temporal purge complete; invoice line row corrected; deprecated columns marked
  • Invoice service — schema parse at boundary; satisfies check; unsafe cast removed
  • Domain types — inline contact summary introduced; invoice number and created-by nullability corrected; all Date fields replaced with string

The reference implementation pattern established here — strict boundary schema, satisfies mapper check, CI gate — is the template for service boundary validation across the remaining services in M5.

v5.22.0 8 March 2026 Improvement

Ordering Determinism — AX-QRY-001 Kernel Axiom Delivery

Kernel Milestone: 4 of 6 Previous: Milestone 4 (Part 3) — Idempotency Contract — AX-CON-002 (v5.21.0) Next: Milestone 4 (Part 5) — Schema-Derived Service Boundary — AX-BND-001

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          unchanged
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       strengthened — query layer now deterministic
Invariant Change
----------------
Every SELECT capable of returning multiple rows now carries an explicit
ORDER BY clause. PostgreSQL row ordering is no longer implicitly assumed
anywhere in the API surface.

Why This Matters

PostgreSQL makes no ordering guarantee without an explicit ORDER BY clause. A query executed twice against the same data can return rows in different sequences depending on internal page layout, vacuum activity, or index selection at execution time. For a general-purpose application this is a minor inconvenience. For a financial platform it is a structural problem.

SpeyBooks processes financial records that must be reproducible: ledger exports, reconciliation sequences, paginated transaction lists, dividend vouchers, VAT summaries. If the underlying row ordering is non-deterministic, two exports of the same data produced minutes apart may differ. Pagination may silently shift — a record appearing on page 2 in one request may not appear at all in the next. Reconciliation logic that processes rows sequentially may reach different conclusions on different days. None of this produces an error. It simply produces wrong results, intermittently, in ways that are extraordinarily difficult to diagnose after the fact.

AX-QRY-001 eliminates this class of defect entirely by making deterministic ordering a mechanically enforced invariant rather than a coding convention.


Ordering Determinism

AX-QRY-001 — What Was Enforced

A full audit of all 112 TypeScript files in the API surface identified every SELECT statement capable of returning more than one row. Each was classified into one of two categories:

Single-row lookups — queries returning exactly one row by primary key or unique constraint. These were corrected by adding an explicit LIMIT 1, documenting intent in code and allowing the PostgreSQL query planner to apply a single-row optimisation path.

Multi-row queries — list queries, join queries, set membership lookups, and aggregate queries grouped by a non-unique field. These were corrected by adding a deterministic ORDER BY clause, with a stable tie-breaker column (id) ensuring total ordering even when the primary sort field contains duplicate values.

Ordering Conventions Established

The following deterministic ordering conventions are now consistently applied across the codebase:

  • Transactions, invoices, dividendsORDER BY created_at DESC, id DESC
  • Contacts, accounts, directorsORDER BY name/code, id
  • Administrative listsORDER BY created_at DESC, id DESC
  • Report aggregatesORDER BY account_type or ORDER BY account_id
  • Audit and event log queriesORDER BY created_at DESC, id DESC

The CI Gate

A permanent TypeScript AST gate (scripts/check-order-by.ts) was introduced as part of this milestone. The gate uses a full TypeScript AST parser combined with a SQL AST parser to analyse every query call in the codebase. It is not regex-based — it understands TypeScript syntax, template literals, and SQL grammar. It classifies each SELECT and fails the build if any multi-row query lacks a deterministic ORDER BY.

The gate correctly exempts genuine single-row patterns: pure aggregates without GROUP BY, scalar function calls, SELECT 1 literals, LIMIT 1 queries, and primary key lookups. Legacy migrations predating AX-QRY-001 are held in a versioned exemption set.

The gate is now a permanent part of the build. Every query written from this point forward is automatically checked before it can reach production.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Non-deterministic row ordering in multi-row queriesOpenClosed (Class M)TypeScript AST CI gate
Silent pagination drift across requestsOpenClosed (Class M)Deterministic ORDER BY on all list queries
Non-reproducible ledger export sequencesOpenClosed (Class M)Consistent ordering conventions across all financial queries
Unintentional multi-row fetch on single-row lookupsOpenClosed (Class M)Explicit LIMIT 1 on all primary key and unique lookups

Kernel Closure Statement

Axiom: AX-QRY-001 — Ordering Determinism

Status: CLOSED as of SpeyBooks v5.22.0

Enforcement layers verified:
  Application layer    — LIMIT 1 on all single-row lookups
  Application layer    — ORDER BY with stable tie-breaker on all multi-row queries
  CI gate              — scripts/check-order-by.ts (TypeScript AST + SQL AST)

CI verification:
  check-order-by.ts    — PASS (0 violations across 112 files, 60 migration files)

Residual risk:
  None within the query ordering domain.
  Raw SQL executed outside the standard query call pattern is not scanned.
  No such patterns exist in the current codebase.

Next dependent axiom:
  AX-BND-001 — Schema-Derived Service Boundary

Security Posture Change

Prior to this release, row ordering in multi-row queries was a Class O guarantee — dependent on PostgreSQL’s internal execution behaviour and developer discipline. It was not enforced and could not be verified programmatically. This release moves query ordering to Class M: the CI gate rejects any query without an explicit ORDER BY, making it impossible for a non-deterministic multi-row query to reach production without a deliberate exemption.


Verification Record

Pre-flight:
  167/167 violations identified by TypeScript AST gate
  Triage complete: 123 single-row lookups, 44 genuine multi-row queries

Patch application:
  Pass 1: 74 fixes applied across 28 files
  Pass 2: 78 fixes applied across 33 files
  Pass 3:  7 fixes applied across  7 files (duplicate query instances)
  Total:  167 queries corrected across 43 files

Post-commit:
  V1  pnpm build — clean, 0 TypeScript errors
  V2  check-order-by.ts — PASS, 0 violations
  V3  112 TypeScript files scanned, 60 migration files scanned

Adversarial review:
  9.5 / 10 Production Gold

Architectural Context

AX-QRY-001 is the fourth axiom delivered under Milestone 4 (Provenance). The previous three axioms established sequential identifier correctness (AX-CON-001), temporal determinism (AX-TMP-001), and idempotency (AX-CON-002). Together they ensure that records are created with correct identifiers, at correct times, and exactly once.

AX-QRY-001 extends this foundation to the read path: records are now not only written deterministically but retrieved deterministically. This is the prerequisite for AX-BND-001 (Schema-Derived Service Boundary), which enforces type correctness at service interfaces — a guarantee that depends on queries returning rows in a predictable, stable order.

More significantly, AX-QRY-001 is a direct prerequisite for Milestone 6, the append-only cryptographic ledger. Ledger hashing requires that the same query run twice produces the same sequence of financial events to hash. Without deterministic query ordering at the application layer that guarantee is structurally impossible. It is now in place.


Operational Impact

Reproducibility: Identical queries now produce identical row sequences across executions, regardless of PostgreSQL internal state.

Pagination stability: Paginated endpoints return consistent records across pages and requests. Silent record duplication or omission at page boundaries is no longer possible.

Export consistency: Financial exports generated at different times against the same data are now sequence-consistent.

CI enforcement: Zero ongoing maintenance cost. The gate runs on every build and rejects any new unordered query before it can reach production.

Ledger readiness: The query layer is now deterministic end-to-end, satisfying the ordering prerequisite for the M6 cryptographic audit trail.


Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4 — AX-CON-001Sequential Identifier CorrectnessComplete
M4 — AX-TMP-001Temporal DeterminismComplete
M4 — AX-CON-002Idempotency ContractComplete
M4 — AX-QRY-001Ordering DeterminismComplete
M4 — AX-BND-001Schema-Derived Service BoundaryIn Progress
M5Schema-Derived Categorical BoundaryPending
M6Append-Only Cryptographic LedgerPending

Files Changed

Backend:

  • scripts/check-order-by.ts — TypeScript AST ordering gate (permanent CI invariant)
  • api/src/lib/idempotency.ts — LIMIT 1 on idempotency key lookup
  • api/src/lib/bank-import/duplicate-detection.ts — ORDER BY on candidate row fetch
  • api/src/middleware/admin.ts — LIMIT 1 on admin status check
  • api/src/middleware/plan-gate.ts — LIMIT 1 on subscription status check
  • api/src/routes/ — 33 route files updated with LIMIT 1 or ORDER BY
  • api/src/services/ — 5 service files updated with LIMIT 1 or ORDER BY

AX-BND-001 (Schema-Derived Service Boundary) is next — TypeBox runtime validation at service interfaces, closing the final M4 axiom and establishing type correctness enforcement from API boundary through to the database layer.

v5.21.0 7 March 2026 Improvement

Kernel Milestone M4: Idempotency Contract (AX-CON-002)

Kernel Milestone: M4 of 6, third delivery Previous: M4 Phase B: Temporal Determinism Standard (v5.20.0) Next: M4 Phase D: Schema-Derived Runtime Validation (AX-BND-001)

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          duplicate financial mutations now blocked at runtime
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       preserved
Invariant Change
----------------
Financial mutation endpoints are now idempotency-safe: a client-supplied
Idempotency-Key header activates a single-winner claim pattern that detects
and replays duplicate requests without writing to the ledger a second time.

Why This Matters

A distributed system has no reliable way to distinguish a retry from a new request. A network timeout after a successful write leaves the client uncertain: did the invoice get created? Was the payment posted? Without an idempotency contract, the safe answer is to retry, and the safe assumption is that the retry will double-write.

The consequence is concrete. A client retrying invoice creation after a timeout gets two invoices with two invoice numbers and two sets of journal entries. A payment retry posts twice against the same balance. Both scenarios corrupt the ledger silently, in ways that do not trigger the double-entry conservation trigger and may not be noticed until reconciliation.

AX-CON-002 closes this threat class entirely for the three mutation routes where journal entries are written. The idempotency module is the runtime enforcement layer that converts the existing DB-level claim table into a live, typed, lifecycle-managed invariant.


Proof Anchors

Database layer (migration 081):
  UNIQUE (organisation_id, idempotency_key) — single-winner claim enforced at DB level
  FORCE RLS + 5 tenant isolation policies — claim table is tenant-scoped
  status CHECK constraint {processing, completed, failed} — canonical terminal states
  ALL policy WITH CHECK — prevents cross-tenant claim injection
  expires_at DEFAULT now() + interval '24 hours' — DB-enforced expiry

Runtime layer (idempotency module):
  hashRequestBody() — SHA-256 of serialised request body
  claimIdempotencyKey() — INSERT ON CONFLICT DO NOTHING + SELECT claim race resolution
  resolveIdempotencyConflict() — 409 (processing), 422 (hash mismatch), replay (completed/failed)
  completeIdempotencyKey() / failIdempotencyKey() — WHERE status='processing' guard on all writes

Runtime layer (route integration):
  guardIdempotency() — opt-in activation via Idempotency-Key header presence
  executeIdempotent() — owns claim lifecycle; handler returns plain body for storage and send
  Three mutation routes guarded: invoice create, status transition, payment post

Idempotency Contract

Key Design

The idempotency key stored in the claim table is {organisation_id}:{Idempotency-Key header}. The client supplies the retry identity via the Idempotency-Key header; the server scopes it to the tenant automatically. No key can cross a tenant boundary.

The request body hash (SHA-256) is stored separately as a payload validator, not as part of the key. This preserves the correct Stripe-style semantics:

RequestResult
Same header, same bodyReplay stored response
Same header, different body422 Unprocessable Entity
Different header, same bodyIndependent request
Different header, different bodyIndependent request

Opt-In for v1

The idempotency header is optional at launch. Routes check for its presence: if the header is absent, the module is bypassed entirely and behaviour is unchanged for existing clients. This avoids breaking browsers, direct API calls, and early integrations. Mandatory enforcement is a post-launch upgrade path once SDK adoption is established.

Route Scoping

Idempotency protection applies only to routes that write journal entries. Routes that are naturally idempotent (updating a draft, deleting a draft) are deliberately unwrapped, keeping the claim table focused on financial invariants rather than general CRUD traffic.

Guarded routes:

  • POST /api/v1/invoices: invoice creation writes double-entry journal entries on sent status
  • PATCH /api/v1/invoices/:id/status: status transition to sent creates journal entries
  • POST /api/v1/invoices/:id/payment: payment posting writes to the ledger

Deliberately unwrapped:

  • PUT /api/v1/invoices/:id: draft update, no journal entries
  • DELETE /api/v1/invoices/:id: draft deletion, no journal entries

Lifecycle Discipline

Every guarded route closes the idempotency lifecycle in every exit path:

  • On success: completeIdempotencyKey stores the response body and transitions status to completed
  • On handled failure (validation, ID errors): failIdempotencyKey stores the error body and transitions to failed
  • On unhandled error: failIdempotencyKey stores a masked 500 before re-throwing

The WHERE status='processing' guard on both completion functions prevents races from writing terminal state twice. Permanent processing rows, the most common idempotency implementation bug, cannot occur.


Schema Pre-flight

Migration 081 corrected two pre-existing issues in the idempotency_keys table before the runtime module was activated:

  • Conflicting status constraints. Two CHECK constraints existed with overlapping but inconsistent allowed values: one permitted failed as a terminal state, the other did not. The non-canonical constraint was dropped. {processing, completed, failed} is now the sole status definition.
  • RLS ALL policy type mismatch. The tenant isolation policy for SELECT operations used a text cast for organisation_id comparison, while the four specific operation policies used the correct UUID cast. The ALL policy was replaced with a UUID-cast version including a WITH CHECK clause, bringing it into alignment.

Both issues were latent: the table was not in active use before this release. Migration 081 corrected them before any production claims were written.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Duplicate invoice creation from network retryOpenClosed (Class M)Runtime: claim module + DB UNIQUE constraint
Double payment posting from retryOpenClosed (Class M)Runtime: claim module + DB UNIQUE constraint
Duplicate journal entries from status transition retryOpenClosed (Class M)Runtime: claim module + DB UNIQUE constraint
Cross-tenant idempotency key collisionOpenClosed (Class M)DB: FORCE RLS + UNIQUE (organisation_id, key)
Payload substitution via key reuseOpenClosed (Class M)Runtime: SHA-256 hash mismatch detection (422)
Permanent processing rows from incomplete lifecycleOpenClosed (Class M)Runtime: WHERE status=‘processing’ guard on all terminal writes

Kernel Closure Statement

Axiom: AX-CON-002 — Idempotency Contract

Status: CLOSED as of SpeyBooks v5.21.0

Enforcement layers verified:
  Database layer    — UNIQUE (organisation_id, idempotency_key), FORCE RLS,
                      status CHECK constraint, expires_at 24h default
                      [migration 081-idempotency-preflight.sql]
  Runtime layer     — claim/resolve/complete/fail lifecycle module,
                      guardIdempotency and executeIdempotent route helpers,
                      three financial mutation routes guarded

CI verification:
  pnpm build                    — PASS (clean compile)
  constraint-coverage-check.sh  — PASS (74/74 constraints mapped)
  axiom-coverage-check          — PASS (18/18 axioms, registry v1.8)
  verify_081()                  — PASS (7/7 schema checks)

Residual risk:
  Routes outside the invoice domain (dividends, bank import confirm,
  opening balance commit) are not yet guarded. These are post-launch
  coverage extensions. The idempotency module is designed for drop-in
  adoption: guardIdempotency and executeIdempotent require no modification
  to apply to additional routes.

Next dependent axiom:
  AX-BND-001 — Schema-Derived Runtime Validation

Security Posture Change

The idempotency claim table existed since the initial tenant hardening milestone but was unused. This release activates it as a live enforcement surface. All claim table operations are tenant-scoped at the database level via FORCE RLS, so the idempotency system inherits the same isolation guarantees as the rest of the financial kernel. A claim key from one tenant cannot interfere with, observe, or replay against another tenant’s claim space.


Verification Record

Pre-flight (migration 081):
  7/7 schema validation checks clean
  Conflicting status constraint identified and removed
  RLS ALL policy type mismatch identified and corrected

Build:
  pnpm build — clean (0 TypeScript errors)

Post-deploy CI:
  constraint-coverage-check.sh  — 74/74 constraints mapped
  axiom-coverage-check           — 18/18 axioms (registry v1.8)
  verify_081()                   — 7/7 PASS

Adversarial review:
  10 / 10 Production Gold
  Key construction corrected: header value included in stored key
  Optional chain corrected: request.apiKeyId (not request.user.apiKeyId)
  P1 resolved: executeIdempotent owns reply.send, handler returns plain body

Architectural Context

AX-CON-001 (v5.19.0) established that financial document numbers are sequential and collision-free. AX-TMP-001 (v5.20.0) established that all temporal values are deterministic and DB-authoritative. AX-CON-002 now establishes that financial mutations are retry-safe.

Together these three axioms close the provenance domain: every financial event has a unique identifier, a deterministic timestamp, and a single-winner creation guarantee. The ledger cannot contain duplicate entries, ambiguous times, or collision-prone identifiers.

The remaining M4 axioms (AX-BND-001, AX-QRY-001) address the read boundary and ordering determinism, which are prerequisites for the cryptographic ledger in M6.


Operational Impact

  • Duplicate invoice creation from a network retry now returns the original response rather than creating a second invoice.
  • Double payment posting is blocked: a retry returns the recorded payment result, not a second ledger entry.
  • The idempotency module is composable: applying it to additional mutation routes requires adding two lines to a route handler.
  • Zero behaviour change for clients not sending the Idempotency-Key header.

Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4ProvenanceActive (AX-CON-001, AX-TMP-001, AX-CON-002 delivered)
M5Schema-Derived Categorical BoundaryPending
M6Append-Only Cryptographic LedgerPending

Files Changed

Backend:

  • api/db/migrations/081-idempotency-preflight.sql: schema pre-flight, status constraint correction, RLS ALL policy WITH CHECK, UUID cast alignment
  • api/src/lib/idempotency.ts: idempotency runtime module (claim, resolve, complete, fail lifecycle)
  • api/src/routes/invoices.ts: route integration, guardIdempotency and executeIdempotent helpers, three mutation routes guarded. AX-TMP-001 cleanup: normaliseDate applied, local date helper removed.

Registry:

  • docs/kernel/axioms.yml: v1.8, AX-CON-002 delivered, M4 delivers updated, post-launch list corrected

The idempotency contract completes the provenance core of the financial kernel. The remaining M4 work addresses how the kernel reads and orders data before M5 closes the categorical boundary.

v5.20.0 7 March 2026 Improvement

M4 Provenance — Temporal Determinism (AX-TMP-001)

Kernel Milestone: M4 of 6 (second delivery) Previous: M4 — Sequential Identifier Correctness (v5.19.0) Next: AX-CON-002 — Idempotency Contract

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          DB-authoritative timestamps enforced at all write paths
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       strengthened — no application-clock source in financial paths
Invariant Change
----------------
All temporal values at the API egress boundary are now normalised through a
canonical module and typed as strings at compile time, eliminating silent Date
object coercion and application-clock contamination of financial records.

Why This Matters

Date handling is one of the most common sources of silent non-determinism in financial software. When a JavaScript Date object is constructed from a PostgreSQL date column, the result depends on the local timezone of the Node.js process, the database driver version, and the column type. Two environments that are byte-identical at the source can produce different outputs. In an accounting system, that means different ledger representations of the same record, depending on where and when the code runs.

This release closes that gap. A canonical temporal normalisation module now governs all date and timestamp values at the point they cross from the database into the domain layer. PostgreSQL is declared the sole time authority for all financial write paths: no new Date() call may source a value written to a ledger record. Every date column arrives at the API boundary as an ISO 8601 string. Every timestamp column arrives as a UTC ISO 8601 string. The type system enforces this at compile time.

This is the foundation on which deterministic replay and future Merkle-chained audit trails depend. A ledger that cannot guarantee the same timestamp representation under replay is not a ledger.


Proof Anchors

Compiler layer:
  db-types.ts — all date and timestamp fields in raw DB row types typed as
  string; Date objects rejected at compile time before reaching domain logic

Runtime layer:
  canonical normalisation module — three helper functions covering DATE,
  TIMESTAMPTZ, and nullable timestamp column types; sole permitted coercion
  path at the DB-to-domain boundary
  mapper sweep — all route mappers and invoice mapper updated to call canonical
  helpers; bespoke local formatDate/formatDateTime functions removed
  DB-authoritative write paths — now() expressions in SQL for reconciliation
  timestamps, dividend payment/void dates, S455 reference dates, and MWCE
  cutover defaults; application-clock sources eliminated from all financial paths

Database layer:
  constraint coverage CI gate — 75/75 constraints mapped in error codec; three
  document sequence constraints added as kernel invariant faults; gate confirms
  no raw SQL error strings reachable by clients

Temporal Determinism

Canonical Normalisation Boundary

A dedicated normalisation module now provides three functions covering all temporal column types in the schema: one for DATE columns producing YYYY-MM-DD strings, one for TIMESTAMPTZ columns producing ISO UTC strings, and one for nullable timestamp columns. These are the only permitted temporal coercion functions in the codebase. No other date formatting is allowed at the DB-to-domain boundary.

  • Database row types All date and timestamp fields in the raw database row types are typed as string, not Date. This is enforced at compile time: the driver’s native coercion produces a Date object, but the type contract rejects it before it reaches domain logic.

  • Mapper boundary Every mapper function that converts a raw database row to an API response now calls the canonical normaliser for each temporal field. Bespoke formatting functions that previously existed in individual mappers have been removed.

  • DB-authoritative write paths Any financial record that requires the current date or timestamp uses a database-side now() expression rather than the application clock. This applies to reconciliation timestamps, payment dates, tax calculation reference dates, and migration cutover defaults.

  • Error boundary coverage Three database constraints protecting the document sequence table were not yet registered in the typed error boundary codec. These have been added as kernel invariant faults. The CI gate that enforces total constraint coverage detected the gap automatically.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Application-clock contamination of financial timestampsOpenClosed (Class M)Runtime — DB now() enforced at all financial write paths
Silent Date object coercion at DB boundaryOpenClosed (Class M)Compiler — DB row temporal fields typed as string
Bespoke date formatting divergence across mappersOpenClosed (Class M)Runtime — single canonical normalisation module
Unmapped database constraints reaching API error boundaryOpenClosed (Class M)Runtime — constraint coverage CI gate

Kernel Closure Statement

Axiom: AX-TMP-001 — Temporal Determinism Standard

Status: CLOSED as of SpeyBooks v5.20.0

Enforcement layers verified:
  Compiler layer   — DB row temporal fields typed as string in db-types.ts;
                     Date objects rejected before domain layer
  Runtime layer    — canonical normalisation module (three helpers);
                     mapper sweep across all route and invoice mappers;
                     DB now() expressions on all financial write paths
  Database layer   — constraint coverage CI gate at 75/75; no raw SQL
                     error strings reachable by clients

CI verification:
  verify-axiom-coverage.sh      — PASS (18 delivered axioms, 0 violations)
  constraint-coverage-check.sh  — PASS (75/75 constraints mapped)

Residual risk:
  None within the temporal determinism domain.

Next dependent axiom:
  AX-CON-002 — Idempotency Contract

Security Posture Change

No invariants moved from Class O to Class M in the security domain. The threat closure above is within the determinism and error boundary domains. The constraint coverage CI gate (AX-ERR-001) is elevated to fully closed: every database constraint in the schema now has a typed mapping in the error codec, removing the possibility of a raw database error string reaching a client response.


Verification Record

Pre-flight:
  Axiom registry v1.7 — 18 delivered axioms, 0 violations
  AX-TMP-001 proof anchors verified against implementation
  Milestone M4 deliverables narrowed to delivered axioms only

Mapper sweep:
  Routes: all temporal fields normalised via canonical helpers
  Invoice mapper: local formatDate/formatDateTime helpers removed
  OBCE mapper: clean (no temporal coercion)
  MWCE proof: cutover_date normalised via canonical helper

DB-authoritative write paths:
  Reconciliation timestamp: DB now() in SQL
  Dividend payment/void dates: DB now()::date::text via client
  Director loan S455 reference date: DB now()::date::text via client
  Opening balance cutover default: date_trunc expression via client

Constraint coverage:
  75/75 constraints mapped — CI gate green
  3 document sequence constraints added as kernel invariant faults

Adversarial review:
  PASS / Production Gold — axiom registry and mapper files
  Two corrections required and applied:
    Milestone M4 deliverables narrowed to delivered axioms only
    AX-TMP-001 proof anchor layer classification corrected
      (compiler: db-types.ts, runtime: normalisation module + mapper sweep)
  Proof anchor ref widened to include lib/mappers/ after invoice mapper sweep

Architectural Context

M4 Provenance is a three-axiom sequence. AX-CON-001 (Sequential Identifier Correctness, v5.19.0) closed the concurrency gap in document number generation. AX-TMP-001 (this release) closes the temporal determinism gap at the DB-to-API boundary. The remaining M4 work covers AX-CON-002 (Idempotency Contract) and the runtime boundary axioms, both of which depend on the stable temporal representation established here.

M5 (Schema-Derived Categorical Boundary) requires M4 to be complete before its import subsystem proofs can be stated with precision. A schema whose timestamps are non-deterministic cannot make closure claims about import conservation.


Operational Impact

  • Zero application-clock contamination in any financial write path
  • Deterministic timestamp representation across all environments and timezones
  • Compile-time rejection of Date objects at the database row boundary
  • Single normalisation function for each temporal column type — no mapper divergence
  • Full database constraint coverage in the error codec — no raw SQL errors reachable by clients
  • Axiom registry CI gate passes clean: 18 delivered axioms, 0 violations

Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4ProvenanceActive — AX-CON-001, AX-TMP-001 delivered
M5Schema-Derived Categorical BoundaryPending
M6Append-Only Cryptographic LedgerPending

Files Changed

Backend:

  • Canonical temporal normalisation module — three normaliser helpers covering all temporal column types
  • Database row type definitions — all date and timestamp fields typed as string
  • Route mapper sweep — all temporal mapper calls updated to canonical helpers
  • Invoice mapper — local date formatting helpers removed, canonical helpers adopted
  • Financial write paths — application-clock sources replaced with DB-side now() expressions
  • MWCE proof module — cutover date normalised via canonical helper
  • Error boundary codec — three document sequence constraint entries added as kernel invariant faults
  • Axiom registry v1.7 — AX-TMP-001 delivered, M4 milestone deliverables updated, proof anchors corrected

M4 Provenance continues with AX-CON-002 Idempotency Contract, establishing single-winner claim semantics for all financial mutation endpoints.

v5.19.0 7 March 2026 Improvement

Kernel M4 Phase 0 — Sequential Document ID Correctness (AX-CON-001)

Kernel Milestone: M4 of 6 — Active Previous: Milestone M3 — Monetary Domain (v5.18.0) Next: M4 Phase A — Temporal Determinism & Schema-Derived Validation

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          document number issuance is now atomic
Tenant isolation:         unchanged
Financial immutability:   unchanged
State machine:            unchanged
Breaking changes:         none
Replay determinism:       preserved
Invariant Change
----------------
Document number issuance is now enforced atomically at the database layer via
row-level locking. Duplicate identifiers are structurally impossible.
Concurrent issuance may commit out of order, but the numeric sequence
remains strictly monotonic.

Why This Matters

Every invoice and quote carries a document number that is visible to clients, referenced in correspondence, and must be sequential for UK compliance purposes. Before this release, numbers were generated using a pattern that was vulnerable to a race condition: two concurrent requests could read the same current maximum and produce identical numbers. This was not a theoretical risk — any period of concurrent usage could trigger it.

This release closes that threat entirely. Number issuance now happens inside a single atomic database operation that acquires a row-level lock, increments the counter, and returns the issued number — all within the same transaction as the document insert. No two requests can receive the same number. Sequence gaps may occur if a transaction rolls back after issuance, but duplicate identifiers are structurally impossible. The invariant is mechanically enforced, not operationally managed.

This is the first axiom delivered under Milestone M4 (Provenance), which establishes the correctness of all identifier and sequencing behaviour across the kernel.


Sequential Document ID Correctness

The Previous Approach

The previous approach read the current highest number and added one. Under concurrent load — two users creating invoices simultaneously, or a retry race during a slow request — both reads could return the same value, producing two documents with identical numbers.

Identical document numbers are not merely inconvenient. They create ambiguity in client correspondence, break audit trails, and in some interpretations conflict with HMRC’s requirement for sequential VAT invoice numbering.

The Kernel Fix

A dedicated sequence table now holds one row per document type per organisation. Issuance is performed by a single atomic database operation that acquires a row-level lock, increments the counter, and returns the issued value. No two concurrent requests can receive the same number.

The sequence table is seeded at the database layer and all existing organisations received correct starting values as part of this release. No manual intervention is required.

Enforcement layer: database (migration 080) + runtime (sequence generator) Proof anchor: migration 080, sequence generator

Axiom Registry

AX-CON-001 (Sequential Identifier Correctness) is now delivered in the kernel axiom registry at v1.6. The axiom requires dual-layer enforcement — database and runtime — both of which are now satisfied.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Concurrent duplicate document numbersOpen — race condition possibleClosed — row-level lockDB + Runtime
Duplicate identifier issuance under concurrent loadOpenClosedDB
Client-influenced sequence skippingOpenClosedDB

Kernel Axiom Registry — v1.6

This release includes a full audit and correction pass on the kernel axiom registry. Eight structural defects were identified and corrected, bringing the registry to a mathematically closed state:

  • Milestone M4 status advanced from pending to active
  • Class M dual-layer enforcement doctrine applied consistently across all axioms
  • Class O axioms corrected to use the appropriate enforcement schema
  • Milestone deliver lists reconciled with actual axiom status
  • All proof anchors verified as mechanical (no operational claims cited as enforcement evidence)

The registry now satisfies its own governance rules across all six milestone gates.


Changes Delivered

LayerChange
DatabaseDedicated sequence table with row-level locking, seeded for all existing organisations
RuntimeAtomic sequence issuance enforced within transaction boundaries
Invoice serviceLegacy sequence bootstrap removed; atomic issuance integrated
Invoice routesRoute-layer database initialisation removed (layer violation corrected)
Quote routesBoth sequence call sites migrated to atomic issuance
Quote API docsDocument number format corrected throughout
Axiom registryAX-CON-001 delivered; eight structural corrections applied (v1.6)
CIOrdering determinism gate authored for AX-QRY-001 (deployment pending)
v5.18.0 7 March 2026 Improvement

Kernel Closure — Dual-Layer Verification Scaffold

Why This Matters

The SpeyBooks kernel doctrine requires every Class M invariant to be enforced by at least two independent layers — a database layer and a verification layer — so that no single failure point can silently compromise financial correctness. M1 through M3 delivered the database enforcement side: constraints, triggers, RLS policies, and domain types. What was absent was the verification side: executable proof scripts that independently confirm each invariant holds, both structurally and against live data.

This release delivers that missing layer. Four verification scripts and a coverage gate script now mechanically prove every delivered axiom — rounding correctness, double-entry conservation, RLS mutation containment, monetary domain integrity, and typed error semantics — and the coverage gate enforces that no future axiom can be marked delivered without satisfying both layers. The gate ran for the first time at the end of this session and reported zero violations across all 17 delivered axioms.


Verification Scripts Delivered

Monetary constraints (AX-RND-001..005, AX-MON-002)

Twelve checks confirming that the canonical rounding functions are present with exact signatures, that all monetary CHECK constraints are installed and validated, that every constraint references the canonical arithmetic function, and that zero live rows violate any monetary invariant. Covers gross conservation, VAT derivation, net pipeline correctness, VAT sign polarity, and NULL prohibition on monetary columns.

Enforced by: verification script — Class M Tests layer. Proof: scripts/verify-monetary-constraints.sql — 12/12 checks passed.

Double-entry conservation (AX-ALG-001)

Six checks confirming that the balance enforcement function exists with exact signature, that the constraint trigger is present, deferred, and invokes the correct function, that the monetary column uses the canonical pence domain, and that zero posted transactions violate the conservation invariant. Includes phantom transaction detection and NULL amount guard.

Enforced by: verification script — Class M Tests layer. Proof: scripts/verify-double-entry.sql — 6/6 checks passed.

RLS mutation containment (AX-TEN-002)

Five checks confirming that all UPDATE RLS policies carry both USING and WITH CHECK clauses, and that WITH CHECK equals USING exactly — not merely that both sides reference the tenant column, but that the write predicate is identical to the read predicate. This closes the cross-tenant reassignment vector at the proof layer.

Enforced by: verification script — Class M Tests layer. Proof: scripts/verify-rls-policy-symmetry.sql — 5/5 checks passed, 25 UPDATE policies verified.

Typed error codec (AX-ERR-001)

Enumerates all 72 CHECK constraints in the accounting schema and confirms each has a typed entry in the constraint error map. Fails the build if any constraint is unmapped. This converts the error codec from a static code artefact to a mechanically verified coverage guarantee.

Enforced by: coverage gate — Class M Tests layer. Proof: scripts/constraint-coverage-check.sh — 72/72 constraints mapped.


Deterministic Constraint Error Codec

The previous error handler resolved database violations by substring-matching raw PostgreSQL error messages — a brittle pattern vulnerable to message text changes and incapable of distinguishing constraint semantics. This has been replaced by a deterministic pipeline:

PostgreSQL constraint violation
→ error.constraint (driver metadata)
→ CONSTRAINT_ERROR_MAP lookup
→ typed ApiError

All 72 accounting schema CHECK constraints are classified into two tiers. Ten kernel invariant constraints — covering ledger algebra, rounding pipeline correctness, and conservation laws — map to 500 responses with structured internal telemetry. Sixty-two business rule constraints map to typed 400 responses with field annotations for precise client feedback. Raw PostgreSQL error strings no longer reach the API boundary under any path.

Enforced by: runtime error codec + CI coverage gate — Class M. Proof: constraint-coverage-check.sh — 72/72 constraints.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Delivered axioms lacking independent proof layerOpen — database enforcement onlyClosed (Class M)Verification scripts, coverage gate
Raw PostgreSQL error strings reachable at API boundaryOpen — substring matching onlyClosed (Class M)Deterministic constraint map, 72 constraints
New constraints introducible without error mappingOpen — no CI gateClosed (Class M)constraint-coverage-check.sh CI gate
WITH CHECK ≠ USING on UPDATE RLS policiesOpen — structural onlyClosed (Class M)Exact predicate equality proof
Double-entry trigger pointing to wrong functionOpen — name check onlyClosed (Class M)tgfoid function identity verification

Security Posture Change

The error codec moves from Class O (substring matching on raw database messages — leaks structural information in development, provides no typed semantics in production) to Class M (deterministic constraint name lookup — no database internals reach the API boundary, kernel invariant faults produce structured internal telemetry). The CI gate ensures this property cannot silently regress as new constraints are added.


Verification Record

Axiom coverage gate:
  17/17 delivered axioms checked
  0 violations
  All Class M axioms: ≥ 2 independent enforcement layers confirmed

verify-monetary-constraints.sql:   12/12 PASS
verify-double-entry.sql:            6/6  PASS
verify-rls-policy-symmetry.sql:     5/5  PASS  (25 UPDATE policies)
constraint-coverage-check.sh:      72/72 PASS

Adversarial review:
  ALG-VERIFY-001/002/003/004 — double-entry script revised, all findings resolved
  TEN-VERIFY-001 — RLS symmetry script revised, equality proof substituted for token check
  AX-ERR-001 codec — approved, two-tier classification confirmed correct

Architectural Context

M1 through M3 established database-layer enforcement across all delivered invariants. This release closes the second-layer gap, satisfying the kernel doctrine’s dual-layer requirement for all 17 delivered axioms. The axiom registry has been updated to reflect delivered Tests layers for the closed axioms.

M4 (Provenance) can now build on a kernel that is not only enforced at the database layer but independently verified — every invariant has a proof script that can be run after any restore, migration, or schema change to confirm the mathematical properties of the ledger remain intact.


Operational Impact

Verification: Running scripts/verify-axiom-coverage.sh now confirms dual-layer closure across all delivered axioms. Zero violations.

Error semantics: Every mapped accounting CHECK constraint violation now produces a typed API error; user-recoverable faults include field annotations where relevant, while kernel invariant faults return safe opaque 500s with structured internal telemetry.

CI gate: Any future migration that adds a CHECK constraint without a corresponding error codec entry will fail the build.

Post-restore: All four verification scripts can be run after any database restore to confirm ledger integrity before traffic is resumed.


Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4ProvenancePending
M5Schema-Derived Categorical BoundaryPending
M6Append-Only Cryptographic LedgerPending

Files Changed

Backend:

  • api/src/lib/api-error.ts — CONSTRAINT_ERROR_MAP added; deterministic constraint → ApiError pipeline replacing substring matching; structured telemetry for kernel invariant faults

Scripts:

  • scripts/verify-monetary-constraints.sql — 12-check monetary domain verification (AX-RND-001..005, AX-MON-002)
  • scripts/verify-double-entry.sql — 6-check double-entry conservation verification (AX-ALG-001)
  • scripts/verify-rls-policy-symmetry.sql — 5-check RLS UPDATE policy symmetry verification (AX-TEN-002)
  • scripts/constraint-coverage-check.sh — CI gate confirming 100% constraint → error codec coverage (AX-ERR-001)

Registry:

  • docs/kernel/axioms.yml — Tests layer delivered for AX-RND-001..005, AX-MON-002, AX-ALG-001, AX-TEN-002, AX-ERR-001

M4 (Provenance) begins with a kernel that is mathematically closed: every delivered invariant has independent proof.

v5.17.1 6 March 2026 Fix

Milestone 3 Completion — Trigger Hardening and Domain Migration

Kernel Milestone: 3 of 6 — Completion patch Previous: Milestone 3 — Monetary Domain Migration (v5.17.0) Next: Milestone 4 — Provenance

System Impact
-------------
Ledger integrity:         strengthened
Ledger mutation:          none — no posted records modified financially
Tenant isolation:         unchanged
Financial immutability:   strengthened — immutability triggers now function correctly
State machine:            unchanged
Breaking changes:         none
Replay determinism:       preserved
Invariant Change
----------------
Financial immutability triggers are now unconditionally executable regardless
of session search_path. Transaction VAT components and director/dividend
amounts are now stored under the canonical monetary domain.

Why This Matters

v5.17.0 established the monetary domain and corrected all application-layer arithmetic paths. During the final domain promotion migrations for transaction VAT components, two trigger paths sharing the same schema-qualification defect were discovered in the financial immutability enforcement layer: both referenced tables without schema qualification. PostgreSQL trigger functions execute without inheriting the session search path, which caused these guards to fail at runtime with a relation resolution error rather than the intended invariant exception. Both defects are corrected here.

Without these fixes, any application path that issued an update or delete against transaction lines associated with locked journals would fail with an opaque database error rather than the intended TMADD 5.0 rejection. The immutability enforcement was present but non-functional on these two paths.

This release also completes the final two domain promotions deferred from v5.17.0: transaction VAT components and director loan / dividend amounts are now stored under the canonical pence domain, eliminating the last remaining primitive-typed monetary columns in the financial kernel.


Immutability Trigger Fixes

Schema qualification defect — update path

A trigger guarding updates to transaction lines belonging to locked opening balance journals referenced the parent transactions table without schema qualification. Because trigger functions do not inherit the application session search path, PostgreSQL could not resolve the unqualified reference and the trigger aborted with a relation resolution error instead of enforcing the TMADD 5.0 Axiom 5 rejection.

Corrected to use the fully qualified reference. The guard now executes deterministically regardless of session configuration.

Enforced by: database trigger — Class M. Proof: migration_077.

Schema qualification defect — delete path

An identical defect existed in the corresponding delete-path trigger. Same root cause, same correction.

Enforced by: database trigger — Class M. Proof: migration_077b.


Domain Promotions

Transaction VAT components

The VAT amount column on transaction lines was a nullable primitive integer. Three-hundred and thirty existing rows carried a NULL value representing zero VAT — a tri-state representation (NULL / zero / non-zero) that is incompatible with the canonical monetary domain, which admits only integer values.

All NULL values were normalised to zero — a semantically neutral transformation, as NULL VAT and zero VAT are financially identical. The column was then promoted to the canonical monetary domain and made non-nullable, collapsing the tri-state to a binary (zero / non-zero). Triggers were temporarily disabled within the migration transaction to permit this schema normalisation and re-enabled immediately after the update. This is the only legitimate trigger bypass in a migration — it does not weaken the immutability guarantee for any application-layer write path.

Enforced by: domain type constraint — Class M. Proof: migration_078.

Director loan and dividend amounts

Director loan transaction amounts and dividend amounts were stored as 32-bit integers, creating an overflow risk for values above approximately £21,000 — a realistic threshold for director salary and dividend workflows. Both columns have been widened to the canonical 64-bit monetary domain, eliminating the overflow risk with no data rewrite required.

Enforced by: domain type constraint — Class M. Proof: migration_079.


Threat Closure

ThreatStatus BeforeStatus AfterEnforcement Layer
Immutability trigger failing on locked-journal update pathOpen — runtime error instead of invariant rejectionClosed (Class M)Database trigger, schema-qualified
Immutability trigger failing on locked-journal delete pathOpen — runtime error instead of invariant rejectionClosed (Class M)Database trigger, schema-qualified
Tri-state NULL / zero / non-zero VAT representationOpen — domain inconsistencyClosed (Class M)Domain type constraint, NOT NULL
32-bit overflow on director loan and dividend amountsOpen — silent truncation above ~£21kClosed (Class M)Domain type widening to 64-bit

Kernel Status

MilestoneDescriptionStatus
M1Tenant IsolationComplete
M2Financial Immutability and State MachinesComplete
M3Monetary Domain MigrationComplete
M4ProvenancePending
M5Schema-Derived Categorical BoundaryPending
M6Append-Only Cryptographic LedgerPending

M4 (Provenance) begins with a fully canonical monetary schema and a correctly functioning immutability enforcement layer.

How we version

SpeyBooks follows Semantic Versioning — a system designed for predictability.

X Major — Breaking changes (rare)
Y Minor — New features
Z Patch — Bug fixes

See something broken? Check our live bug tracker.

© 2026 SpeyTech