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

Part 19: How to Add a Tier-N Package

The mega-split is finished as a layout, but the family is still growing. The roadmap names requirements-explore (TUI trace explorer) as a planned Tier-2 sibling that does not yet exist. Future Tier-1 packages — a coverage-trend analyzer, a complexity scanner, a fuzzing harness — are pencilled. This page is the cookbook for adding one. The shape is identical regardless of tier; the differences are which siblings the new package may depend on and which src/ directory hosts its dog-fooded Requirements.

The naming convention

The npm name is @frenchexdev/requirements-<noun>, where <noun> is the capability in a single word or kebab-case phrase. "Requirements" is the bounded-context prefix; "-noun" describes the capability. Existing names: -scanner, -compliance, -test-smells, -behavioral-check, -trace, -spec-io, -versioning, -scaffolders, -sync, -wizards, -styles, -styles-demo. The pattern is consistent.

The workspace directory is packages/requirements-<noun>/. No nesting, no group folders — every package sits at the workspace root next to its siblings. The directory holds src/, test/, package.json, tsconfig.json, and optionally vitest.config.ts if the package has tests.

The package.json shape

The reference template, distilled from the existing siblings (the example here is for a hypothetical Tier-1 requirements-coverage-trend package):

{
  "name": "@frenchexdev/requirements-coverage-trend",
  "version": "0.0.1",
  "description": "Coverage-trend analyzer — tracks per-AC coverage over time and emits drift reports. Tier-1 analyzer in the @frenchexdev/requirements-* hexagonal architecture.",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "default": "./dist/index.js"
    },
    "./coverage-trend": {
      "types": "./dist/coverage-trend.d.ts",
      "import": "./dist/coverage-trend.js",
      "default": "./dist/coverage-trend.js"
    }
  },
  "dependencies": {
    "@frenchexdev/requirements-core": "workspace:*",
    "@frenchexdev/requirements-requirements": "workspace:*",
    "@frenchexdev/requirements-scanner": "workspace:*",
    "@frenchexdev/requirements-compliance": "workspace:*"
  },
  "peerDependencies": {
    "typescript": ">=5.4"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vitest": "catalog:",
    "@vitest/coverage-v8": "catalog:"
  },
  "scripts": {
    "build": "tsc",
    "test": "vitest run --coverage",
    "test:watch": "vitest",
    "prepublishOnly": "npm run build",
    "req:compliance": "requirements compliance --strict",
    "req": "requirements"
  },
  "files": ["dist/**/*.js", "dist/**/*.d.ts", "dist/**/*.d.ts.map"],
  "license": "SEE LICENSE IN LICENSE",
  "author": "Stéphane Erard <stephane.erard@gmail.com> (https://serard.dev)"
}

Six conventions matter:

  • "type": "module" — ESM-only, no CommonJS shim.
  • "exports" with at least the root and one named capability subpath. Each entry has types / import / default in that order.
  • peerDependencies: typescript >=5.4 and devDependencies.typescript: "catalog:" — peer + dev, never dependencies.
  • scripts.test = "vitest run --coverage" — coverage is mandatory (feedback_vitest_always_coverage.md).
  • scripts.req:compliance = "requirements compliance --strict" — the dog-fooding gate.
  • files: ["dist/**/*.js", "dist/**/*.d.ts", "dist/**/*.d.ts.map"] — exactly this, never more.

Allowed dependencies, per tier

The acyclic graph is enforced socially today, by code review against the package's tier classification in the description field. A future workspace lint rule will encode it mechanically. Until then, the rules:

  • Tier 0 (shared-kernel, requirements-requirements). Zero non-workspace dependencies. requirements-requirements depends on requirements-shared-kernel and nothing else. requirements-shared-kernel depends on nothing.
  • Tier 1 (analyzers). May depend on Tier 0 plus other Tier-1 packages where strictly necessary. May depend on third-party AST / serialisation / crypto libraries (typescript, ts-morph, ajv, node:crypto, @stryker-mutator/*).
  • Tier 2 (use cases). May depend on Tier 0 + Tier 1 + other Tier-2 packages. May depend on third-party libraries that are surface-agnostic (diff, picocolors). Must NOT depend on commander, @clack/prompts, or any other CLI library — Tier 2 is port-driven.
  • Tier 3 (CLI). May depend on everything below. The only legitimate place for commander and @clack/prompts.
  • Cross-cutting (styles, styles-demo). Depend only on Tier 0 (plus, for styles-demo, on requirements-lib until Phase 6 sunsets that barrel).

The description field encodes the tier. The pattern: "<capability>. Tier-N <role> in the @frenchexdev/requirements-* hexagonal architecture.". Code review checks the description against the dependency list.

The src/ skeleton

Two files at minimum: src/index.ts (re-exports) and src/<capability>.ts (the implementation).

src/index.ts:

// @frenchexdev/requirements-coverage-trend — Tier-1 analyzer:
// tracks per-AC coverage over time and emits drift reports.
export * from './coverage-trend';

src/coverage-trend.ts exports the typed surface. Keep it under 500 lines; if it grows further, split it into multiple files exported through the same index.ts. The single capability file pattern is universal: every Tier-1 analyzer follows it, every Tier-2 use case follows it.

The dog-fooding step — the keystone

The new package does not exist, from the family's point of view, until its requirements are written. The convention is:

  1. Add one or more req-*.ts files to packages/requirements-requirements/src/ describing the capability the new package realises. Use the same shape as the existing req-*.ts files (Part 03). Each file exports one abstract class extending Requirement. The example for a coverage-trend analyzer:
// packages/requirements-requirements/src/req-coverage-trend.ts
import { Requirement, Priority } from '@frenchexdev/requirements-shared-kernel';

export abstract class ReqCoverageTrendRequirement extends Requirement {
  readonly id = 'REQ-COVERAGE-TREND';
  readonly title = 'Per-AC coverage must be tracked over time with drift detection';
  readonly priority = Priority.High;
  readonly status = 'Approved' as const;
  readonly kind = 'NonFunctional' as const;

  readonly statement = {
    pattern: 'ubiquitous' as const,
    response: 'persist a per-AC coverage hash on every compliance run; surface drift > 10% as a strict-mode failure.',
  };

  readonly rationale = {
    claim: 'Coverage erosion goes unnoticed without per-AC time-series tracking; trend reports catch decay before it ships.',
    kind: 'principle' as const,
    evidence: [
      { kind: 'precedent' as const, requirement: 'codecov/coverage-badges',
        rationale: 'Industry-standard pattern: coverage as a versioned artifact, not a per-run number.' },
    ],
  };

  readonly fitCriteria = [
    { kind: 'unit-test' as const,
      describes: 'computeCoverageTrend emits a CoverageTrendReport with one entry per AC',
      binds: ['computeCoverageTrendEmitsPerAcReport'] },
    { kind: 'coverage-threshold' as const, metric: 'line' as const, min: 98, scope: 'src/coverage-trend.ts' },
  ];

  readonly verificationMethod = 'Test' as const;
  readonly source = { type: 'stakeholder' as const, role: 'observability', date: '2026-05-22' };
  readonly risk = { level: 'High' as const, ifNotMet: 'Coverage drifts silently between releases; regression escapes to production.' };
}
  1. Add a corresponding Feature file to packages/requirements-requirements/src/feature-*.ts (or coverage-trend.ts to match the capability name). The Feature extends Feature, declares the Acceptance Criteria as abstract methods, and links to one or more Requirements via @Satisfies. The shape (Part 04 shows a verbatim example) is universal.

  2. Re-export both files from packages/requirements-requirements/src/index.ts, in the appropriate section (Requirements first, Features second).

  3. Implement the Feature's Acceptance Criteria in the new package's src/<capability>.ts by extending the abstract Feature class with concrete method bodies. Each method body is the AC's verification logic — but in a Tier-1 / Tier-2 package, the bodies usually delegate to pure functions that the test files exercise via @FeatureTest + @Verifies.

  4. Write tests under the new package's test/ using @FeatureTest and @Verifies decorators only — never describe/it. The req-dog-food Requirement forbids the alternative; the strict compliance gate enforces it.

  5. Run npx requirements compliance --strict in the new package. The strict mode fails if the new package's Feature has no @Satisfies link, if any AC has no @Verifies test binding, if describe/it appears in any test file, or if any coverage threshold is missed.

The keystone — and the reason the dog-fooding step is non-negotiable — is that without req-*.ts files in requirements-requirements/src/, the new package has no audited reason to exist. The package preaches a DSL that demands every Feature link to a Requirement; if the package itself ships Features without links, it violates its own DSL and the strict gate refuses to let it publish. The keystone makes the rule self-enforcing.

The verification gate before merging

Before opening the PR that adds the new package, run locally:

npx pnpm install
npx pnpm --filter @frenchexdev/requirements-coverage-trend build
npx pnpm --filter @frenchexdev/requirements-coverage-trend test
cd packages/requirements-coverage-trend && npx requirements compliance --strict

Five expectations:

  • install produces no cyclic-deps WARN (the new package's dep graph is acyclic).
  • build succeeds — TypeScript can find every import.
  • test reports 100% coverage on src/** per the per-file threshold the package's vitest.config.ts enforces.
  • compliance --strict passes — every Feature has a @Satisfies link, every AC has a @Verifies binding, no describe/it.
  • The bindings.json artefact regenerated by compliance is checked into the PR — the trace graph stays consistent.

If any of the five fails, the PR is not ready. Each gate corresponds to one of the universal Requirements in requirements-requirements/src/ (Part 05): req-dog-food, req-dsl-complete, req-discoverable-traceability. The package is dog-fooding its own DSL from day one; the gates are the dog-fooding made physical.

The next page covers where the family is going next — Phases 4, 5, and 6 of the roadmap — and what changes the reader of this catalogue should expect to see over the coming months.

⬇ Download