ci: run the TypeScript suite, and make its type checker pass (v1.43.1) - #65
ci: run the TypeScript suite, and make its type checker pass (v1.43.1)#65lapc506 wants to merge 1 commit into
Conversation
Three workflows, none of which ran `vitest` or `tsc`. `test-hooks.yml` covers `hooks/**`, `shellcheck.yml` covers `**.sh`, and `publish.yml` runs `npm run build` only on a `v*` tag push — which the CHANGELOG's own header records as something this project never does, having shipped without tags. So `src/`, the published library, had no gate: 60 tests that ran when somebody remembered, and a type checker that had been red long enough for one of four call sites to be silenced with a cast while the other three stayed broken. This is shape 1 from the toolkit's own enforcement audit — "it does not look" — and the partial coverage made it worse than an outright gap. The hook suite IS gated, so every pull request went green, and green reads as verified. A check that covers part of a surface while reporting like it covers all of it is shape 4 applied to scope instead of to an assertion: correct, and binding nothing where it matters. NO `paths:` FILTER, which is the one place `test.yml` departs from its two siblings. A path filter is how this gap was built: scoping `test-hooks.yml` to the directory it tests is right for that workflow and is exactly what left `src/` uncovered. Enumerating the paths that can break a type check means maintaining a second copy of the dependency graph, and the second copy drifts. The suite costs ~1.4 s; scoping buys nothing. Neither step is `continue-on-error`, because an advisory test run is a correct verdict that stops nothing. THE TYPE FIX. `printHumanResult` demanded `Record<string, unknown>` while its body only calls `Object.entries` and never indexes by an arbitrary key. No `*Result` interface in `src/lib/` declares an index signature, so none of them could satisfy it. Widening the parameter to `object` fixes all three failing call sites and makes the `as unknown as Record<string, unknown>` at the fourth unnecessary; it is removed. `Object.entries` accepts both types identically, so there is no behaviour change. The cast is the artifact worth naming: somebody hit this at one call site, silenced it there, and the other three stayed red because nothing ran `tsc`. The fix is smaller than the workaround because the workaround never asked what the signature was demanding. Verified locally, exactly what the workflow will run: tsc --noEmit 0 errors (was 3) npm test 60 passed hooks/test-hooks.sh 346 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 1 P3. Confidence: 4.80/5.00.
Walkthrough
main directly instead of following GitFlow expectations (feature → develop → main). This is a non-blocking informative notice, and does not affect the review verdict of the code itself.
Review Walkthrough
This PR establishes a TypeScript CI workflow (test.yml) running on every pull request and push to main to typecheck and run the Vitest test suite. It also fixes a compilation error in src/cli.ts by widening the parameter of printHumanResult from Record<string, unknown> to object, making the tsc compiler pass successfully.
Areas Reviewed
- CI Workflows: Reviewed
.github/workflows/test.ymlconfiguration, actions, and runner settings. - CLI Code: Reviewed
src/cli.tsfor typings ofprintHumanResultand usage ofObject.entries. - Project Metadata: Reviewed
.claude-plugin/marketplace.json,.claude-plugin/plugin.json,package.json, andCHANGELOG.mdversion bump consistency.
Safety Rationale
Widening the input type of printHumanResult to object has zero runtime impact and resolves static analysis errors due to missing index signatures on return types from src/lib/ functions. The test CI setup runs identical commands to those executed locally, ensuring continuous stability without impacting deployment or release flows.
Approved — 0 blockers, 1 P3.
🔵 P3 — Minor
.github/workflows/test.yml:53— 🔵 P3 (minor) — Adding a"typecheck": "tsc --noEmit"script topackage.jsonand callingnpm run typecheckhere aligns CI commands with local developer tooling. Currently, developers must manually runnpx tsc --noEmitwhich can drift from CI patterns over time if not declared as a standard package script.
[pass 1]
Total findings: 1 business context (1 total)
| run: npm ci | ||
|
|
||
| # Before the suite: a type error is a fact about the code that does not | ||
| # need a test to run, and reporting it first makes a red build easier to |
There was a problem hiding this comment.
🔵 P3 (minor) — Adding a "typecheck": "tsc --noEmit" script to package.json and calling npm run typecheck here aligns CI commands with local developer tooling. Currently, developers must manually run npx tsc --noEmit which can drift from CI patterns over time if not declared as a standard package script.
[pass 1]
…and not `any` PR #65 (`ci: run the TypeScript suite`, MERGEABLE) adds a workflow running `npx tsc --noEmit` and `npm test` on every PR with **no `paths:` filter**. Its own changelog reports tsc "failing on three errors in `src/cli.ts`" — measured against `main`, where `src/audit/worktree-cleanup.test.ts` does not exist yet. This PR introduces that file carrying **9 errors of its own**, so whichever of the two lands second goes red on a gate the other one built. Fixed here rather than left as a merge-order trap. The fix is a declaration file, not a suppression. `scripts/worktree-cleanup.mjs` stays plain ESM because a slash command invokes it as `node scripts/...` with no build step available; `scripts/worktree-cleanup.d.mts` sits beside it and TypeScript resolves it automatically for the `.mjs` import. It is worth more than the error count. The suite was `any` throughout, and every case is `{ ...clean, oneField: x }` — an excess-property check does not reach inside a spread, so under `any` a MISSPELLED field name is not an error, it is a fact the classifier never reads. The test then passes while asserting nothing whatsoever about the guard named in its own title. That is precisely the "regression test that still passes while proving nothing" this suite was written to avoid, present in the suite itself. Typing `clean` as `WorktreeFacts` surfaced it immediately: `mergedBy: 'pr'` widened to `string` and failed against the `'ancestor' | 'cherry' | 'pr' | null` union, in every one of the spread cases. `ahead` and `midOperation` now carry their `null`-means-unmeasured meaning in the type rather than only in a comment. Measured: `tsc --noEmit` on worktree-cleanup files 9 errors -> 0. Repo total 15 -> 6, and all 6 remaining are the pre-existing `src/cli.ts` ones that PR #65 fixes — so after either merge order the gate is green. `npm run build` green, 109 tests passing. Created by Claude Opus 5 on behalf of @lapc506
The gap
Three workflows, none running
vitestortsc:test-hooks.ymlhooks/**,scripts/build-rules.mjsshellcheck.yml**.shpublish.ymlv*tagnpm run buildpublish.ymleffectively never fires — the note at the top ofCHANGELOG.mdrecords that this project shipped withoutv*tags. Sosrc/, the published library, had no gate at all: 60 tests that ran when somebody remembered, and atscthat had been red on three errors.This is shape 1 from this toolkit's own ENF audit — "it does not look." And the partial coverage made it worse than an outright gap: the hook suite is gated, so every PR went green, and green reads as verified. A check covering part of a surface while reporting like it covers all of it is shape 4 applied to scope rather than to an assertion — correct, and binding nothing where it matters.
No
paths:filter, deliberatelyThe one place
test.ymldeparts from its siblings. A path filter is how this gap was built: scopingtest-hooks.ymlto the directory it tests is right for that workflow, and is exactly what leftsrc/uncovered.Enumerating the paths that can break a type check means maintaining a second copy of the dependency graph, and the second copy is what drifts. The suite costs ~1.4 s. There is nothing to buy by scoping it.
Neither step is
continue-on-error— an advisory test run is a correct verdict that stops nothing.The type fix
printHumanResultdemandedRecord<string, unknown>while its body only callsObject.entriesand never indexes by an arbitrary key. No*Resultinterface insrc/lib/declares an index signature, so none satisfied it.Two lines. Fixes all three failing call sites and makes the cast at the fourth — the doctor path — unnecessary.
Object.entriesaccepts both types identically, so no behaviour changes.The cast is the artifact worth naming. Somebody hit this error at one of four call sites, silenced it with
as unknown as, and the other three stayed red because nothing ever rantsc. The fix is smaller than the workaround because the workaround never asked what the signature was demanding.Verification
Run locally — exactly what the workflow will run:
Not fixed here
The CHANGELOG link footer is missing entries for 1.39.0, 1.40.0, 1.41.0 and 1.43.0, and
[Unreleased]still compares againstv1.38.0. Four releases of drift, so it belongs in its own change rather than riding along here.Created by Claude Code on behalf of @lapc506
🤖 Generated with Claude Code