What it reifies
@frenchexdev/requirements-scaffolders is the first Tier-2 package — the layer where use cases live. A use case is a coherent workflow the CLI exposes; the workflow this package owns is "generate test source for every uncovered AC the compliance report finds". The package turns a Feature with five Acceptance Criteria, of which three have no test bindings, into three pieces of stub TypeScript source — each containing the @FeatureTest class, the @Verifies method, and the // TODO: implement body — placed in the appropriate test directory for its level.
The package.json description: "TestScaffolder registry + 7 built-in scaffolders (unit / functional / e2e / a11y / i18n / visual / perf)."
Seven scaffolders because the kernel's TestLevel enum has seven members:
export enum TestLevel {
Unit = 'unit',
Functional = 'functional',
EndToEnd = 'e2e',
Accessibility = 'a11y',
Internationalization = 'i18n',
Visual = 'visual',
Performance = 'perf',
}export enum TestLevel {
Unit = 'unit',
Functional = 'functional',
EndToEnd = 'e2e',
Accessibility = 'a11y',
Internationalization = 'i18n',
Visual = 'visual',
Performance = 'perf',
}Each scaffolder owns one level. Each level has its own conventions for where the test file lives (test/unit/ vs test/e2e/ vs test/a11y/), what filename suffix to use (.test.ts vs .e2e.spec.ts vs .a11y.spec.ts), and what boilerplate the generated source needs (vitest imports for unit, Playwright imports for e2e, pa11y harness for a11y). The scaffolder encapsulates those decisions; the caller chooses which scaffolder to invoke.
The public surface
src/index.ts re-exports two modules:
// @frenchexdev/requirements-scaffolders — Tier-2 application service:
// TestScaffolder port + registry + 7 built-in scaffolders + scaffold-core.
export * from './scaffold-core';
export * from './scaffolders/index';// @frenchexdev/requirements-scaffolders — Tier-2 application service:
// TestScaffolder port + registry + 7 built-in scaffolders + scaffold-core.
export * from './scaffold-core';
export * from './scaffolders/index';The TestScaffolder port is the contract every scaffolder implements:
import type { Feature, TestLevel, ACResult } from '@frenchexdev/requirements-shared-kernel';
export interface UncoveredAc {
featureId: string;
acName: string;
feature: typeof Feature;
}
export interface ScaffoldedSource {
path: string;
content: string;
}
export interface TestScaffolder {
level: TestLevel;
defaultDir: string;
filenameSuffix: string;
description: string;
generate(feature: typeof Feature, uncovered: UncoveredAc[]): ScaffoldedSource[];
}import type { Feature, TestLevel, ACResult } from '@frenchexdev/requirements-shared-kernel';
export interface UncoveredAc {
featureId: string;
acName: string;
feature: typeof Feature;
}
export interface ScaffoldedSource {
path: string;
content: string;
}
export interface TestScaffolder {
level: TestLevel;
defaultDir: string;
filenameSuffix: string;
description: string;
generate(feature: typeof Feature, uncovered: UncoveredAc[]): ScaffoldedSource[];
}The port is small on purpose. A scaffolder reports its level and its default conventions; it receives a Feature class plus a list of uncovered ACs; it returns one or more ScaffoldedSource records, each a { path, content } pair. The package never writes the source to disk — that is the caller's responsibility, usually requirements-sync (Part 14) for the preview/apply pattern.
The registry exposes the seven built-ins by level:
import type { TestScaffolder } from './port';
export const UNIT_SCAFFOLDER: TestScaffolder;
export const FUNCTIONAL_SCAFFOLDER: TestScaffolder;
export const E2E_SCAFFOLDER: TestScaffolder;
export const A11Y_SCAFFOLDER: TestScaffolder;
export const I18N_SCAFFOLDER: TestScaffolder;
export const VISUAL_SCAFFOLDER: TestScaffolder;
export const PERF_SCAFFOLDER: TestScaffolder;
export function createScaffolderRegistry(
overrides?: Partial<Record<TestLevel, TestScaffolder>>,
): Record<TestLevel, TestScaffolder>;import type { TestScaffolder } from './port';
export const UNIT_SCAFFOLDER: TestScaffolder;
export const FUNCTIONAL_SCAFFOLDER: TestScaffolder;
export const E2E_SCAFFOLDER: TestScaffolder;
export const A11Y_SCAFFOLDER: TestScaffolder;
export const I18N_SCAFFOLDER: TestScaffolder;
export const VISUAL_SCAFFOLDER: TestScaffolder;
export const PERF_SCAFFOLDER: TestScaffolder;
export function createScaffolderRegistry(
overrides?: Partial<Record<TestLevel, TestScaffolder>>,
): Record<TestLevel, TestScaffolder>;The createScaffolderRegistry factory accepts overrides — a project that wants its e2e scaffolder to emit Cypress imports instead of Playwright can pass a single replacement and inherit the other six.
scaffold-core is the orchestrator. It computes which ACs are uncovered (joining the compliance report with the Feature registry), picks the scaffolder per level, calls generate, aggregates the results into a WritePlan, and returns. The naming mirrors the rest of the family: core always means pure orchestrator that takes typed inputs and returns typed outputs.
Where it sits
Tier 2, first sibling. Depends on requirements-compliance (for the uncovered-AC computation), requirements-spec-io (for source-string emission), requirements-requirements, and the kernel. Does not depend on Commander, clack, picocolors, or any CLI library. The package can be consumed by an IDE extension, a CI plugin, a custom CLI, or the official requirements shell — none of those consumers force the others to pull in unwanted dependencies.
Three things the package must not do:
- Write files.
generatereturnsScaffoldedSource[]. The caller decides when and where to write. - Run an interactive prompt. A wizard that asks the user "which test level for this AC?" is
requirements-wizards's job (Part 15); this package answers "given a chosen level, what source should I emit?". - Hard-code the seven levels. The port is the contract; the seven built-ins are convenient defaults. A project that needs an eighth level (say
accessibility-keyboard-only) registers a new scaffolder viacreateScaffolderRegistry({ ['kbd-a11y']: myScaffolder } as never)and the rest of the family works unchanged.
A concrete call-site
The CLI's scaffold test and scaffold e2e subcommands wire the package as follows:
import {
generateScaffoldForSpec,
createScaffolderRegistry,
type WritePlan,
} from '@frenchexdev/requirements-scaffolders';
import { TestLevel } from '@frenchexdev/requirements-shared-kernel';
import { applyWritePlan } from '@frenchexdev/requirements-sync';
import { fs } from '@frenchexdev/requirements-core/ports';
const registry = createScaffolderRegistry();
const plan: WritePlan = await generateScaffoldForSpec({
featureId: 'COMPLIANCE-CORE',
level: TestLevel.Unit,
registry,
testDirsByLevel: { [TestLevel.Unit]: 'test/unit' },
});
if (options.dryRun) {
for (const source of plan.sources) {
console.log(`would write ${source.path} (${source.content.length} bytes)`);
}
} else {
await applyWritePlan(fs, plan);
}import {
generateScaffoldForSpec,
createScaffolderRegistry,
type WritePlan,
} from '@frenchexdev/requirements-scaffolders';
import { TestLevel } from '@frenchexdev/requirements-shared-kernel';
import { applyWritePlan } from '@frenchexdev/requirements-sync';
import { fs } from '@frenchexdev/requirements-core/ports';
const registry = createScaffolderRegistry();
const plan: WritePlan = await generateScaffoldForSpec({
featureId: 'COMPLIANCE-CORE',
level: TestLevel.Unit,
registry,
testDirsByLevel: { [TestLevel.Unit]: 'test/unit' },
});
if (options.dryRun) {
for (const source of plan.sources) {
console.log(`would write ${source.path} (${source.content.length} bytes)`);
}
} else {
await applyWritePlan(fs, plan);
}The pattern is the same compute → preview → apply rhythm the rest of the family uses. generateScaffoldForSpec produces the typed plan; applyWritePlan (from requirements-sync) is the only side-effecting step, and the CLI gates it on --dry-run / interactive confirmation.
Why it is its own package
Two arguments.
First, the scaffolder registry is the Open/Closed seam of the test-emission concern. A project that needs a custom scaffolder — say, a domain-specific contract test for a SagaOrchestrator — registers it via createScaffolderRegistry and the rest of the family treats it like any other built-in. The Open/Closed Principle is enforced at the package boundary: the package never needs to be modified to support a new test level; new scaffolders arrive as new files in the consumer's repo. Bundling scaffolders into requirements-lib made the registry a closed shop because the registry implementation was tangled with the rest of the lib.
Second, the package's consumer surface includes future tooling. The CV site's build pipeline could use the scaffolder registry to emit per-blog-article test stubs (for a future "every article has a smoke test" convention). An LSP extension could use it to flash a "scaffold tests for this Feature" code action. A CI bot could use it to open a PR with the scaffolded tests when compliance reports orphans. Each future consumer pulls in only requirements-scaffolders, not the rest of the family.
The seven built-in scaffolders are also the reference implementations every future scaffolder mimics. Their source under packages/requirements-scaffolders/src/scaffolders/ is small (one file per level, roughly 100 lines each), readable, and copy-paste-friendly. The cookbook for adding a new scaffolder is "copy the closest sibling, change the level enum and the template, export from index.ts, register in the consumer". That cookbook lives in Part 19.
The next page covers the package that owns the preview/apply rhythm for any plan — including the WritePlan returned by scaffold-core: requirements-sync.