Skip to content

chore(release): Cut the v2.0.0-beta.2 Ailanthus beta - #314

Merged
nfebe merged 20 commits into
mainfrom
dev
Aug 13, 2026
Merged

chore(release): Cut the v2.0.0-beta.2 Ailanthus beta#314
nfebe merged 20 commits into
mainfrom
dev

Conversation

@nfebe

@nfebe nfebe commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Second Ailanthus beta. Carries the exports, the assistant's reach across budgets, refunds, reminders and groups, and the schema, scoping and precision fixes since beta.1, plus the delivery change that moved the hosted image build out of here.

nfebe added 20 commits July 18, 2026 22:37
A transaction could carry any number of categories, which made "what did I
spend this on" ambiguous and let the same amount count under several headings
at once. Recording or editing one now takes a single category.

Existing transactions keep the categories they already have until edited, at
which point the first is kept and the rest fall away.
Asking the assistant to categorize a range of transactions only ever changed
one of them. Every write it could make acted on a single record, and it was
told to make one and then explain itself, so a request covering forty rows
produced one proposal and a summary implying more had happened. It can now
propose the whole set in one go, giving every transaction the same category or
a different one each, and the person confirms the sweep once rather than
clicking through a card per row. A single record still proposes on its own.

Every amount it reported came back in dollars no matter what currency the
person keeps their money in. It was told to honour their default currency
while nothing could tell it what that was, and a figure read from their
records carries no currency of its own, so it fell back on what it had been
trained to assume. It is now told whose money it is looking at before it
answers, can ask for their settings, and converts at the same rates the rest
of the app uses instead of doing the arithmetic itself. Where the currency
cannot be known it now asks rather than guessing.

Confirmation cards also named records by their internal id, which means
nothing to the person approving them. They now name the transaction being
changed and the category it will get.
The readme opened by calling Trakli an income tracker built in Laravel, which
undersells it twice: it tracks a good deal more than income, and naming the
framework tells a reader nothing about whether it is for them. What followed
was a flat list where the things nobody else does sat between the things
everybody does.

It now opens with the person it is for, whose money is spread across cash,
mobile money and more than one currency, and whom every other app quietly
cannot serve. The list leads with what is ours and keeps the table stakes
below it.
A deleted record kept the sync timestamp from before the deletion, so
clients that compare timestamps treated the tombstone as unchanged and
never applied the deletion.
The received amount of a transfer was silently truncated to a whole
number, so cents vanished on every transfer and cross currency
conversions with small rates produced zero amount transactions.

Amounts now keep four decimal places end to end, transfers whose
received amount would round to zero are rejected, and responses no
longer round sub cent amounts.
…fers

Rows with a zero or unparseable amount imported as zero amount
transactions instead of failing with a clear reason. Transfer rows
never matched their marker after case normalization, so every transfer
in a CSV was reported as an invalid type; they import as transfers
again.
The guard meant to keep the first client generated id checked an
attribute that does not exist, so it always passed and a second device
could silently replace the mapping created by the first.
A holding created with automatic pricing reported a zero unit price
until the next scheduled reprice run. With 1.2.0 it gets its first
price at creation, and repricing is scoped to the caller.
The lead framed bank sync as the villain and pitched only the
multi-currency, cash-heavy case, underselling the app to anyone with a
simple setup. The pitch now follows trakli.app: control and net worth
first, the assistant doing the logging, an AI native section, and the
power detail in its own list. Holdings joined the feature list since
the app tracks assets, not just income and expenses.
Two tables the assistant could read were not tied to the user asking. Holdings
were described but left off the readable list, so the entity was dead weight;
adding it alone would have exposed every user's assets, since ownership there
is polymorphic and needs the owner type pinned as well as the id. The join
table linking transactions to categories was already readable with no scoping
at all, which returned other users' transaction and category pairings to
whoever asked.

Both are now scoped: holdings by their owner, and the join table through the
transaction it belongs to.

Separately, a wallet-to-wallet transfer writes a paired income and expense row
for money that never left the user, and both were counted as real income and
spending. Every figure the assistant reported was inflated by the value of the
user's transfers, twice over. Those rows are now excluded from income and
expense, and can be asked for on their own.

Tests cover the invariants rather than the specific tables, so a future table
that is readable without a filter, or scoped by an owner id without its type,
fails the build.
Deciding which fields a user may edit before confirming an agent action, and
re-checking the result, is one policy split across two halves that have to
agree: a field left out of the allowlist is also a field the checks never see.
Both halves lived in the chat controller among unrelated request handling,
which is neither where a reader looks for them nor a place that stays
readable as action types are added.

Behaviour is unchanged; the rules move together into one class that states why
ownership fields are deliberately not editable.
Nine of the models a user works with were invisible to the assistant. It could
not list a transfer, say how a budget was doing, or record that money coming
back was a refund rather than fresh income, which also meant budgets counted
refunded spending as spent.

Each model now describes itself once, and that description drives its read
tool, the fields a create tool accepts, and its place in the semantic layer.
Models with a hand-written read tool are described too, so the query layer and
the tools stay in step without a second tool competing for the same name.

Creating a budget, a refund and a repeating transaction each need more than
setting columns, so those keep purpose-built tools; groups and reminders are
served by a generic one built from their own declaration. All of them propose
rather than write, and the user confirms.

The semantic layer is now generated from those declarations instead of
maintained alongside them, and a test fails the build if the two drift apart.
The agent-facing model declarations this branch adds need the resource support
released in 1.1.0. The existing constraint already allowed it, so only the lock
moves.
Clients sending a form or multipart body cannot express a real boolean,
so flags such as a transaction's recurrence arrived as text and were
rejected outright. Creating a recurring transaction from the mobile app
failed for this reason.

Boolean flags now accept the usual textual spellings on any endpoint,
while values carrying no clear meaning are still rejected rather than
quietly read as false. A recurrence interval sent the same way no longer
breaks scheduling.
The gating seam is now the shared entitlements package instead of a
private copy of the same interface. Behaviour is unchanged: every
feature is still allowed and nothing is metered, so a self-host stays
free, and a deployment that wants to charge rebinds one interface.

Keeping our own copy meant a plugin had to pick between two identical
contracts, and only one of them is what the packaged Cashier adapter
and gating middleware already speak. Adopting the shared one is what
lets the cloud plugin drop those in rather than rebuild them.

The package's own endpoints stay off here, since a snapshot reporting
no plan would contradict a gate that allows everything. Its tables are
still created so a plan-aware deployment is a drop-in.
Creating a transaction on staging has been failing because a column added
by a migration five weeks ago never reached that database. The
conformance check that exists to catch exactly this covered only two
features, so it stayed silent while the application accepted writes it
could not complete.

Every column added by a later migration is now declared, so a database
that falls behind answers with a 503 naming what is missing instead of
failing at insert time, and a deploy that leaves the schema behind now
fails visibly rather than continuing.

Three reminder columns had also been living entirely in the conformance
spec with no migration behind them, so a fresh database only acquired
them as a side effect of the conformance step. They now have a migration.

A test covers the rule the drift broke: every column added to an existing
table has to be declared, so the check cannot go stale again.
The mobile app offers PDF and Excel export from the transaction history,
but neither option did anything and nothing on the server could produce
those files.

Transactions and a period statement can now be downloaded as CSV, XLSX or
PDF. The transaction download reads the same filters as the listing, so a
file matches the rows the user was looking at, and the statement carries
the same figures as the analytics screen including the caveat shown when
an exchange rate was unavailable.

A selection too large to render is refused with a message asking for a
narrower range rather than timing out part way through the download.
A single row limit across all three formats was wrong. Rendering a PDF
holds the whole document in memory and costs roughly 0.4MB a row, so it
exhausts a 128MB worker somewhere between two and three hundred rows,
while a spreadsheet manages several thousand and a CSV streams. The
shared limit was set for the cheapest of them, so a large PDF request
would have killed the worker instead of returning an error.

Each format now declares what it can hold and the request is refused
above that, naming the limit that applied and what every other format
allows so the caller can offer a working alternative.
The hosted image was assembled by a workflow living here but called from the
repository that owns the plugins. A reusable workflow checks out its caller, so
it checked out a repository with no application source and would have failed on
its first run. The build now lives where it is called from, and only the
Dockerfile it needs stays here.

The gating contract is documented as what it is, a seam a plugin can rebind,
without describing what anyone charges for.
Prepares the second Ailanthus beta: bumps the version so the release checks run
against a fresh tag, and records the changelog covering exports, the assistant's
reach across budgets, refunds, reminders and groups, and the schema, scoping and
precision fixes since the first beta.
@sourceant

sourceant Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Review Summary

This release candidate (beta.2) significantly matures the Ailanthus series. It introduces a comprehensive multi-format export system, extends the AI assistant's reach to encompass all core financial models, and addresses critical precision issues in wallet-to-wallet transfers.

🚀 Key Improvements

  • Modular export architecture in app/Exports supporting PDF, XLSX, and CSV.
  • AI support for Budgets, Refunds, and Recurring Rules via new specialized tools.
  • Improved precision for cross-currency transfers using BCMath with scale 4 in TransferController.
  • Enhanced mobile sync support by ensuring soft-deletes update the last_synced_at timestamp.

💡 Minor Suggestions

  • Standardize currency rounding to 4 decimal places across all AI tools.
  • Eager load sync states in GetUserDefaultsTool to reduce database queries.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. No specific code suggestions were generated. See the overview comment for a summary.

@github-actions

Copy link
Copy Markdown

Coverage Report
PR coverage: 72.46%
Baseline: 72.46%
Change: ✅+0%

@nfebe
nfebe merged commit 2a2a9b4 into main Aug 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant