Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

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';

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[];
}

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(),
});

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.

⬇ Download