10 KiB
Tech debt — what it costs to not pay this yet
Last verified: 2026-08-02 against commit
cd8144e. Populated by phase 5 of the documentation clean-up chain.
Debt is different from backlog.md: a backlog item blocks a specific flow. A debt item doesn't block anything today — it raises the cost of everything that comes after it. The phase brief named six candidates to assess (items 2, 3, 4, 5, 7, 8 below); two of those (3, 4) turned out less severe than the framing assumed once checked against the code, and this phase adds two more (1, 6) found while doing that checking. Each entry below states what it costs now, what it costs at meaningfully more usage, and the trigger that should cause it to get paid.
1. No contract-generation pipeline — client DTOs hand-drift from the server
What it is: docs/integration/openapi/swagger.v1.json is a regenerated, dated snapshot, but nothing turns
it into client types. client/package.json has no openapi-typescript/NSwag/orval step — every service
domain's TypeScript types are hand-written and hand-kept in sync with the real server DTOs.
| Now | At meaningfully more usage |
|---|---|
| The cost has already been paid twice, expensively: phase 2 found 24 phantom client-side endpoints nobody had checked against swagger; phase 3 found 34 client seam operations that would 404 on a mock→real flip and several DTO shape mismatches severe enough to break, not just show stale data (verification, refunds, payouts, patient-records, partner-center — see implemented.md). Both discoveries took a dedicated full-session audit to surface. | Every future domain repeats this discovery cost, and nothing stops a new drift from being introduced tomorrow — there is no CI check comparing client types to the live contract. This compounds with domain count, not with traffic: the more domains behind mocks, the more of these audits eventually needed. |
Trigger to pay it: don't wait for the next silent break — the next 2-3 units in next-up.md are exactly "flip a mocked domain to real." Wiring a generated-types step (even scoped to just the domains being de-mocked next, not a big-bang rewrite) pays for itself on the very next flip. Rough size: M.
2. Single-instance in-process scheduler
What it is: weekly payout-batch generation, and (once cron'd) the credential-expiry scan and EVV no-show sweep, all run as in-process jobs with no distributed lock. Correct today because exactly one API instance runs. Same root cause as BL-220 (Redis not deployed).
| Now | At meaningfully more usage |
|---|---|
| Zero cost — one instance, no race. | A rolling deploy that briefly runs two instances already risks a duplicate weekly payout batch — real money, generated twice. Horizontal scaling for any other reason (traffic, availability) can't happen without this being solved first; it's a hidden prerequisite baked into "just add another container." |
Trigger to pay it: the moment a second instance is even considered, for any reason — not just load. Rough size: M (a distributed lock via Redis, per BL-220's own scoping).
3. Testing-convention asymmetry between the two projects
Reassessed — this is not a raw-count gap. Server test files (119, spanning Baya.Test.Api WebApplication-
Factory integration tests and Baya.Test.Foundation handler/unit tests) and client test files (125, Jest) are
close in count, and money-path server coverage is genuinely strong: Payments, Bookings, BNPL, Payouts, and
Refunds each have dedicated API-integration and handler-level test files. The real asymmetry is in
enforcement:
- docs/rules/server/cqrs.md makes testing a mandatory step for every new
feature: "Add handler unit tests and at least one
WebApplicationFactoryintegration test for the area: happy path 200, unauthenticated 401, validation 400." - Root CLAUDE.md's own client gate is reactive, not mandatory: "
npm run test:ciif you touched a tested component." New client code has no enforced testing bar at all.
| Now | At meaningfully more usage |
|---|---|
| 125 client tests exist, but — consistent with the flow atlas's own mock-vs-real accounting — coverage almost certainly concentrates on the domains that were real early and thins out on the ones still mocked. | As the mocked domains in next-up.md flip to real, the newly-real client code lands with no enforced test the way a newly-shipped server handler always does. Regressions there have no automated net; the flow atlas's manual, one-session verification (§4 below) is the only thing that has ever caught them. |
Trigger to pay it: natural to pair with each domain de-mock in next-up.md rather than run as its own initiative — write the rule once, backfill tests as each domain is touched anyway. Rough size: S to write the convention, M per domain to backfill.
4. No automated test crosses the client↔server boundary
Reassessed — "absence of E2E tests over the money paths" overstates it. The money paths have real
automated coverage at the handler and API-integration layers (§3). What's genuinely missing is anything that
drives a real browser against a real running API — no Playwright, no Cypress, nothing in
client/package.json's scripts beyond Jest. That gap is exactly why phase 3's flow atlas had to boot the
whole stack and manually click/curl through all 23 flows to get a trustworthy status — a deliberate,
one-session, human/agent-driven substitute for automated E2E.
| Now | At meaningfully more usage |
|---|---|
| Absorbed as a one-time cost per audit — expensive (a full session), but infrequent. | This cost doesn't scale with traffic, it scales with how often you need to trust a full-stack claim — every future audit like phase 3's repeats the same manual walkthrough from scratch, because nothing keeps the previous one's findings mechanically re-checkable. |
Trigger to pay it: before the next full-stack audit is needed, or the first time a money-path regression reaches production undetected — whichever comes first. Rough size: M — the server side is already well tested, so this only needs to close the browser↔API gap: a handful of Playwright specs over the critical path (login → search → book → pay → cancel), not a rewrite of anything existing.
5. Search without Elasticsearch
SqlNurseSearch over nurse_search_index is real, correct, and — per
decisions.md — the deliberate MVP implementation, same item as
BL-221.
| Now | At meaningfully more usage |
|---|---|
| Fine at MVP data volume; no measured strain. | A wide denormalized SQL index degrades on filter/sort/paging combinations well before a purpose-built search engine would. |
Trigger to pay it: measured latency/throughput strain on nurse_search_index — not a date.
Rough size: L.
6. The ESLint unused-vars gate is a no-op
BL-250: the config patches an export path
that doesn't carry the rule, so @typescript-eslint/no-unused-vars never actually runs — despite
client/CLAUDE.md's own golden rule 11 claiming it does.
| Now | At meaningfully more usage |
|---|---|
| A safety net that looks present but isn't — dead code accumulates invisibly, with no signal to anyone that it's happening. | The longer this stays broken, the more has silently piled up by the time someone finally looks — this is a debt that compounds purely with time and commit count, independent of traffic or scale. |
Trigger to pay it: a dedicated infra task (this item's own stated trigger). Rough size: S — it's a config-path bug, not a redesign.
7. Two remaining raw-state admin forms
BL-217: GrantRoleDialog and PreviewBatchDialog are the only two
survivors of an otherwise-complete app-wide react-hook-form migration.
| Now | At meaningfully more usage |
|---|---|
| Trivial — two isolated components. | Still trivial. This is the one item on this list that genuinely doesn't get worse with scale; it's listed only because it's a rough edge for anyone reading the code expecting one form pattern everywhere. |
Trigger to pay it: whenever either dialog is next touched for an unrelated reason — not worth a dedicated pass. Rough size: S.
8. Windows-generated client lockfile
Documented directly in DEPLOY.md:
client/package-lock.json, generated on Windows, omits wasm32-only optional packages Linux's npm wants,
so a bare npm ci fails in the container build. Absorbed today by a npm install --package-lock-only step
baked into the client Dockerfile before npm ci.
| Now | At meaningfully more usage |
|---|---|
| Zero operational cost — the workaround runs on every image build without incident. | Still zero cost at scale; this is environment debt, not scale debt. The real risk is a future edit: anyone who "cleans up" the Dockerfile without knowing why that line is there reintroduces a build failure that only reproduces on Linux/CI, not on the Windows machine that likely made the edit. |
Trigger to pay it: DEPLOY.md already gives the exact fix — regenerate the lockfile on Linux once and commit it, then delete the workaround line. Rough size: S. Cheapest item on this list to close permanently.
What didn't make this list
archive/clarify-chain/README.md's own diagnosis (260+ markdown files, ~10 rule sources, contract drift) is the
documentation debt this entire phase chain exists to retire — phases 0-4 already paid most of it down, and
phase 6/7 finish the job. It isn't repeated here because it isn't code debt, and because tracking it twice
would just be two ledgers again, the exact problem this chain was created to end.