Skip to content

Add Spec Kit, linting, formatting, coverage and a live sweep - #26

Merged
adamjohnwright merged 3 commits into
mainfrom
chore/speckit-and-quality
Sep 14, 2026
Merged

adamjohnwright merged 3 commits into
mainfrom
chore/speckit-and-quality

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

Stacked on #25. Base is fix/formatter-field-paths; retarget to main once that merges.

Three things this repo had no way to catch, and now does.

Spec Kit

Design decisions were living in conversations and PR descriptions, where they are unfindable a month later. .specify/ carries the templates; specs/ will carry the decisions.

The constitution (.specify/memory/constitution.md) is not aspirational — every principle is written from a failure this repo actually had, or a boundary the team has already decided:

  • Reactome data through the public services; no Neo4j from a deployed instance
  • Analysis runs in the Analysis Service, not in the agent
  • A type assertion is not a verification
  • Silence is the dangerous failure, not the crash

The Spec Kit skills under .claude/skills/ are tracked deliberately — people clone this repo and point an agent at it, so the workflow should come with it. .gitignore now excludes only machine-local settings rather than all of .claude/.

Static analysis

ESLint with type-aware rules, plus Prettier. The no-unsafe-* rules are warnings — the honest local type for an unpinned Reactome field is any, and a fixture test is the real guard against bad shapes. The rules that catch silent wrongness are errors.

Two genuine bugs fell out:

  • neo4j.ts handed an async function to process.once, so a failed driver close rejected with nobody listening — an unhandled rejection on shutdown.
  • http.ts rethrew lastError, which is unknown, so a non-Error rejection propagated as something callers could not read .message off.

require-await is off by decision: MCP handlers are declared async uniformly so every tool in a file has the same signature.

Typechecking that covers the tests

tests/ was in no tsconfig at all, so nothing checked it. tsconfig.eslint.json includes tests and config files, and npm run typecheck now uses it.

Turning on noUncheckedIndexedAccess found three unguarded array reads in src/ — the same class as the field-path bugs in #25. Tests get textOf() and calledUrl() helpers so the flag does not just become a field of !.

Coverage

44% statements, via npm run test:coverage. Thresholds are set just under that as a ratchet, not a target — most tools still have no test.

A live sweep

npm run sweep calls all 53 tools against the live services and flags output containing undefined, [object Object], or nothing at all. Unit tests pin shapes we already know about; only this finds a tool that never worked.

It found one immediately: search_diagram shared the grouped-results helper, but /search/diagram/{id} returns a flat entries array — so the tool threw result.results is not iterable on every call. It had never returned an answer. Fixed and pinned.

It runs weekly and on demand rather than in CI, because a red run there can mean Reactome changed rather than this repo did.

CI

Now runs lint, format check, typecheck, build and coverage. The build was never run in CI before. npm audit runs advisory-only in its own job, so a new advisory against a transitive dev dependency does not block an unrelated PR.

Note on the lockfile

package-lock.json is regenerated. The committed one was stale enough that npm audit reported 8 vulnerabilities (5 high); a fresh resolve within the same semver ranges reports 0. npm ci from the new lockfile passes the full check.

Verification

  • npm ci from a clean tree, then npm run check (lint → format → typecheck → build → test): passes
  • 59 tests, 0 lint errors, 0 lint warnings
  • npm audit: 0 vulnerabilities

The Prettier pass is its own commit (d58da7c) and is listed in .git-blame-ignore-revs, so it can be skipped in review and in blame.

🤖 Generated with Claude Code

adamjohnwright and others added 3 commits September 14, 2026 15:20
Three things this repo had no way to catch, and now does.

**Spec Kit.** Design decisions were living in conversations and pull request
descriptions, where they are unfindable a month later. `.specify/` carries the
templates and `specs/` will carry the decisions. The constitution in
`.specify/memory/constitution.md` is not aspirational -- every principle is
written from a failure this repo actually had, or a boundary the team has
already decided (Reactome data through the public services; no Neo4j from a
deployed instance; analysis runs in the Analysis Service).

The Spec Kit skills under `.claude/skills/` are tracked deliberately. People
clone this repo and point an agent at it, so the workflow should come with it;
`.gitignore` now excludes only machine-local settings instead of all of
`.claude/`.

**Static analysis.** ESLint with type-aware rules, and Prettier. The unsafe-*
rules are warnings -- the honest local type for an unpinned Reactome field is
`any`, and a fixture test is the real guard against bad shapes. The rules that
catch silent wrongness are errors. Two genuine bugs fell out:

  - neo4j.ts handed an async function to `process.once`, so a failed driver
    close rejected with nobody listening -- an unhandled rejection on shutdown.
  - http.ts rethrew `lastError`, which is `unknown`, so a non-Error rejection
    propagated as something callers could not read `.message` off.

`require-await` is off by decision: MCP handlers are declared async uniformly
so every tool in a file has the same signature.

**Typechecking that covers the tests.** `tests/` was in no tsconfig at all, so
nothing checked it. `tsconfig.eslint.json` includes tests and config files;
`npm run typecheck` now uses it. Turning on `noUncheckedIndexedAccess` found
three unguarded array reads in src -- the same class as the field-path bugs it
follows. Tests get `textOf()` and `calledUrl()` helpers so the flag does not
just become a field of `!`.

**Coverage.** 44% statements, reported by `npm run test:coverage`. Thresholds
are set just under that as a ratchet, not a target.

**A live sweep.** `npm run sweep` calls all 53 tools against the live services
and flags output containing `undefined`, `[object Object]`, or nothing at all.
Unit tests pin shapes we already know; only this finds a tool that never worked.
It found one immediately: `search_diagram` shared the grouped-results helper,
but `/search/diagram/{id}` returns a flat `entries` array, so the tool threw
"result.results is not iterable" on every call. Fixed and pinned.

It runs weekly and on demand rather than in CI, because a red run there can
mean Reactome changed rather than this repo did.

**CI** now runs lint, format check, typecheck, build and coverage. The build
was never run in CI before. `npm audit` runs advisory-only in its own job, so a
new advisory against a transitive dev dependency does not block an unrelated PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only -- no behaviour change. Kept separate so it can be skipped in
review and in blame, per the constitution's rule that broad reformatting rides
with substantive change or not at all.

Scope is the code this repo owns. The vendored Spec Kit assets under .specify/
and .claude/ are excluded so that a `specify` upgrade does not become a
conflict, and markdown is excluded because reformatting prose churns
documentation for no readability gain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant