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

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

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 validation
  • ts-morph ^24.0.0 — TypeScript AST manipulation
  • ts-json-schema-generator ^2.3.0 — devDep only, schema regeneration

Three things the package must not do:

  • Write source files. spec-to-source returns a string; the caller decides whether to write it. The package never writes; the requirements-sync use case (Part 14) wraps the writing.
  • Mutate the kernel registries. spec-from-source produces standalone RequirementSpec / FeatureSpec values; the registries are populated only by side-effect imports of the actual .ts source files.
  • Ship a default ajv instance. The caller passes one in. Different consumers may share an ajv across many schemas, or sandbox it, or replace ajv with 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);
}

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.

⬇ Download