Review 25921, point 5 (pre-existing since #472): after a reattachment,
reattachOrphan reloaded through loadCategories, whose loading state
unmounted the detail panel, so a category form open behind the window
lost its unsaved changes. The silent re-read of checkHierarchy becomes
readCategoriesAgain (waits for a drag-and-drop's writes, a newer load
wins, no loading state), used by both; a failed re-read after a
reattachment now shows in the window, where the user is.
Also pins the guard « a newer load wins » (review 25921, R8): a load
started during the check's read lands, the check's older answer is
dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision of Max of 2026-10-20 (review 25921): transactions, non-zero
budget amounts and active keywords are checked only while the target
still takes input (is_inputable = 1, the condition of markAsParent),
that is when it really becomes a parent and stops taking input. An
existing parent, no longer inputable, accepts a new or reattached
subcategory whatever it holds, as in every released version: the
refusal protected nothing there. All three codes and all three entry
points follow the rule; the hierarchy rows now carry is_inputable,
and createCategory reads them through its transaction's handle.
Also pins a negative amount, in the grid and in a template, as a
refusal (review 25921, R1/R2): the grid stores what is typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review of the reload: updateCategorySortOrders writes one UPDATE
per category, each through the lock on its own, so a check opened right
after a drag-and-drop could read halfway and show half the move in the
tree. moveCategory keeps its write's promise, settled either way, and
checkHierarchy waits for it before reading. Pinned by a hook test with
the write held open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REVIEW.md, outside « Niveaux »: a pitfall on the vitest tests that read
src-tauri/ (a PR touching only src-tauri/** skips check-frontend, review
25592 point 6), and, in the Release zone, the --locked relay to re-prove
when @tauri-apps/cli or cargo-xwin is bumped (review 25416 point 2).
architecture.md: the same relay, the six cargo metadata calls coming
from the Tauri CLI only (25416 point 3); the mount error of Transactions
wiped by the first debounced page read, and the banner cleared by
refreshFolder and browseFolder (25592 points 7 and 2); the catch-up
measurement asserts success, not duration, and PriceFetchControl among
the harness users (25443 points 1 and 5); the Ctrl+click note's reason
as read in tauri 2.10.2 and wry 0.54.1 (25487 point 5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 5: the commands/ line said 3 modules and 17
commands; generate_handler! registers 45, in 10 modules (fs 7,
export_import 5, profile 8, license 11, auth 5, token_store 1,
entitlements 1, feedback 2, backup 3, balance 2), listed as in
docs/architecture.md. Review 25487, point 7: balance/ (12),
categories-migration/ (5) and onboarding/ (3) added, of the 138
components (non-test .tsx under src/components).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25424, point 3: the English interface never says "Journal" (the
card is "Logs"), the French one says "Journaux". Unreleased entry only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25487, point 2 (R13): [A, z, Vide] gives z, Vide, A; dropping
the subtotal of an empty block at the final flush turned no test red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25443, point 2: the transfer icon's tooltip goes through t with
its account and direction (X4), LicenseKeyForm's cancel button through
t (X11), and the notes textarea opens under the row whose button was
clicked, read in the Fragment keyed by that row's id (X8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25424, point 1: the `if (!cancelled)` of ChangelogContent's
.catch had no test (V1). Modelled on the late-answer test: the French
file rejects after the English one was read, and the history still shows
the English notes, not the empty message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25409, points 1 and 2: the canary now fails if both imports name
one file (T4), and a fixture pins that a missing branch is reported by
its leaves, never by its container (T1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 1: the pre-check reads exists(), not is_dir(); under
is_dir() a file named "Exemple Tangerine" let "Exemple Desjardins" be
created before create_dir failed with a raw "File exists". The
reviewer's test pins it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 3: the page's tests take the marked t and the key
guard compares no value, so a translation that lost {{name}} passed the
suite. The real locale files, through a real i18next with the app's
options, now render folderExists with the folder's name in French and in
English, and the two other codes without a leftover placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 2 (pre-existing): the banner of a refused sample
folder asks to refresh the list, yet survived a "Refresh" that went
through, and a folder change. refreshFolder, and browseFolder once a
folder is chosen, now clear the error before reading the folder, as
selectSource does; a failed rescan puts up its own error, and a
cancelled picker keeps the banner. The hook tests also pin browseFolder's
order (review 25487, point 1): the folder is set before its scan, so the
root files shown are the new folder's. Fixed entry in both changelogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25431: a window centered on screen grows when its content
arrives, and moves its fields with no scroll nor resize; the open list,
placed in a portal, stayed where it was. While open, the combobox now
reads its position again after every render (useLayoutEffect without
dependencies); `keepPlacement` hands back the same state object for an
unchanged position, so React renders nothing more. Pinned by
CategoryCombobox.placement.test.ts (harness, useLayoutEffect mapped on
its useEffect, fake input) and keepPlacement's own tests. The boundary
test of placeListbox now sits at exactly 240 px below with room above
(point 1), and AddKeywordDialog's comment no longer claims the list is
cut (point 3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision of Max of 2026-10-19 (review 25487, point 4):
docs.dashboard.steps[0] (FR and EN) and step 1 of the guide's Dashboard
section listed six of the eight steps. They now name the eight, by the
labels of onboarding.checklist.items, in the list's order, and say the
two visit steps are checked with the card's "Got it" or, for the
reports, a click on the outlined Trends card. A test holds the step to
the checklist's labels and order; the #446 changelog entries follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25437, point 2: the loop named by its own smallest member, not by
a smaller id under it (RM1); the page hands the window every category,
not the tree (RM11); a category that is its own parent stays out of the
budget, and displaysAsRoot says so (RM15). RM7 (depth counted on active
categories at the service) is pinned in categoryService.test.ts by
1a36d4e, RM2 by be4b923.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a drag-and-drop, SET_TREE leaves state.categories with what the
tree shows, without the members of a loop: the window then showed "—"
for the orphan and offered a parent the service refused (review 25437,
point 1). checkHierarchy now reads the categories again before the
diagnostic, without the page's loading state, so a form open behind
the window stays as it is; a load started meanwhile wins. Pinned by a
drag-and-drop-then-check hook test, plus the refusal-then-success case
of the window's message (RM2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unreleased #393 entries now say the refusal covers budget amounts
and keywords and applies when a subcategory is created; guide tip and
architecture.md (validation, createCategory, out-of-scope drag-and-drop,
the node:sqlite test note) follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision of Max of 2026-10-19 (review 25437, point 3): a category that
holds direct transactions, a non-zero budget amount (budget_entries or
budget_template_entries) or active keywords cannot become a parent. The
check (`checkParentContent`) runs through the transaction's handle,
before any write, in `updateCategory` (changed parent), `reattachCategory`
and now `createCategory`, which writes its row and the parent's flag in
one transaction. Order: transactions, budget, keywords; the first found
is the refusal (`parent_has_budget`, `parent_has_keywords` added to
`CategoryHierarchyError`, translated in FR and EN).
The service tests run the content queries as written on node:sqlite,
tables created from consolidated_schema.sql.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review tidy-up: the presence test looked the dots up twice; it now
keeps the one list it asserts on. No assertion changed.
Resolves#446
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
architecture.md: 138 components, counted as the .tsx files of
src/components outside tests (find src/components -name '*.tsx'
! -name '*.test.tsx' | wc -l), was 61; per domain, balance 12 with its
actual names (BalanceOnboardingCard is gone), categories 8, reports 35,
and the missing categories-migration/ line (5). The /reports hub has 5
navigation cards, named. The useImportWizard row says SET_IMPORT_FOLDER
empties rootFiles and keeps the scanned sources until the next scan.
"La mise en évidence" notes that a Ctrl+click on the Trends card fires
onClick, so it checks the "Browse the reports" step while the router
does not navigate; judged theoretical without tabs in the webview.
CLAUDE.md: 138 components (was 53), import 18 (was 14).
CHANGELOG (EN, FR): Changed for the help texts, Fixed for the root
files warning after choosing another import folder.
Resolves#446
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reports.help.tips[2] and [3] named "Monthly Trends" and "Category Over
Time" ("Tendances mensuelles", "Catégories dans le temps"), tabs that
no longer exist: they now name the Trends and Category Analysis cards
of the hub. docs.dashboard.features[6] said the eight steps tick
themselves from the data; the two visit steps are checked with "Got it".
French texts as validated by Max on 2026-10-16, English aligned; the
guide's line of the dashboard features follows docs.dashboard.features.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reorderRows: a collapsed sub-group (a subtotal with no child row)
followed by a sibling leaf gives Vide, z, A, the order read by running
the case first; then the same block followed by a sibling root, a group
(Vide, z, A, b, B) or a leaf (Vide, z, A, R).
OnboardingBanner: the decorative h-1 w-1 dot is absent while the visit
steps are not initialized and while the Budget entitlement is unknown,
and present, between the counter and the next step, once the total is
known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SET_IMPORT_FOLDER now empties rootFiles. Until the new folder's scan
landed, or for good when that scan failed, the warning about files
placed directly in the import folder named the previous folder's files
under the new folder's name. scannedSources is left as it was: the
source list of the last scan stays until the new one, as before.
Reducer tests next to "a scan error leaves the root files of the last
scan in place": a folder change empties the root files and keeps the
sources until the next scan, and a scan error after a folder change
names no root file of the previous folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review findings, no behaviour change. architecture.md: the mount reads
paragraph no longer plays on "compte" (count and account). fs_commands.rs:
the doc comment of create_sample_import_folders said a write error carries
"never a path", while it names the sample folder and its file; it now says
it never carries the import folder's path.
Resolves#384
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review finding: create_sample_import_folders refuses on any existing
entry named like a sample folder, a file included, which "Refresh the list"
never shows. The message now says to refresh the list and, if the entry
does not show up, to rename it before trying again (FR and EN), and the
changelog entries follow.
Resolves#384
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
architecture.md: the Tauri commands recounted from generate_handler! (45,
was 39), with the five that were never named documented in new sections
of their modules (feedback_commands.rs: send_feedback,
get_feedback_user_agent; backup_commands.rs: ensure_backup_dir,
get_file_size, file_exists) and fetch_price described as it is, not as
"to come in Phase 5" (also in the balance service and the tree's command
modules, now 10). Hooks: 29 files, the three undocumented ones added to the
table. The Transactions empty state reads the count alone (D1), the mount
reads stand alone, and the sample folder flow describes its codes, its
errorValues and its Journal line.
CLAUDE.md: 29 hooks in the tree. Guide and docs.gettingStarted.steps:
"the keywords that come with the app" instead of the seed jargon.
CHANGELOG (EN, FR): Changed for the Transactions card, Fixed for the sample
folder messages and banners.
Resolves#384
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With folderName as its only prop, ImportEmptyState tells "no folder" from
"a folder" by null alone. The page hands "" for an import folder stored as
"", as hasFolder did, so the empty state keeps the "no source" variant the
step card describes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seed is not all priority 0: COSTCO and WALMART are at 50, COUCHE-TARD
ESSENCE at 10. The comment now says why the mock's sort stays right: it
sorts by descending priority like the SQL, and no line of either sample is
matched by more than one seed keyword, so the order cannot move the counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
D1 of Max: showsEmptyState of Transactions reads totalInDb === 0 only, like
Budget. On an empty profile the card comes up as soon as the count is
loaded, instead of the filter bar, the totals and the loading line until
the first page is back. A null count (not loaded, or failed) keeps the
regular layout.
The mount reads of useTransactions and useBudget run under
Promise.allSettled: each read that succeeds is dispatched, the first
failure becomes the page's error, and a failed count leaves totalInDb null
instead of emptying the category, source and account filters.
Tests: the reducer test of the loading page rewritten for D1, the page's
empty-profile case rendered (card in place of the filter bar, table and
pagination), and both hooks' mount reads run through the shared harness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_sample_import_folders refuses with stable codes instead of English
sentences: sample_folder_exists:<name>, import_folder_missing (no longer
carrying the folder's path) and invalid_date. The wizard maps them to
import.empty.sampleErrors.* through the pure sampleFolderError, the folder
name interpolated from the new errorValues of the state; any other message
stays raw. The raw refusal also goes to the Journal as a warning: a code, or
a write error naming a sample folder and the OS error, never a path.
scanFolderInternal now tells whether the scan went through, and the success
banner only follows a rescan that did: a failed rescan leaves its error
alone on screen.
ImportEmptyState keeps folderName alone (null without a folder); the page
passes "" for a folder stored as "", as hasFolder did.
Rust tests: the three codes asserted exactly, the case of the second sample
folder already there (nothing written), and {{M1}} pinned as the most
recent month line by line against the templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review of PR #473: the header still said every React rendering of the
file was bypassed, while its "displayed texts" tests render the control with
its hooks from the shared harness. architecture.md: the slot presets are
commented in each test file (the two direct writes of the onboarding wiring
test are covered by the comment of mountWith).
Resolves#437
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Where a test presets a harness slot by position (PriceFetchConsentToggle,
useOnboardingChecklist's mountWith, SystemsSettingsPage), a comment names
the dependency on the component's hook order: any hook added before shifts
the slots without a word (the `slot` helper of fakeReact).
- docs/architecture.md, Tests: "Limites connues" lists the slot presets, and
links to ADR 0021 for withoutFolders and for the catch-up measurement
instead of repeating them.
- ADR 0021 section 7: the relative-path limit is stated precisely (a run with
two separators or more is still cut to its last segment; only the words of
a folder holding a blank stay), and a second limit is written: a folder
name holding one of the stops (a backtick, `;`, `|`, a closing parenthesis
not followed by a separator...) ends the path there, and that name, or its
start, stays in the line.
- db.ts: the comment of withoutFolders says "a closing parenthesis not
followed by a separator", as the code does, and points to ADR 0021.
No behaviour change.
Resolves#437
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- db.test.ts: `\r` joins the path stops of withoutFolders (removing it from
PATH_STOP now fails).
- The six keys whose "t removed" mutation survived the review of PR #435
(comment 23499) are asserted in their marked form:
transactions.transferIcon.ariaLabel (a row linked to a transfer) and
transactions.notes.placeholder (title of every row's notes button) in
TransactionTable.render.test.tsx; license.keyPlaceholder and
license.activate in LicenseKeyForm.render.test.tsx;
dashboard.kpiSectionTitle in DashboardPage.render.test.tsx;
balance.priceFetching.consent.accept in PriceFetchControl.test.tsx, the
consent dialog opened by presetting the control's first harness slot
(the dialog's title, body and decline texts are pinned with it).
- TransactionTable.wiring.test.ts opens a row's notes through its button
and pins the textarea placeholder, the second call of the notes key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deactivated child of the reattached category is not counted in the
number of categories put back, which the Journal line reports.
Resolves#393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The architecture section said the module had three consumers; the walks
do, and the budget reads displaysAsRoot as well.
Resolves#393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reattachment is committed before the orphan list is checked again: if
that check failed, the tree kept showing the categories as they were. The
tree now reloads in a finally block, and the failure shows in the window.
Resolves#393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG (Added: the "Check hierarchy" window; Fixed: the parent checks
at save time and the budget rows of a category whose parent is deleted or
gone), the Categories section of the user guide and its docs.* keys in
both languages, and docs/architecture.md: the categoryHierarchy module, the
diagnostic, the validation and its typed error, the window, the budget
root rule, and what stays out of scope (drag-and-drop, archive restore).
Resolves#393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget grid and the Budget vs Actual report read active categories
only and started from the categories without a parent, so a category whose
parent was deactivated or no longer exists vanished from both, budget
amounts included. It now stands at the top level, as in the tree and the
category pickers (displaysAsRoot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateCategory checks a new parent before writing: the category itself,
one of its descendants or a category caught in a cycle (cycle), a fourth
level counting its sub-tree (too_deep), a parent holding transactions of
its own (parent_has_transactions). The refusal is a CategoryHierarchyError
that the Categories page shows by the translation of its code. The reads
and the writes, the new parent's is_inputable included, run in one
transaction through its handle; an unchanged parent is not checked, so a
category stored before these rules can still be renamed.
getCategoryDepth keeps the ids it has seen and ends on a cycle.
A "Check hierarchy" button, between "All keywords" and "Re-initialize",
opens a window that lists the orphan categories (caught in a parent loop,
parent deleted, parent gone), one per group, with a new parent for each:
the root by default, or a parent of the category form's list.
reattachCategory applies the same rules; after it, the list and the tree
reload and the Journal gets one line with the reason and the number of
categories put back, no name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/utils/categoryHierarchy.ts holds the depth, height and descent walks
that buildParentOptions had inline, each with a set of the categories it
has seen, so a parent_id cycle ends the walk. A category caught in a cycle,
or under one, now has no depth instead of a made-up one, and the parent
list of the category form never offers it: a child placed there would be
hidden with it. The module also carries the orphan diagnostic, the parent
change refusal and the root rule that the service, the Categories page and
the budget use next.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review: the Portails paragraph listed StarterAccountsModal next to
LinkTransfersModal as if both had a test of this kind; only
LinkTransfersModal has one. The other windows are named as windows the
same rule applies to.
Resolves#429
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review: the CHANGELOG entries said the list opens upward when the
room below is short; it does only when it also fits above (in a short
window it stays below). The Portails paragraph of architecture.md now
quotes the two errors a test meets without the mock, both reproduced
on a throwaway test: "Target container is not a DOM element" without
a DOM, "Portals are not currently supported by the server renderer"
with a DOM-like container.
Resolves#429
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG (EN and FR, Changed): typing in the category of an adjustment
line and in the category filter of the "Link transfers" window, the
adjustment lines limited to the categories open to input, and the open
list no longer cut by the edge of a window or of the Transactions table.
Guide, section 7, and its in-app twin (docs.adjustments.steps, FR and
EN): type a few letters to filter the category list of a line.
architecture.md, Tests section: how a test renders a window drawn by
createPortal (identity mock, document stub), what of the combobox's
portal is pinned without a DOM (placeListbox.ts) and what is checked in
the app, and that the list no longer sits in its container's DOM.
Resolves#429
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The category filter of the "Link transfers" window was a native select.
It now takes the compact CategoryCombobox, modelled on the Transactions
filter bar: "Any category" first as an extra option, active while the
filter is empty, and picking it sets the filter back to null; the list
of transactions reloads as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each line of the adjustment form chose its category in a native select
showing raw names. It now takes the compact CategoryCombobox of the
split window: typing filters, hierarchical order, seed names
translated. A new line still takes the first category of the list.
D2: the hook reads the Transactions list, getAllCategories (active and
open to input), instead of its own query on every active category. The
list feeds two pickers only, the form and the split window of the
Adjustments page; the detail view names a line's category from the
entries' join, so a line saved on a category hidden from input keeps
its name there and its value, its field empty until changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open list of CategoryCombobox was positioned `absolute` inside its
input's container, so a scrolling ancestor clipped it: the body of the
split window, the `overflow-x-auto` wrapper of the Transactions table.
It is now rendered in a portal on document.body, `position: fixed`, at
z-[300] (above every window, the highest at z-[210]).
The pure rule lives in placeListbox.ts: under the input, as wide as it,
4 px below; above it when the room below is short of the list's 240 px
and the room above has them; below when neither side has them. The
list is placed in a layout effect before the first paint, then follows
the input on every scroll (capture phase, any scrolling container) and
resize while it is open, skipping scrolls of the list itself; the
listeners are removed with the list (followAnchor returns the cleanup).
The outside press counts the list as inside, since it no longer sits in
the container's DOM. The closed markup is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review of PR #470: the System card of the Settings home lists the
page's sections as chips, still with the version history before the logs.
The chips now follow the page (version, update, logs, history), and a test
renders the page and compares its section headings with the card, so the
two cannot drift apart again. SECTIONS is exported for that test, as
DocsContent exports its own. The /settings/systems row of
docs/architecture.md names both unreleased headings and the card.
Resolves#372
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>