Part 01: The Monolithic Barrel
Before the mega-split, the whole @frenchexdev/requirements-* family fit into two packages: the kernel — then called requirements-core — and a single barrel called requirements-lib. The barrel's stated job in its package.json description was modest enough: "Analysis + specs + schemas + command cores — port-driven, no Commander/clack. Consumable by CLIs, LSPs, and IDE extensions." That description was honest about the consumers; it was misleading about what was inside.
What "lib" actually contained
Inside packages/requirements-lib/src/, six unrelated concerns lived as siblings.
The AST analyzers — test-bindings-scanner.ts, test-smells.ts, compliance-core.ts, compliance-report.ts, behavioral-check.ts — were stateless functions over the domain: walk a TypeScript AST, extract decorator metadata, produce a manifest. They depend on ts-morph and nothing else.
The application cores — scaffold-core.ts, sync-core.ts, bidi-sync-core.ts, feature-new-core.ts, requirement-core.ts, explore-core.ts — orchestrated use cases. They composed analyzers, ran interactive prompts through abstract PromptPort interfaces, and decided when to write to disk. They depend on diff, picocolors, and a registered FileSystem port.
The spec serialisers — spec-from-source.ts, spec-to-source.ts, schema-generate.ts, schema-register.ts — owned the TypeScript ↔ JSON round-trip and the ajv schema validation. They depend on ts-morph and ajv.
The trace engine — trace-core.ts, trace-graph.ts — turned scanner output into a navigable REQ → FEAT → AC → TEST graph with Mermaid and DOT renderers. It depends on the scanner.
The versioning ledger — versioning.ts — content-hashed specs with node:crypto and tracked drift. It depends on nothing.
The utility layer — audit-hooks.ts, toggling.ts, ac-suggester.ts, rename-core.ts, watch-core.ts — was a grab-bag of cross-cutting helpers consumed by everything above.
Each concern was port-driven, each had its own tests, and each was lint-clean in isolation. None of that helped: they all lived in the same npm package, so they all shared a version, a tsconfig.json, a coverage threshold, and a publish boundary. A consumer who needed only the scanner imported ajv and picocolors transitively because the barrel said so. The barrel's exports map had grown to fourteen subpaths trying to compensate, including five under ./analysis/* and three under ./schemas/*.json — a tell that the maintainer already knew the package wanted to be more than one.
The cyclic-deps WARN
The problem surfaced as a pnpm install warning:
WARN There are cyclic workspace dependencies:
packages/build-pipeline
packages/requirements
packages/requirements-lib
packages/ts-codegen-pipelineWARN There are cyclic workspace dependencies:
packages/build-pipeline
packages/requirements
packages/requirements-lib
packages/ts-codegen-pipelineTwo cheap cleanups removed the runtime portion of the cycle — an unused @frenchexdev/build-pipeline devDep got dropped from ts-codegen-pipeline, and @frenchexdev/ts-codegen-pipeline got demoted from dependencies to devDependencies in requirements-lib (it was only used in a sourcegen.config.ts). After those, the runtime dep graph was acyclic. pnpm's remaining WARN concerned a devDep-only loop requirements-lib --devDep→ ts-codegen-pipeline --dep→ requirements-lib — benign at runtime, but symptomatic of something deeper.
The deeper problem was that requirements (the CLI) was not a thin adapter. It shipped its own duplicate Requirement<S> base class in packages/requirements/src/base.ts, its own duplicate req-discoverable-traceability.ts, and even a two-line shim labelled "moved during Ship 2" in packages/requirements/src/analysis/test-bindings-scanner.ts — evidence of an unfinished prior migration. The CLI is supposed to be a presentation adapter; it was holding domain types. Meanwhile, requirements-core was misnamed. Its description claimed "Kernel — base classes, decorators, RequirementStyle abstraction, port types. Zero runtime dependencies." That is a Shared Kernel in DDD terminology, not a domain core. The naming was actively confusing future contributors. The cyclic-deps WARN was the visible artefact of those missing boundaries. Fixing the cycle without fixing the layering meant the WARN would come back the next time anything in the family grew.
Why a barrel becomes a smell
A barrel package is a fine starting point: when two or three concerns share the same domain, the same dependencies, and the same release cadence, the cost of one extra package.json per concern outweighs the clarity. The smell appears at the fourth concern — or earlier if the concerns have non-overlapping dependency surfaces.
By the time requirements-lib was carrying six concerns, the symptoms were textbook. The Single Responsibility Principle was violated at the package boundary: package.json could not honestly answer the question what is this for? The Dependency-Inversion Principle was violated transitively: a consumer of the scanner depended on picocolors because the sync engine, which the consumer never touched, was in the same barrel. The Open-Closed Principle was met only by accident: the registry-based extension points (Style, Scaffolder) worked, but the barrel itself was modified every time any of the six concerns evolved. Releases conflated unrelated changes, coverage thresholds had to satisfy the lowest common denominator, and the test matrix grew super-linearly with the number of concerns sharing the same vitest.config.ts.
The fix was not to add another barrel. The fix was to turn each concern into its own package, with explicit tier discipline and an acyclic-by-construction dependency graph. That is the mega-split this series documents. The next page lays out the target architecture — four hexagonal tiers plus two cross-cutting packages — and the DDD reasoning that makes the layering more than a refactor.