Skip to content

Configure enforce-module-boundaries with port/adapter tags #694

Description

@ddeboer

Problem

@nx/enforce-module-boundaries is enabled in eslint.config.mjs, but depConstraints is still the generated wildcard and no package carries nx.tags:

depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }]

So we currently get entry-point enforcement (no deep imports into another package’s source) and cycle detection – both valuable – but every one of the 30 packages may depend on every other. The port/adapter structure that already exists in the codebase is a convention, not a constraint.

Proposed tags

Two axes, in each package’s package.json under nx.tags.

scope:dataset, pipeline, search, sparql, distribution, task, shared

type:

Tag Packages
type:model dataset
type:port task-runner, sparql-importer, sparql-server, distribution-downloader
type:domain pipeline, search, distribution-health, distribution-probe
type:adapter sparql-qlever, sparql-anything, task-runner-docker, task-runner-native, search-typesense, dataset-registry-client
type:app search-api-server, search-indexer
type:util text-normalization, fastify-rdf, docgen

Proposed constraints

type:model    → (nothing internal)
type:port     → type:model, type:port
type:domain   → type:model, type:port, type:domain, type:util
type:adapter  → anything
type:app      → anything

The load-bearing rule is type:domain must not depend on type:adapter: it keeps framework packages off concrete infrastructure. type:port → type:port/model keeps contracts light, which is what makes them substitutable.

Deliberately not included: “only type:app may depend on type:adapter”. Adapter-to-adapter composition is legitimate – see sparql-qlever below.

One violation

pipelinedataset-registry-client. In packages/pipeline/src/selector.ts, pipeline defines the DatasetSelector port and ships a concrete implementation holding a Client:

private readonly registry: Client;

Client and Paginator are value imports from @lde/dataset-registry-client, so the framework package carries a concrete registry dependency.

Fix, following the convention already established by pipeline-console-reporter, pipeline-void, pipeline-shacl-validator and pipeline-shacl-samplerpipeline defines the ports, pipeline-* packages implement them (ConsoleReporter implements ProgressReporter):

Extract the registry-backed DatasetSelector into a new package, e.g. pipeline-registry-selector, depending on @lde/pipeline and @lde/dataset-registry-client. pipeline keeps the interface and drops the dependency.

Not into dataset-registry-client itself: that would make a generic registry client depend on @lde/pipeline, pulling the framework into every consumer that only wants to query the registry.

Why extract, given there is only one implementation?

The port stays in pipeline – that is where it belongs, since pipeline is the client that declares what it needs. Only the implementation moves, and the reason is not the number of implementations but the dependencies each one drags in:

Package External dependencies
pipeline-console-reporter chalk, ora, log-symbols, pretty-ms
distribution-downloader filenamify-url
dataset-registry-client ldkit, @traqula/parser-sparql-1-1, @traqula/generator-sparql-1-1, @traqula/rules-sparql-1-1

The rule this reflects, and which the repo already follows: an implementation lives with its interface, unless it forces that package to carry dependencies it otherwise would not.

  • LastModifiedDownloader stays alongside the Downloader interface – filenamify-url costs nothing, and splitting would produce two shallow packages.
  • ConsoleReporter is already split, because co-locating it would force every pipeline consumer, including a headless server, to install terminal formatting libraries.
  • The registry-backed DatasetSelector is the heaviest of the three: it pulls ldkit plus a full SPARQL parser and generator stack into everyone who imports the framework.

With this resolved the tag scheme is green across all 30 packages, with no allow entries.

Checked and deliberately not flagged

  • pipelinesparql-importer / sparql-server – both are ports, not adapters. sparql-importer/src/index.ts exports interface Importer plus result types; the concrete importer is sparql-qlever. Depending on these is the correct direction.
  • sparql-qlevertask-runner-docker and task-runner-nativecreateQlever.ts is a deliberate factory. It takes mode: 'docker' | 'native' and hides both runner classes from callers, enforcing the invariant that importer and server share one runner. Widening the interface to let callers wire runners themselves would be worse.
  • distribution-downloader exports both the Downloader interface and LastModifiedDownloader. Cohesive; splitting it to satisfy the type:port rule would produce two shallow packages. Tagged type:port as-is.

Steps

  1. Add nx.tags to every package’s package.json.
  2. Replace the wildcard depConstraints with the rules above.
  3. Extract pipeline-registry-selector; drop dataset-registry-client from pipeline.
  4. Confirm nx run-many -t lint is green.

Note

packages/sparql-anything/package.json yields no name when read – worth checking, since it will be invisible to the tag scheme either way.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions