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.
4.6 KiB
Claude Code — Dev Guide for singularity-forge
See 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.
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:
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.
# 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:
- Pass it in every
loadPrompt("template-name", { ..., newVar })call site (auto-prompts.tsis the main one for execute-task). - Add it (with a sensible placeholder value) to any test that calls
loadPrompt("template-name", {...})— seesrc/resources/extensions/sf/tests/plan-slice-prompt.test.ts. - Run
npm run copy-resourcesto 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.