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-versioning is the audit trail of the spec. Every Requirement and every Feature has a content — the bag of typed fields the class declares — and the content can change over time. A priority upgraded from Medium to Critical, a new fitCriteria entry added, a rationale.claim rewritten — each of these is a semantic change the spec needs to record. The package computes content hashes, pins version numbers, detects drift between a recorded version and the current source, and keeps a history of every observed hash.

The package.json description: "Spec versioning utilities — content-hashing, version pins, drift detection, history bookkeeping. Pure (node:crypto only)."

The phrase "node:crypto only" is the defining constraint. The package has no runtime dependency outside the kernel and the vocabulary. No semver, no git, no sha-js, no crypto-js — only the Node.js built-in crypto module. The promise is that anywhere Node runs, the versioning library runs.

The public surface

src/index.ts is one line:

// @frenchexdev/requirements-versioning — Tier-1: Spec versioning utilities.
export * from './versioning';

The module exports four function families plus a VersionedSpec envelope type. The hashing layer:

import type { Requirement, Feature } from '@frenchexdev/requirements-shared-kernel';

export declare function hashRequirement(r: Requirement): string;
export declare function hashFeature(f: Feature): string;
export declare function hashSpecBundle(items: ReadonlyArray<Requirement | Feature>): string;

hashRequirement and hashFeature produce a canonical SHA-256 hex of the abstract-field values. Canonical means: field order is fixed, array order is preserved, optional fields are omitted from the input when absent, and the input is JSON-stringified with a deterministic serialiser. The same spec, hashed twice, produces the same hex; two specs that differ by a single character in any field produce different hex.

The pinning layer:

export interface VersionPin {
  id: string;
  version: string;
  hash: string;
  recordedAt: string;
}

export declare function pinVersion(
  spec: Requirement | Feature,
  version: string,
  clock?: () => Date,
): VersionPin;

pinVersion records the (id, version, hash, recordedAt) tuple. The clock parameter is a port for the wall clock — tests pass a deterministic fake, production passes the default () => new Date(). Pure-by-default, port-overridable.

The drift layer:

export type DriftStatus = 'unchanged' | 'modified' | 'missing' | 'unrecorded';

export interface DriftReport {
  id: string;
  status: DriftStatus;
  expectedHash?: string;
  actualHash?: string;
}

export declare function detectDrift(
  pinned: ReadonlyArray<VersionPin>,
  current: ReadonlyArray<Requirement | Feature>,
): DriftReport[];

detectDrift joins pinned versions with current specs and classifies each ID: unchanged (pinned hash equals current hash), modified (both exist but hashes differ), missing (pinned but no current spec), or unrecorded (current but no pin).

The history layer:

export interface HistoryEntry {
  id: string;
  hash: string;
  observedAt: string;
}

export declare function appendHistory(
  existing: ReadonlyArray<HistoryEntry>,
  spec: Requirement | Feature,
  clock?: () => Date,
): HistoryEntry[];

appendHistory is the bookkeeper. Each call appends an entry; consecutive identical hashes still produce one entry per call so the timestamp record is preserved.

Where it sits

Tier 1, smallest sibling. Depends on requirements-requirements (for the spec instances it hashes) and on node:crypto (built-in). Does not depend on the scanner, on compliance, or on spec-IO. Consumed by requirements-wizards (Part 15) — every wizard run pins the produced spec — and by the CV site's build pipeline, which uses drift detection to flag spec changes in PR previews.

Three things the package must not do:

  • Read or write storage. The pin/history entries are typed values; the caller decides where to store them (a versions.json file, an SQLite database, a remote API). The package never touches fs.
  • Enforce a version-numbering scheme. pinVersion accepts any string for version; semver is one convention, calendar versioning is another, monotonic-integer is a third. The package is policy-neutral.
  • Compare hashes across hashing implementations. The hash format is documented (SHA-256 hex of canonical JSON), and the canonical JSON serialiser is documented. If a future major version changes the serialiser, the change is a breaking version bump — no silent re-hashing.

A concrete call-site

The CV site's build pipeline uses drift detection to decide which Requirements to flag in a PR comment:

import {
  detectDrift,
  hashRequirement,
  type VersionPin,
  type DriftReport,
} from '@frenchexdev/requirements-versioning';
import { getRequirementRegistry } from '@frenchexdev/requirements-shared-kernel';
import '@frenchexdev/requirements-requirements';

const pinned: VersionPin[] = JSON.parse(
  await fs.readFile('packages/requirements-requirements/versions.json', 'utf8'),
);

const current = Array.from(getRequirementRegistry().values());
const drift: DriftReport[] = detectDrift(pinned, current);

const breakingChanges = drift.filter(d => d.status === 'modified');
if (breakingChanges.length > 0) {
  await renderPrComment(breakingChanges);
}

The pin file is checked into the workspace, refreshed whenever a maintainer runs requirements pin --all, and read by the build pipeline on every PR.

Why it is its own package

Three arguments.

First, the package's purity makes it the easiest to reuse. A consumer that wants to know whether their copy of a Requirement matches the canonical one only needs requirements-versioning and the spec value; they do not need a scanner, a compliance reporter, or a trace renderer. Future external consumers — a static-site documentation generator that wants drift badges, a Notion plugin that wants change notifications, a CI bot that wants to post a Slack message on spec drift — depend on this package alone. Bundling versioning into requirements-lib would have meant every drift-aware consumer paid for the full analyzer surface.

Second, the node:crypto boundary is the package's contract. The promise "no external crypto library, no dependency drift, deterministic across Node versions" is what gives downstream consumers confidence to commit pin files into long-lived repositories. The same hash function computes the same digest in five years that it does today, because node:crypto's SHA-256 implementation has been stable since Node 0.10. Keeping the package separate makes that promise auditable: anyone reading the package.json sees zero runtime dependencies and knows the contract is preserved by the npm registry's immutability.

Third, version pinning is the natural home for the req-versioned-traceability requirement declared in Part 05. The Requirement says: "every Requirement and Feature must have a recorded version history; drift must be detectable; the history must be append-only." The package realises the Requirement directly — every exported function corresponds to one of the Requirement's fitCriteria entries. The mapping from spec to code is one-to-one, which is the cleanest form of dog-fooding.

That ends Tier 1. Seven packages, each a single stateless analyzer over the registries and the manifest, each acyclic, each independently consumable. The next three pages cover Tier 2 — the application services that orchestrate the analyzers into user-facing workflows.

⬇ Download