Conversation
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.
Code Review SummaryThis 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
💡 Minor Suggestions
|
|
Coverage Report |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.