9 Commits
Author SHA1 Message Date
MichilisandClaude Opus 5 37370dd079 phase-8: run it on Postgres, and find out what that was hiding
CI / types, lint and rules coverage (push) Canceled after 0s
CI / tests (postgres) (push) Canceled after 0s
CI / tests (sqlite) (push) Canceled after 0s
CI / playwright (push) Canceled after 0s
Six phases claimed the product runs on SQLite and on Postgres. Nothing
had ever run it on Postgres. TEST_DATABASE_URL now points the whole
suite at a real server and CI runs both arms.

The first run found a bug that would have shipped. better-auth's banned
flag is integer 0/1 on SQLite and a real boolean on Postgres, and the
code read it as `banned === 1`, so on Postgres an account someone asked
us to freeze went on receiving email. Both of those flags are now typed
for either dialect and read through isFlagSet.

It also found the migration advisory lock being taken on a pool. An
advisory lock belongs to the session that took it, so a lock on one
pooled connection and an unlock on another leaves it held. Two replicas
migrating at once is the ordinary case in k8s and is precisely what it
was there to protect.

New for the scaled mode: k8s manifests with migrations as an
initContainer, one poller in its own worker Deployment rather than one
per API replica, and an Ingress that exposes the web app only. The
storage drivers finally have tests, S3 included, since scaled mode
requires it and it had never been exercised.

Two acceptance tests, both checked against a deliberately broken build
first: two workers claiming a hundred jobs report 188 claims with SKIP
LOCKED removed, and the in-flight request is cut off with the drain wait
removed.

340 tests on SQLite, 341 on Postgres, 70 Playwright, rules coverage 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 22:03:13 +00:00
MichilisandClaude Opus 5 e4eb1617d1 phase-7: motion, an installable app, and a capture that survives no signal
GSAP carries the counter roll-ups, the bandeja card physics, the dialog
transitions and the three success moments FLOWS.md allows. Every one of
them checks prefers-reduced-motion first and does nothing when it is set.

boneyard and canvas-ui are not what SPEC.md's stack table says they are:
on npm the names belong to two abandoned projects that do neither job.
The skeletons were already ours; the two canvas spots are now sixty lines
each with no dependency. DECISIONS.md records the substitution.

The app installs, keeps a scan taken with no network in IndexedDB and
sends it when there is one, falls back to a page that explains itself,
and can push a deadline notice. Reading the log of what is queued is the
source of truth, so the notice clears when the capture actually lands.

The CSP now allows scripts by per-request nonce rather than by
'unsafe-inline'. That forced /offline to render per request: a
prerendered page carries a build-time nonce no live policy matches, so
its scripts were blocked and it never hydrated.

Two crashes fixed on the way. web-push throws on a VAPID subject that is
not https: or mailto:, and the code handed it APP_PUBLIC_URL, so any
machine with push keys died at boot; a misconfigured optional channel now
switches itself off and says why. And a subscription the push service
answers 410 for is deleted rather than retried forever.

Lighthouse on the production build: accessibility 100, best practices 96,
SEO 100, performance 73. The performance number is not trustworthy on
this machine and DECISIONS.md says why; total blocking time did fall from
17.6s to 1.7s once the hero canvas stopped drawing at full resolution
every frame and the landing page stopped importing GSAP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 20:48:48 +00:00
MichilisandClaude Opus 5 4c39926483 phase-6: the staff console, and a log that says who did what
Flow H, three screens behind a role check: find an account, work the
ingestion error queue, read and export the audit log. Superadmins can
change a role, never their own.

The error queue merges ingest errors and dead jobs into one table with a
cursor that pages both sources; only a job can be retried and only an
ingest row resolved, with a note that migration 003 gives it somewhere
to live.

writeAudit no longer defaults a missing subject to the actor, which had
been recording a user search as staff looking themselves up. Omitting
the subject still means acting on yourself; null now means the action
has no subject, which is what a search, a retry and an export are.

Reading the log is not audited. Exporting it is: a copy leaving the
building is a different act from looking.

e2e/global-setup.ts asks for every screen once before the suite starts,
so a dev server's first-request compile is paid before the first test
rather than by it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:55:59 +00:00
MichilisandClaude Opus 5 6620650e9e phase-5: declarations, from generated numbers to a PDF you file yourself
Formulario 120 and 515 assembly, the review screen, approval, the PDF, and the
guided checklist that walks the user through presenting it in Marangatu.

Approval is a promise about what the user actually saw: the numbers are
recomputed on the way in, and if the documents moved since the declaration was
generated the answer is a 409 with the fresh figures stored as a draft, not a
silent approval of numbers nobody read. An approved declaration is never
regenerated or invalidated.

Editing, confirming, rejecting or reclassifying a document flips any ready
declaration covering its period back to draft, which is what the dashboard
reads to stop offering it for review.

The PDF is rendered once and cached against the declaration, so two downloads
are byte for byte the same document; every write that changes the numbers
clears the cache. Approval pre-renders it, and the route renders on demand, so
a failed job costs a wait rather than a missing file. It stays Spanish in both
locales, like the form it mirrors, and carries a BORRADOR watermark until it is
approved.

The seeded declarations are computed through the same code path the product
uses, so their figures agree with the documents behind them.

Three defects the screenshots caught: the floating scan button swallowed the
tap meant for Aprobar on a phone, the summary breakdown reused labels meant for
other screens, and five tab labels collided at 390px.

Tests that can avoid depending on a fresh seed now do; the ones that cannot say
so in the assertion rather than timing out. db:reset warns that the API has to
be stopped first, having learned that the hard way.

293 vitest tests, 59 Playwright tests, rules coverage still 100%, typecheck and
lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 05:50:29 +00:00
MichilisandClaude Opus 5 5dc284c5f2 phase-4: the live tax position, deadlines and the notifications behind them
The dashboard, the deadline engine, the three sweeps and the notification
channels. Confirm a comprobante and the IVA and IRP figures move; every
headline number opens the documents behind it.

The dashboard does no arithmetic of its own: it picks inputs and calls
packages/rules. The acceptance test asserts the API's numbers equal what
computeF120 and computeF515 produce over the same rows, before and after the
bandeja is confirmed, so a drift in either direction fails.

Deadlines come from the calendario perpetuo, skipping any period earlier than
the date the taxpayer took the obligation on. Without that guard a brand new
account opens on a red overdue card for a period that predates it.

Sweeps are idempotent by dedupe key rather than bookkeeping: a restarted
poller, a second replica and a crash mid-sweep all converge on one run, and one
reminder per user per period per milestone. They queue notifications rather
than sending them, so a channel being down retries on the job schedule. The
T-10 test derives the date from dueDateFor rather than restating the calendar.

Auto-confirm only touches what the rules were confident about and never a
decision the user already made. An unconfigured channel is absent rather than
broken: the fan-out skips it, the UI hides it, and a frozen account receives
nothing.

Web: the dashboard's three zones with swipeable position cards and traceable
numbers, the deadlines timeline, and the app shell with the tab bar and the
persistent scan button FLOWS.md asks for.

Two additions to the specs, both marked: hasDocuments on DashboardDto, without
which a position of all zeros is indistinguishable from a real one and the
first-run state never shows; and an insight_dismissals table, which FLOWS.md
requires and SPEC.md has nowhere to put.

Also: pnpm db:reset, because the e2e suite changes the seed it runs against and
a suite that is not repeatable is not a suite.

280 vitest tests, 51 Playwright tests, rules coverage still 100%, typecheck and
lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:42:32 +00:00
MichilisandClaude Opus 5 b074456b70 phase-3: ingestion, from a QR in the camera to a card in the bandeja
The whole pipeline: storage behind one driver interface (local disk and S3),
a portable job queue with a poller, QR and CDC parsing, OCR through the
Anthropic API, dedupe, manual entry, and the bandeja that turns all of it into
one decision per card.

Scanning tries the trustworthy door first: a QR is parsed and prefilled from
its CDC; a photo without one is queued for OCR when a key is configured and
otherwise opens the manual form against the stored file. A QR that will not
parse records an ingest error and falls through rather than losing the photo.

Job claiming is the only dialect divergence, as SPEC allows: FOR UPDATE SKIP
LOCKED on Postgres, a conditional UPDATE against SQLite's single writer. Retry
backoff follows SPEC exactly and a job abandoned by a killed process returns to
the queue once its lock goes stale, which is the phase 3 acceptance case.

OCR uses structured outputs rather than parsing prose, so the model cannot
return anything but the RULES.md schema, and every field is nullable because
unreadable is a real answer.

Web: scan with live QR decoding (BarcodeDetector, ZXing fallback, wasm served
from our own origin), manual entry with the IVA split worked out from the
total, the bandeja with swipe, buttons and keyboard all doing the same thing,
and the documents list and detail with an editable classification.

The seed now carries Maria's 34 purchases and 8 sales and Carlos's 6, all
classified through the real rules, plus the two open ingest errors.

Two defects found and fixed with tests: seeded documents could be dated in the
future, which would corrupt any projection computed from them, and the category
buttons announced their keyboard shortcut as part of their name.

255 vitest tests, 37 Playwright tests, rules coverage still 100%, typecheck and
lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 02:20:31 +00:00
MichilisandClaude Opus 5 0d7651b17c phase-2: identity, from the landing hook to the profile screen
Flows A1 to A6 and E3 end to end. A visitor types a RUC on the landing page,
sees their real filing dates, registers, verifies a six digit code, grants
consent, completes a three step setup and lands on the first run screen, with
the profile, consent and audit rows to show for it.

API: public RUC lookup behind a token bucket (10/min/IP), the full /me surface
(profile, dependents, consents, notification prefs, data export, account
deletion), an append-only audit module that exports an insert and nothing else,
and a PII module that is the only thing allowed near those tables.

Deletion and consent revocation both freeze the account and drop every session,
reusing better-auth's ban flag rather than adding a second notion of disabled.
Nothing is destroyed yet: the purge is a job for phase 4. deadlineDigit is
always derived server side, never accepted from the client.

Web: landing with the RUC hook, registration, OTP verification, consent, the
setup wizard, the profile screen with "Tus datos", and legal pages that ship as
marked placeholders per COPY.md section 13. Money, Skeleton, Switch and
EmptyState components added.

The seed is now complete for identity: Maria at 4123456-1, filing digit 6 and
day 19, with a dependant, consents and prefs; Carlos as an IVA-only company.

Two real defects found by building the screens and fixed with tests:
the OTP boxes dropped a digit because the handler fired effects inside a
setState updater that React 19 invokes twice, and the switch knob rendered
outside its track because translate-x-5.5 does not resolve.

232 vitest tests, 26 Playwright tests across mobile and desktop, coverage still
100% on the rules, typecheck and lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 23:33:10 +00:00
MichilisandClaude Opus 5 80b10c958e phase-1: packages/rules complete, 100% covered
Every algorithm in docs/RULES.md, implemented exactly as written: the RUC
check digit, the calendario perpetuo with weekend and holiday roll forward,
CDC and KUDE QR parsing, keyword classification, computeF120, computeF515 and
the dashboard projections, plus the two form definitions.

Both worked examples reproduce verbatim on the first implementation: F120 at
Gs. 277.273 to pay with the Gs. 350.000 flip case, F515 at Gs. 11.290.000 on a
5,65% effective rate. 162 tests over the package, coverage enforced at 100%
statements, branches, functions and lines; the only exclusions are three
bounded-loop guards marked v8 ignore with a comment saying why.

No tax rule was invented. All six TODO-TAX-VERIFY items from RULES.md have a
test pinning today's behaviour and a row in the DECISIONS.md register, so
verification later is a red/green diff. Where RULES.md was silent the choice is
marked SPEC-GAP in the code and listed too, the notable one being that
taxpayer.hasIrp gates the deduction amount.

No floats anywhere in a money path: money is a branded Pyg of whole guaranies
and percentages go through integer arithmetic rounded half up.

Also: contracts now takes IRP_CATEGORIES from rules rather than declaring the
eight strings twice; classification returns reason codes with catalog strings
in both locales, so the detail sheet localizes; apps/api/.env.example now names
the same web port as apps/web/.env.example, without which a fresh checkout
fails sign in on the origin check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:15:28 +00:00
MichilisandClaude Opus 5 ae2ea20b7e phase-0: foundation, both apps boot end to end
Monorepo (pnpm workspaces) with two deployable apps and three pure packages.

apps/api (Hono on Node): Zod validated env that fails fast and names the problem,
Kysely factories for SQLite and Postgres chosen by DATABASE_URL scheme, portable
migrations covering the whole SPEC section 5 schema, Better Auth with the four
roles and seeded demo accounts, localized error envelope, /healthz and /readyz,
graceful SIGTERM drain. Dialect specific SQL is confined to the two factories.

apps/web (Next.js App Router): locale routed shell in es and en with a language
switcher, sign in screen, and a runtime /api proxy so the browser only ever sees
one origin and cookies stay first party.

packages/i18n ships both catalogs complete; es is generated from COPY.md and a
test re-derives it from the document on every run so it cannot drift.
packages/contracts holds the Zod schemas and the typed client the web app uses.

Verified: 43 vitest tests, 14 Playwright tests on mobile and desktop, typecheck
and lint clean, migrate and seed from a clean database, sign in through the proxy
with CSRF rejection of foreign origins.

Not verified here: docker compose. This user has no access to the docker socket.

RULES.md is absent from docs/, so packages/rules exports only RULES_VERSION and
no tax rule, check digit or deadline was invented. See DECISIONS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:46:35 +00:00