143 lines
10 KiB
Markdown
143 lines
10 KiB
Markdown
# 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](../../archive/clarify-chain/README.md).
|
|
|
|
Debt is different from [backlog.md](../status/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](../status/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](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](deferred.md#scale-later-infrastructure) (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](../rules/server/cqrs.md) makes testing a mandatory step for every new
|
|
feature: "Add handler unit tests **and** at least one `WebApplicationFactory` integration test for the
|
|
area: happy path 200, unauthenticated 401, validation 400."
|
|
- Root [CLAUDE.md](../../CLAUDE.md)'s own client gate is reactive, not mandatory: "`npm run test:ci`
|
|
**if 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](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](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](../status/decisions.md) — the deliberate MVP implementation, same item as
|
|
[BL-221](deferred.md#scale-later-infrastructure).
|
|
|
|
| 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](deferred.md#cleanup-of-convenience-and-data-integrity-hygiene): 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](../status/backlog.md#minor-115): `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](../../DEPLOY.md#known-wrinkle-the-client-lockfile-is-windows-generated):
|
|
`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`](../../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.
|