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.
100 lines
4.6 KiB
Markdown
100 lines
4.6 KiB
Markdown
# 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 ~60–90 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.
|