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)"
}{
"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 hastypes/import/defaultin that order.peerDependencies: typescript >=5.4anddevDependencies.typescript: "catalog:"— peer + dev, neverdependencies.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-requirementsdepends onrequirements-shared-kerneland nothing else.requirements-shared-kerneldepends 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 oncommander,@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
commanderand@clack/prompts. - Cross-cutting (styles, styles-demo). Depend only on Tier 0 (plus, for styles-demo, on
requirements-libuntil 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';// @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:
- Add one or more
req-*.tsfiles topackages/requirements-requirements/src/describing the capability the new package realises. Use the same shape as the existingreq-*.tsfiles (Part 03). Each file exports one abstract class extendingRequirement. 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.' };
}// 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.' };
}Add a corresponding Feature file to
packages/requirements-requirements/src/feature-*.ts(orcoverage-trend.tsto match the capability name). The Feature extendsFeature, 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.Re-export both files from
packages/requirements-requirements/src/index.ts, in the appropriate section (Requirements first, Features second).Implement the Feature's Acceptance Criteria in the new package's
src/<capability>.tsby 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.Write tests under the new package's
test/using@FeatureTestand@Verifiesdecorators only — neverdescribe/it. Thereq-dog-foodRequirement forbids the alternative; the strict compliance gate enforces it.Run
npx requirements compliance --strictin the new package. The strict mode fails if the new package's Feature has no@Satisfieslink, if any AC has no@Verifiestest binding, ifdescribe/itappears 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 --strictnpx 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 --strictFive expectations:
installproduces no cyclic-deps WARN (the new package's dep graph is acyclic).buildsucceeds — TypeScript can find every import.testreports 100% coverage onsrc/**per the per-file threshold the package'svitest.config.tsenforces.compliance --strictpasses — every Feature has a@Satisfieslink, every AC has a@Verifiesbinding, nodescribe/it.- The
bindings.jsonartefact 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.