Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

# `nuxi typecheck` prepares Nuxt types itself, then runs vue-tsc.
# Fails the job on any type error (non-zero exit).
- name: Typecheck
run: npx nuxi typecheck
202 changes: 202 additions & 0 deletions .kiro/specs/fix-typecheck-errors/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Design — Fix pre-existing typecheck errors

> Traceability: implements **GitHub Issue #10** ("chore: fix pre-existing
> typecheck errors"). Relates to #4.

## Problem

`npx nuxi typecheck` is **not clean** on `main`. Re-baselined on
`chore/fix-typecheck-errors` (branched from `main` at `e590105`, i.e. *after*
PR #12 merged) it reports **78 errors** — identical count and breakdown to the
issue, because the test files PR #12 added already follow the `!`-on-index
convention and contribute zero.

Current breakdown (re-measured, not copied from the issue):

| Count | Code(s) | File |
|------:|---------|------|
| 38 | TS2532 / TS18048 | `utils/toRegisterRows.test.ts` |
| 13 | TS2532 | `utils/toTransactionInput.test.ts` |
| 12 | TS2532 | `utils/toTransactionInput.property.test.ts` |
| 7 | TS18048 | `utils/roundTrip.property.test.ts` |
| 6 | TS18048 | `utils/toRegisterRows.property.test.ts` |
| 1 | TS2532 | `server/api/__tests__/migration.test.ts` |
| 1 | **TS2322** | `pages/accounts/[...path].vue` |

By error code: **40× TS2532**, **37× TS18048** (both `noUncheckedIndexedAccess`),
**1× TS2322** (a real type mismatch).

CI does not currently gate on typecheck, so these have accumulated silently.

## Two distinct problems

This is **not** one homogeneous fix. There are two root causes:

### A. 77 test-file errors — `noUncheckedIndexedAccess` ergonomics (benign)

With `noUncheckedIndexedAccess`, any array index or destructure yields
`T | undefined`. The tests index results they *know* are populated (they
construct the input) but never tell the compiler. Three shapes appear:

1. **Array index** — `rows[0].runningBalance` → `rows[0]` is `Row | undefined`.
2. **Destructure** — `const [row] = toRegisterRows(...)` → `row` is
`Row | undefined`.
3. **Mock-call tuple** — `mockAppendTransaction.mock.calls[0][0]` → both
`[0]` indexes are possibly-undefined (`migration.test.ts:113`).

These are genuinely safe accesses in test context, so the fix is to assert
non-null at the indexing site. This is **the same pattern the codebase already
adopted** for newly written tests (per the issue), so we are converging on an
existing convention, not inventing one.

### B. 1 page-component error — a **real latent runtime bug** (TS2322)

`pages/accounts/[...path].vue:106` — `@delete="deleteTx"`:

```
Type '(row: { transactionIndex: number; }) => Promise<void>'
is not assignable to type '(index: number) => any'.
```

`AccountRegister` declares `defineEmits<{ edit: [index: number]; delete:
[index: number] }>()` and emits **a plain number**
(`emit('delete', row.original.transactionIndex)`). But the page handler is
typed/written to receive an **object**:

```ts
async function deleteTx(row: { transactionIndex: number }) {
deleting.value = row.transactionIndex // number.transactionIndex → undefined
await $fetch('/api/transactions', {
method: 'DELETE',
query: { index: row.transactionIndex }, // index: undefined ← bug
})
}
```

At runtime the handler receives a number, so `row.transactionIndex` is
`undefined` and the delete request sends `index: undefined`. **The typecheck
error is surfacing a genuine defect**, exactly as the issue anticipated
("may be a real type gap, not just test ergonomics"). This one is fixed by
correcting the handler, not by silencing the index.

## Proposed solution

### A. Test files — non-null assertion at the indexing site

Prefer `!` exactly where the issue recommends ("`!` on indexed access where the
index is provably in range"):

```ts
// array index
expect(rows[0]!.runningBalance).toBe(-5)

// destructure
const [row] = toRegisterRows(txs, 'assets:checking')
expect(row!.inflow).toBeNull()

// mock-call tuple
const txInput = mockAppendTransaction.mock.calls[0]![0]
```

Rationale for `!` over the alternatives:

- **`!` (chosen)** — minimal diff, matches the convention already in newer
tests, keeps each assertion on one line, reads as "this is provably present."
- **Guard (`if (!row) throw`)** — heavier; only worth it where a destructure is
reused across many lines. We will use a single hoisted `const row = rows[i]!`
where a row is dereferenced repeatedly in one test, to avoid `!`-noise on
every line (judgment call per block, favoring readability).
- **Rewriting helpers to return non-optional types** — out of scope and wrong:
the helpers are correctly typed; the gap is purely at test call sites.

No production `utils/` source changes — only the `.test.ts` / `.property.test.ts`
call sites.

### B. Page component — fix the handler signature and body

Align `deleteTx` with the emitted `number`, fixing the latent bug:

```ts
async function deleteTx(transactionIndex: number) {
if (!confirm('Delete this transaction? This cannot be undone.')) return
deleting.value = transactionIndex
try {
await $fetch('/api/transactions', {
method: 'DELETE',
query: { index: transactionIndex },
})
...
```

While here, tighten `editTx(_row: any)` → `editTx(_index: number)` to match the
`edit: [index: number]` emit and remove the `any` (coding-standards: no `any`).
This is in-scope cleanup of the same emit contract, not scope creep.

This change is behavior-affecting (it repairs delete). **Regression guard
(decided 2026-06-14): the typecheck gate itself.** The suite has no
component-mount harness and `deleteTx` is not exported, so a runtime test would
require new devDeps (`@nuxt/test-utils` + happy-dom) and brittle Nuxt UI
stubbing — disproportionate for a one-line client fix. Instead, the now
CI-gated `nuxi typecheck` (R4) structurally forbids the regression: reverting
`deleteTx` to a non-`number` param reintroduces the exact `TS2322` and fails CI.
See R3.4.

### CI gate (final step)

Once typecheck is clean, the issue asks to "make `npx nuxi typecheck` a required
gate (CI) so this can't regress." There is currently **no CI workflow in the
repo at all** (tests aren't gated either).

**Decision (confirmed): add a typecheck-only CI workflow here.** Create
`.github/workflows/ci.yml` that, on push/PR to `main`, runs `npm ci` then
`npx nuxi typecheck`. Deliberately **no test job** in this ticket: the test
suite has hledger-on-PATH-gated tests whose CI wiring overlaps with #11, so a
test job is left to a follow-up. A typecheck-only job is self-contained (no
hledger needed) and directly fulfills the issue's "can't regress" requirement.

Notes for the workflow:
- This will be the repo's first GitHub Actions file — keep it minimal and
conventional (checkout → setup-node with npm cache → `npm ci` → typecheck).
- `nuxi typecheck` needs the Nuxt types prepared; it runs `nuxi prepare`
implicitly, but the workflow should not assume a committed `.nuxt/`.
- Pin `actions/checkout` and `actions/setup-node` to major version tags.

## Files touched

| File | Change | Errors fixed |
|------|--------|-------------:|
| `utils/toRegisterRows.test.ts` | `!` on indexed/destructured rows | 38 |
| `utils/toTransactionInput.test.ts` | `!` on indexed access | 13 |
| `utils/toTransactionInput.property.test.ts` | `!` on indexed access | 12 |
| `utils/roundTrip.property.test.ts` | `!` on destructured rows | 7 |
| `utils/toRegisterRows.property.test.ts` | `!` on indexed/destructured rows | 6 |
| `server/api/__tests__/migration.test.ts` | `!` on mock-call tuple | 1 |
| `pages/accounts/[...path].vue` | fix `deleteTx`/`editTx` signatures (bug fix) | 1 |
| `.github/workflows/ci.yml` *(new)* | typecheck-only CI gate (npm ci → nuxi typecheck) | — |

No config files touched (`tsconfig*`, `nuxt.config`, `vitest.config` are
off-limits per coding-standards — the goal is a genuinely clean typecheck, not a
silenced one).

## Edge cases / risks

- **Behavior-preserving for tests:** `!` is a compile-time assertion only — zero
runtime change; the test suite must still pass identically afterward.
- **The page fix changes runtime behavior** (delete now sends a real index).
Must be verified by a test and ideally a manual delete in the running app.
- **Regression visibility:** without a CI gate, a clean typecheck can silently
re-break. Mitigated by the (deferred) CI recommendation; at minimum the final
task re-asserts a clean run.
- **`any` in tests** is permitted by coding-standards (mocking), so existing
`e: any` / mock `any` usage in these files is left as-is unless it causes an
error.

## Alternatives considered

- **Relax `noUncheckedIndexedAccess` or exclude tests from typecheck** —
rejected outright; coding-standards forbid fixing via tooling config, and the
issue explicitly calls this out.
- **Wrap every test access in `if (!x) throw`** — rejected as default; too noisy
for the 77 provably-safe sites. Used sparingly only where a value is reused.
- **Silence the page error with `as any` on the handler** — rejected; it would
hide the real delete bug instead of fixing it.
86 changes: 86 additions & 0 deletions .kiro/specs/fix-typecheck-errors/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Requirements — Fix pre-existing typecheck errors

> Traceability: **GitHub Issue #10**. Builds on the approved
> [design.md](./design.md).

## User stories & acceptance criteria

### R1 — Clean typecheck
**As a** contributor, **I want** `npx nuxi typecheck` to pass with zero errors,
**so that** type regressions are visible and the check can gate CI.

- **R1.1** — WHEN a developer runs `npx nuxi typecheck` on this branch, THE
SYSTEM SHALL exit `0` with **no `error TS` lines** (currently 78).
- **R1.2** — THE fix SHALL NOT modify any tooling/compiler config
(`tsconfig*.json`, `nuxt.config.ts`, `vitest.config.*`, etc.); in particular
`noUncheckedIndexedAccess` SHALL remain enabled and no test files SHALL be
excluded from typecheck.

### R2 — Test call-site fixes (the 77 `noUncheckedIndexedAccess` errors)
**As a** contributor, **I want** the test indexing errors resolved without
weakening the tests, **so that** the suite keeps asserting the same behavior.

- **R2.1** — WHEN an array index or destructured element that is provably
in range is dereferenced in a test, THE SYSTEM SHALL use a non-null assertion
(`!`) at the indexing site (e.g. `rows[0]!.x`, `const [row] = …; row!.x`,
`mock.calls[0]![0]`).
- **R2.2** — WHERE a single value is dereferenced repeatedly within one test
block, THE fix MAY hoist it to one `const x = arr[i]!` rather than repeating
`!` on every line, when that reads more clearly.
- **R2.3** — THE fix SHALL NOT change any production `utils/` source; only the
`.test.ts` / `.property.test.ts` call sites and `migration.test.ts`.
- **R2.4** — Each affected test SHALL retain its original assertions (no test
deleted, skipped, or weakened to dodge the error).

### R3 — Page component delete bug (the 1 `TS2322` error)
**As a** user, **I want** deleting a transaction from the account register to
send the correct index, **so that** the right transaction is deleted.

- **R3.1** — THE `deleteTx` handler in `pages/accounts/[...path].vue` SHALL
accept a `number` (matching `AccountRegister`'s `delete: [index: number]`
emit) and use it directly as the delete index.
- **R3.2** — WHEN a user confirms deletion of a transaction, THE SYSTEM SHALL
issue `DELETE /api/transactions` with `query.index` set to the emitted
numeric transaction index (never `undefined`).
- **R3.3** — THE `editTx` handler SHALL be typed `(_index: number)` to match the
`edit` emit, removing the `any` parameter.
- **R3.4** — The handler/emit contract SHALL be protected from regression by the
**typecheck gate** (R4): if `deleteTx` ever reverts to a non-`number` param it
reintroduces the same `TS2322`, failing CI. _Decision (2026-06-14):_ a runtime
test is NOT added — the suite has no component-mount harness, `deleteTx` is not
exported, and adding one (`@nuxt/test-utils` + happy-dom + Nuxt UI stubbing)
is disproportionate for a client-only fix the type gate already guards. See
design "CI gate" / Edge cases.

### R4 — CI gate
**As a** maintainer, **I want** typecheck enforced in CI, **so that** a clean
typecheck cannot silently regress.

- **R4.1** — THE repo SHALL contain `.github/workflows/ci.yml` that, on push and
pull_request targeting `main`, runs `npm ci` then `npx nuxi typecheck`.
- **R4.2** — THE workflow SHALL fail the job when typecheck reports any error
(i.e. rely on the non-zero exit code; no error-swallowing).
- **R4.3** — THE workflow SHALL pin `actions/checkout` and `actions/setup-node`
to major-version tags and use npm dependency caching.
- **R4.4** — THE workflow SHALL NOT include a unit-test job (deferred to a
follow-up that handles hledger-on-PATH; see #11).

## Non-functional requirements

- **NFR1 — Behavior preservation:** `npm test` SHALL pass after the change with
the same set of tests as before (the page fix may add one test; no test is
removed).
- **NFR2 — No `any` introduced:** the change SHALL NOT add `any`/`as any`; it
SHALL remove the one `any` in `editTx` (R3.3). Pre-existing permitted test
`any` (mocking, `catch (e: any)`) MAY remain.
- **NFR3 — Minimal diff:** fixes SHALL be confined to the files listed in the
design's "Files touched" table.

## Out of scope

- Adding a **unit-test job** to CI (needs hledger on PATH — see #11).
- The broader test-suite audit (skipped/gated tests, legacy paths) — that is
**#11**.
- Any refactor of `toRegisterRows` / `toTransactionInput` production types or
the `AccountRegister` component beyond the emit-contract alignment.
- Implementing transaction **edit** (still a "not yet supported" toast).
71 changes: 71 additions & 0 deletions .kiro/specs/fix-typecheck-errors/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Tasks — Fix pre-existing typecheck errors

> Traceability: **GitHub Issue #10**. Implements [design.md](./design.md),
> satisfies [requirements.md](./requirements.md).
> Verify each task with the single-file typecheck where possible:
> `npx vue-tsc --noEmit -p . 2>&1 | grep <file>` is unreliable under Nuxt, so
> use `npx nuxi typecheck 2>&1 | grep <file>` to confirm a file is clean, and a
> full `npx nuxi typecheck` only at the end.

## Test call-site fixes (Group R2)

- [x] **T1 — `utils/toRegisterRows.test.ts`** (38 errors). Add `!` at each
flagged index/destructure site (lines ~50–384); hoist `const row = rows[i]!`
where a row is reused across several asserts. _Verify:_ `npx nuxi typecheck
2>&1 | grep toRegisterRows.test` → no output; `npx vitest run
utils/toRegisterRows.test.ts` passes. _Covers:_ R2.1–R2.4.

- [x] **T2 — `utils/toTransactionInput.test.ts`** (13 errors, lines ~21–130).
`!` on indexed access. _Verify:_ typecheck grep clean + `npx vitest run
utils/toTransactionInput.test.ts`. _Covers:_ R2.1, R2.4.

- [x] **T3 — `utils/toTransactionInput.property.test.ts`** (12 errors, lines
~93–159). `!` on indexed access inside fast-check properties. _Verify:_
typecheck grep clean + `npx vitest run utils/toTransactionInput.property.test.ts`
(allow extra time — property tests are slow). _Covers:_ R2.1, R2.4.

- [x] **T4 — `utils/roundTrip.property.test.ts`** (7 errors, lines ~108–122).
`!` on destructured `row`. _Verify:_ typecheck grep clean + `npx vitest run
utils/roundTrip.property.test.ts`. _Covers:_ R2.1, R2.4.

- [x] **T5 — `utils/toRegisterRows.property.test.ts`** (6 errors, lines
~177–190). `!` on indexed/destructured row. _Verify:_ typecheck grep clean +
`npx vitest run utils/toRegisterRows.property.test.ts`. _Covers:_ R2.1, R2.4.

- [x] **T6 — `server/api/__tests__/migration.test.ts`** (1 error, line 113).
`mockAppendTransaction.mock.calls[0]![0]`. _Verify:_ typecheck grep clean +
`npx vitest run server/api/__tests__/migration.test.ts`. _Covers:_ R2.1, R2.4.

## Page component bug fix (Group R3)

- [ ] **T7 — Fix `deleteTx`/`editTx` in `pages/accounts/[...path].vue`.**
Change `deleteTx(row: { transactionIndex: number })` →
`deleteTx(transactionIndex: number)`, updating both `deleting.value` and the
`query.index` to use `transactionIndex` directly. Change `editTx(_row: any)` →
`editTx(_index: number)`. _Verify:_ `npx nuxi typecheck 2>&1 | grep
'\[...path\]'` → no output (clears the lone TS2322). _Covers:_ R3.1–R3.3.

- [x] **T8 — Regression guard for the delete path.** _Decision (2026-06-14):_
no runtime test added — the suite has no component-mount harness and
`deleteTx` is not exported, so a test would require new devDeps + brittle Nuxt
UI stubbing. The fix is instead guarded by the **CI typecheck gate** (T9):
reverting `deleteTx` to a non-`number` param reintroduces the same `TS2322`
and fails CI. design.md + R3.4 updated to reflect this. _Covers:_ R3.4, NFR1.

## CI gate (Group R4)

- [x] **T9 — Add `.github/workflows/ci.yml`.** On `push` and `pull_request` to
`main`: checkout (`actions/checkout@v4`), `actions/setup-node@v4` (Node 20,
`cache: npm`), `npm ci`, then `npx nuxi typecheck`. No test job. _Verify:_
`npx js-yaml .github/workflows/ci.yml` (or equivalent) parses; review the run
on the eventual PR. _Covers:_ R4.1–R4.4.

## Final checkpoint

- [x] **T10 — Full verification.**
1. `npx nuxi typecheck` → exits 0, **zero** `error TS` lines (R1.1).
2. `npm test` → full suite green (NFR1).
3. `git diff --stat` confined to the design's "Files touched" table (NFR3);
confirm no config files changed (R1.2) and no new `any` (NFR2).
Then update `AI-MAP.md` if any quirk note is warranted (e.g. the delete-index
bug fix), and report results.
Loading
Loading