singularity-forge/CLAUDE.md
Mikael Hugo 2eaea85020 fix(cli): add test-import-drift lint guard
AC1: Document convention in CLAUDE.md — test files over-importing (>5)
from a SF module should use namespace imports to avoid the anti-pattern
where a new describe() block uses an undeclared function (ReferenceError
at vitest run-time, not caught by biome lint).

AC3/AC4: add check-test-imports.mjs — static analysis script that scans
all *.test.{js,mjs,ts} files for itemized imports (≥6) + camelCase
identifier not in the import list. Exposes the failure mode at lint time.
Includes regression test (check-test-imports.test.mjs, 5/5 passing).

Closes sf-mp8ujgry-aoqcx0.
2026-05-16 23:26:38 +02:00

100 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude Code — Dev Guide for singularity-forge
See [AGENTS.md](AGENTS.md) for SF planning conventions and the promote-only state rule.
The foundational product contract is [ADR-0000: SF Is a Purpose-to-Software Compiler](docs/adr/0000-purpose-to-software-compiler.md).
## Build pipeline (MUST READ before editing extension source)
Source TypeScript files under `src/resources/extensions/sf/` are **not loaded
directly at runtime**. The loader (`src/loader.ts`) resolves extension entry
points from `dist/resources/extensions/sf/` (compiled `.js`) and copies them
to `~/.sf/agent/extensions/sf/` via `initResources`. Editing a `.ts` source
file has **no effect** until you recompile:
```bash
npm run copy-resources # tsc --project tsconfig.resources.json + file copy
```
This clears and rebuilds `dist/resources/` in one shot. Expect ~6090 s on
first run; subsequent runs reuse tsc's incremental cache if you keep one.
The `dist-redirect.mjs` resolver (used by tests and `dev-cli.js`) only
redirects `.js → .ts` for imports whose `parentURL` is inside `/src/`. Files
loaded from `~/.sf/agent/extensions/sf/` (compiled JS) are **not** redirected.
## Running tests
**Use vitest — no pre-compilation step needed.**
```bash
# Run a specific test file (fast, no coverage overhead):
npx vitest run src/resources/extensions/sf/tests/<name>.test.ts --config vitest.config.ts
# Run the full SF extension test suite:
npm run test:unit
# Run only tests affected by recent changes (fast feedback loop):
npx vitest run --changed --config vitest.config.ts
# Watch mode for active development:
npx vitest --config vitest.config.ts
```
**Do not use Python for one-off JSON/hash work.** The resource fingerprint in
`~/.sf/agent/managed-resources.json` is computed by Node's SHA-256 — Python's
`hashlib` produces a different result for the same files, which breaks the
fast-path check in `initResources` and causes a 30-60 s full resync on every
launch. Use `node -e` (or `jq`) for any shell-level JSON/hash operations in
this repo.
## Lint: test-import-drift guard
**Problem:** Test files with itemized `import { foo } from "module"` and many
named imports (≥6) carry a maintenance trap — adding a new `describe(...)`
block that uses a module function without updating the import list causes
`ReferenceError` at vitest run-time, not at lint time. Biome's ESM lint does
not catch `used identifier not declared`.
**Guard:** `npm run check:test-imports` runs `scripts/check-test-imports.mjs`
which statically scans all `*.test.{js,mjs,ts}` files for this anti-pattern.
It flags files that have ≥6 itemized imports AND reference a camelCase identifier
not in the import list. False positives for test locals, vitest globals, Node
builtins, and keyword-like words are filtered.
The check is NOT integrated into `npm run lint` by default (too broad for the
current threshold) but runs as `npm run check:test-imports`. Add it to the
`lint` script if the threshold is later lowered.
**Convention:** Test files whose subject is the public surface of a SF module
(migration tests, integration tests over a module's full API) should use
`import * as <Namespace> from "<module>"` instead of itemized imports when
≥6 named members are needed. This avoids the maintenance trap entirely.
The check script targets files with ≥6 itemized imports + an undeclared camelCase
identifier. Known false-positive categories are filtered (test locals, vitest globals,
Node builtins, keyword-like words, boolean flags), but some legitimate cases
may still appear in complex test files — use judgement when triaging output.
## Key directories
| Path | Purpose |
|------|---------|
| `src/resources/extensions/sf/` | Extension TypeScript source (edit here) |
| `dist/resources/extensions/sf/` | Compiled output (rebuilt by `copy-resources`) |
| `~/.sf/agent/extensions/sf/` | Installed copy (synced from dist on startup) |
| `src/resources/extensions/sf/prompts/` | Prompt templates (`.md`) |
| `src/resources/extensions/sf/tests/dist-redirect.mjs` | Module resolver hook for tests |
## Template variables
When adding a new `{{variable}}` to a prompt template in `prompts/`, you must:
1. Pass it in every `loadPrompt("template-name", { ..., newVar })` call site
(`auto-prompts.ts` is the main one for execute-task).
2. Add it (with a sensible placeholder value) to any test that calls
`loadPrompt("template-name", {...})` — see
`src/resources/extensions/sf/tests/plan-slice-prompt.test.ts`.
3. Run `npm run copy-resources` to land the change in dist.
`loadPrompt` throws at runtime if any `{{var}}` in the template has no
corresponding key in the vars object — this is intentional to catch
template/code drift early.