What it reifies
@frenchexdev/requirements-scanner is the AST walker that turns a directory of test files into a typed manifest of test-to-code bindings. It is the first Tier-1 analyzer because every other Tier-1 package — compliance, test-smells, behavioural-check, trace — reads its output. Without the scanner, the registries the kernel populates would describe Features and Requirements but say nothing about which tests verify which Acceptance Criteria; with the scanner, the manifest closes the loop.
The package's package.json description is precise: "AST test-bindings scanner — walks @FeatureTest/@Verifies test files, emits a Feature×AC → SymbolTarget[] manifest." Three nouns matter: @FeatureTest, @Verifies, and SymbolTarget. The first is the class-level decorator that pairs a test class with the Feature it claims to test; the second is the method-level decorator that pairs each test method with one Acceptance Criterion; the third is the { file, symbol } pair the scanner emits for each AST node the test method actually calls.
The public surface
The src/index.ts is a single re-export:
// @frenchexdev/requirements-scanner — Tier-1 AST scanner for @FeatureTest/@Verifies test files.
export * from './test-bindings-scanner';// @frenchexdev/requirements-scanner — Tier-1 AST scanner for @FeatureTest/@Verifies test files.
export * from './test-bindings-scanner';The scanner file itself exports the type vocabulary and the entry points:
import * as ts from 'typescript';
import type { FileSystem } from '@frenchexdev/requirements-core/ports';
export interface SymbolTarget {
file: string;
symbol: string;
}
/** featureId → acName → [{file, symbol}]. */
export type BindingsManifest = Record<string, Record<string, SymbolTarget[]>>;
export type ImportKind = 'named' | 'default' | 'namespace';
export interface ImportEntry {
localName: string;
moduleSpec: string;
kind: ImportKind;
}
export interface MethodBinding {
methodName: string;
ac: string;
targets: SymbolTarget[];
}
export interface ParsedTestClass {
className: string;
featureClass: string;
methods: MethodBinding[];
}
export interface TestParseResult {
file: string;
classes: ParsedTestClass[];
warnings: string[];
}import * as ts from 'typescript';
import type { FileSystem } from '@frenchexdev/requirements-core/ports';
export interface SymbolTarget {
file: string;
symbol: string;
}
/** featureId → acName → [{file, symbol}]. */
export type BindingsManifest = Record<string, Record<string, SymbolTarget[]>>;
export type ImportKind = 'named' | 'default' | 'namespace';
export interface ImportEntry {
localName: string;
moduleSpec: string;
kind: ImportKind;
}
export interface MethodBinding {
methodName: string;
ac: string;
targets: SymbolTarget[];
}
export interface ParsedTestClass {
className: string;
featureClass: string;
methods: MethodBinding[];
}
export interface TestParseResult {
file: string;
classes: ParsedTestClass[];
warnings: string[];
}Five concepts. A SymbolTarget is the leaf — one symbol in one file. A MethodBinding is one test method, the AC it claims, and the symbols its body reaches. A ParsedTestClass is one @FeatureTest-decorated class with its Feature and its method bindings. A TestParseResult is the per-file output, including any warnings the parser raises. The BindingsManifest is the merged top-level view: a two-level map indexed by Feature ID then by AC name.
The scanner is pure in the functional sense. Every parser function — parseTestFile, collectImports, resolveTarget, getRootIdentifier, aggregateTestBindings, diffManifests — takes its inputs and returns its outputs without touching fs or child_process. The single side-effecting boundary is the FileSystem port from @frenchexdev/requirements-core/ports: callers pass it in, the scanner reads test files through it, and the scanner does no other I/O.
Where it sits
Tier 1, the leaf analyzer. Depends on requirements-core (the transitional kernel, soon to be folded into requirements-shared-kernel), requirements-requirements (for the vocabulary), and typescript as a peer-dep — the scanner uses the TypeScript compiler API directly. No ts-morph; the parser is hand-written against ts.SourceFile.
Two things the scanner must not do:
- Read or interpret the registries. That is the compliance reporter's job (Part 07). The scanner produces the manifest; it does not cross-reference the manifest with what the kernel knows about Features and Requirements.
- Render output. Mermaid graphs, console tables, JSON reports — all belong to higher layers. The scanner's output is a plain
BindingsManifest; consumers decide what to do with it.
A concrete call-site
The compliance reporter is the first consumer. Here is the shape:
import { scanTestBindings, type BindingsManifest } from '@frenchexdev/requirements-scanner';
import { generateReport } from '@frenchexdev/requirements-compliance/compliance-report';
import { fs } from '@frenchexdev/requirements-core/ports';
const manifest: BindingsManifest = await scanTestBindings(fs, {
testDir: 'packages/requirements-compliance/test',
srcDir: 'packages/requirements-compliance/src',
});
const report = generateReport({
bindings: manifest,
features: getFeatureRegistry(),
requirements: getRequirementRegistry(),
});import { scanTestBindings, type BindingsManifest } from '@frenchexdev/requirements-scanner';
import { generateReport } from '@frenchexdev/requirements-compliance/compliance-report';
import { fs } from '@frenchexdev/requirements-core/ports';
const manifest: BindingsManifest = await scanTestBindings(fs, {
testDir: 'packages/requirements-compliance/test',
srcDir: 'packages/requirements-compliance/src',
});
const report = generateReport({
bindings: manifest,
features: getFeatureRegistry(),
requirements: getRequirementRegistry(),
});scanTestBindings is the entry point. fs is the FileSystem port — the scanner reads test files through it, the test imports it walks resolve relative to srcDir. The output is a BindingsManifest; the reporter then walks it, cross-referencing against the Feature and Requirement registries the kernel maintains.
A second consumer is requirements-trace (Part 10): it ingests the same manifest, joins it with the satisfaction links from @Satisfies, and produces the REQ → FEAT → AC → TEST four-tier graph.
Why it is its own package
Two arguments, both forced by the consumer surface.
First, the scanner is used by ts-codegen-pipeline, which sits in a different bounded context — the Roslyn-style multi-stage code generator described in project_sourcegen_package.md. Before the split, ts-codegen-pipeline had to depend on the whole requirements-lib barrel to consume scanTestBindings. It pulled in ajv, picocolors, diff, every Tier-1 and Tier-2 capability, plus the Stryker plugins — five hundred kilobytes of transitive dependency for one function. After the split, ts-codegen-pipeline depends on requirements-scanner directly. The transitive cost drops to the scanner's own dependency: the kernel, the vocabulary, and typescript as peer-dep.
Second, the scanner is the proof-of-concept extraction for the whole roadmap. Phase 1a of the roadmap is named "requirements-scanner — the proof-of-concept extraction"; the scanner is what the roadmap chose to extract first because it is the leaf with the cleanest boundary. Every subsequent extraction repeats the same pattern: a workspace package created, the implementation moved, the barrel re-exports preserved as a compat shim, the cross-package consumer (ts-codegen-pipeline in this case) switched to the new direct import. The five sibling Features in requirements-lib that were tracking ReqDiscoverableTraceabilityRequirement updated their @Satisfies import paths to the new package and the scanner's own Feature — TestBindingsScannerFeature with 88 ACs — moved with the code.
The scanner is also where the "tests travel with the code they cover" rule first becomes a workspace constraint. Before the split, scanner tests lived in requirements-lib/test/; after the split, they live in requirements-scanner/test/. The per-package coverage threshold (≥98% on src/lib/ equivalent paths) is preserved by the move. A regression in scanner coverage now fails the scanner package's own CI-equivalent run; the failure cannot be hidden by an unrelated test in a sibling concern.
The next page covers the largest single consumer of the manifest: requirements-compliance, which turns the scanner output and the kernel registries into the traceability matrix that npx requirements compliance --strict reads.