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 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 analyzerstest-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 coresscaffold-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 serialisersspec-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 enginetrace-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 ledgerversioning.ts — content-hashed specs with node:crypto and tracked drift. It depends on nothing.

The utility layeraudit-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-pipeline

Two 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.

⬇ Download