What it reifies
@frenchexdev/requirements-spec-io is the boundary where the spec stops being TypeScript-only. The kernel and the vocabulary together define the spec in code — twenty-two Requirement subclasses and roughly thirty Feature subclasses, each a .ts file with typed fields. That is convenient for type-checking and for consumption by other TypeScript packages, but it is not consumable by non-TypeScript tooling: a CI badge generator, a JSON-Schema-aware editor, an external audit tool, a translation layer to another spec format. This package makes the spec serialisable to JSON, validates the JSON against a generated JSON Schema, and round-trips the JSON back into typed TypeScript source.
The package.json description: "Spec serialization (JSON ↔ TS source) + JSON Schema generation/registration + bidirectional sync."
The public surface
src/index.ts re-exports five modules, each owning one direction or one helper:
// @frenchexdev/requirements-spec-io — Tier-1: spec serialization (JSON ↔ TS source)
// + JSON Schema generation/registration + bidirectional sync.
export * from './spec-from-source';
export * from './spec-to-source';
export * from './schema-generate';
export * from './schema-register';
export * from './bidi-sync-core';// @frenchexdev/requirements-spec-io — Tier-1: spec serialization (JSON ↔ TS source)
// + JSON Schema generation/registration + bidirectional sync.
export * from './spec-from-source';
export * from './spec-to-source';
export * from './schema-generate';
export * from './schema-register';
export * from './bidi-sync-core';spec-from-source is the TS → JSON direction. It uses ts-morph to parse a req-*.ts or feature-*.ts file, extract the abstract-field values from the class declaration, and produce a typed RequirementSpec or FeatureSpec JSON value. The TypeScript compiler API is doing the work; ts-morph is the ergonomic wrapper.
spec-to-source is the JSON → TS direction. It takes a RequirementSpec JSON value and emits a .ts source string that, when compiled, produces an abstract class with the same abstract-field values. The emitter uses literal types ('Approved' as const, Priority.Critical) to preserve compile-time narrowing.
schema-generate uses ts-json-schema-generator (a devDep) to walk the kernel's type declarations and emit a requirement-spec.schema.json and feature-spec.schema.json. These schemas are then committed under packages/requirements-spec-io/schemas/ and shipped as part of the package's files array.
schema-register registers the schemas with an ajv instance. The signature:
import Ajv, { type ValidateFunction } from 'ajv';
import type { RequirementSpec, FeatureSpec } from '@frenchexdev/requirements-shared-kernel';
export declare function registerSchemas(ajv: Ajv): {
validateRequirement: ValidateFunction<RequirementSpec>;
validateFeature: ValidateFunction<FeatureSpec>;
};import Ajv, { type ValidateFunction } from 'ajv';
import type { RequirementSpec, FeatureSpec } from '@frenchexdev/requirements-shared-kernel';
export declare function registerSchemas(ajv: Ajv): {
validateRequirement: ValidateFunction<RequirementSpec>;
validateFeature: ValidateFunction<FeatureSpec>;
};bidi-sync-core is the orchestrator. It compares an in-memory spec (built from the registries at module load) against the on-disk JSON (parsed and validated), produces a SyncPlan describing the diff, and exposes apply / dry-run modes. The SyncPlan shape is reused by requirements-sync (Part 14), which adds the coloured-diff rendering layer.
Where it sits
Tier 1, second-to-last in the dependency depth of the analyzer band. Depends on requirements-compliance (because the bidi-sync uses crossReference to enrich the diff with traceability impact), requirements-requirements (for the vocabulary), and three external libraries:
ajv^8.17.1 — schema validationts-morph^24.0.0 — TypeScript AST manipulationts-json-schema-generator^2.3.0 — devDep only, schema regeneration
Three things the package must not do:
- Write source files.
spec-to-sourcereturns a string; the caller decides whether to write it. The package never writes; therequirements-syncuse case (Part 14) wraps the writing. - Mutate the kernel registries.
spec-from-sourceproduces standaloneRequirementSpec/FeatureSpecvalues; the registries are populated only by side-effect imports of the actual.tssource files. - Ship a default
ajvinstance. The caller passes one in. Different consumers may share anajvacross many schemas, or sandbox it, or replaceajvwith a different validator entirely.
A concrete call-site
The CV site's build pipeline uses spec-from-source to generate per-blog-article traceability boxes. The CLI's feature schema subcommand uses schema-generate to regenerate the schemas. The CLI's feature sync subcommand uses bidi-sync-core to diff and apply. Here is the feature sync shape:
import {
diffSpecToDisk,
applySyncPlan,
type SyncPlan,
} from '@frenchexdev/requirements-spec-io/bidi-sync-core';
import { registerSchemas } from '@frenchexdev/requirements-spec-io/schema-register';
import Ajv from 'ajv';
import { fs } from '@frenchexdev/requirements-core/ports';
const ajv = new Ajv({ allErrors: true });
const { validateRequirement, validateFeature } = registerSchemas(ajv);
const plan: SyncPlan = await diffSpecToDisk(fs, {
specDir: 'packages/my-package/src',
jsonDir: 'packages/my-package/spec',
validateRequirement,
validateFeature,
});
if (plan.violations.length > 0) {
// ajv validation failed somewhere
console.error(plan.violations);
process.exit(1);
}
if (options.dryRun) {
console.log(`Would change ${plan.entries.length} files`);
} else {
await applySyncPlan(fs, plan);
}import {
diffSpecToDisk,
applySyncPlan,
type SyncPlan,
} from '@frenchexdev/requirements-spec-io/bidi-sync-core';
import { registerSchemas } from '@frenchexdev/requirements-spec-io/schema-register';
import Ajv from 'ajv';
import { fs } from '@frenchexdev/requirements-core/ports';
const ajv = new Ajv({ allErrors: true });
const { validateRequirement, validateFeature } = registerSchemas(ajv);
const plan: SyncPlan = await diffSpecToDisk(fs, {
specDir: 'packages/my-package/src',
jsonDir: 'packages/my-package/spec',
validateRequirement,
validateFeature,
});
if (plan.violations.length > 0) {
// ajv validation failed somewhere
console.error(plan.violations);
process.exit(1);
}
if (options.dryRun) {
console.log(`Would change ${plan.entries.length} files`);
} else {
await applySyncPlan(fs, plan);
}The pattern is the terraform plan → preview → apply pattern that req-spec-is-source-of-truth (Part 03) mandates: compute the plan, render the preview, apply only the changes the user confirms. The package implements the plan and apply halves; the rendering is delegated to requirements-sync (Part 14) on top.
Why it is its own package
Two arguments.
First, the consumer surface for JSON round-trip is genuinely external. The CV site's blog pipeline reads spec JSON to generate traceability boxes. A future LSP extension reads spec JSON to drive editor autocompletion. A future CI plugin reads spec JSON to publish PR comments. None of those consumers want to depend on ts-morph or ts-json-schema-generator; they want JSON in, JSON out. Extracting spec-IO to its own package lets the LSP and the CI plugin depend on only the subpath they need — ./schema-register for the LSP, ./bidi-sync-core for the CI plugin — without pulling in the rest of the analyzer band.
Second, the ajv and ts-morph dependency surface is heavy enough to deserve isolation. Together they add roughly two megabytes to a transitive install — significant for a Tier-1 sibling. Keeping them in requirements-spec-io means consumers of requirements-scanner or requirements-trace skip them entirely. Pre-split, requirements-lib carried both libraries and every consumer paid for them whether they used spec-IO or not.
The package is also where the JSON Schema lives, physically. The schemas under packages/requirements-spec-io/schemas/ are checked into the workspace, regenerated by schema-generate, and shipped via the package's files array. External consumers — say, a VS Code extension that wants schema-aware IntelliSense on a project's requirements.json — can install only requirements-spec-io and import the schema directly from the package's published schemas/ directory.
The next page covers the smallest Tier-1 sibling — requirements-versioning — which content-hashes specs and detects drift over time.