TypeScript module graph — enforce direct architecture boundaries#

Use typescript-module-graph when the rule is about ordinary TypeScript or JavaScript modules, not a framework-specific route or plugin shape. Every scanned module becomes a typescriptModule component. Every statically named dependency becomes an imports edge before any policy is applied.

This profile is available in the v0.3.1 registry release. Install the exact version and run its packaged proof:

npm view bce-engine@0.3.1 version dist.integrity
npm install --save-dev --save-exact bce-engine@0.3.1
npx --no-install bce demo --recipe module-layering

The clean tree imports from application code into domain code and scores 100. The drift tree adds one reverse domain-to-application import; C3 names that edge and its source line.

Fastest agent path: use the existing MCP done-check#

This profile does not need a module-specific MCP server or another agent integration. Point the existing bce-mcp stdio server at the target repository, then let the installed BCE skill use its normal read-only loop:

doctor_repository {}
run_gate {}

run_gate {} reads the live working tree, including uncommitted fixes. Its structured report names the same constraint, importer, target, and source line as the CLI; after the source correction, the same zero-argument call must return gateFailed: false and outcome: "pass". MCP cannot author, approve, weaken, or land policy.

Launch the installed bce-mcp binary with the target repository as its working directory. Normal bce onboard installs the same Agent Skill, project-local MCP wiring, advisory mode, and immutable CI in one operation. See agent-loop.md for client wiring and onboarding.md for the generated setup path.

Directional layering#

The portable first rule keeps packages/domain/** below packages/app/**. This is the core fragment from the complete executable fixture:

{
  "minEngineVersion": "0.3.0",
  "constraints": [
    {
      "id": "domain-cannot-import-app",
      "type": "forbiddenDependency",
      "severity": "critical",
      "to": "module:packages/app/**",
      "scopePaths": ["packages/domain/**"]
    }
  ],
  "extraction": {
    "profile": "typescript-module-graph",
    "paths": ["packages/**/*.ts"],
    "minFiles": 2
  }
}

scopePaths always selects importer modules. to selects the dependency target. The profile requires both fields on C2 and C3 so a stale or accidentally global boundary cannot look green. It also requires minEngineVersion >=0.3.0; an older gate can then say “upgrade” before it tries to parse newer vocabulary.

Run the measured author → RED → fix → GREEN module-layering walkthrough to create this boundary against a real two-layer tree. The example deliberately includes an unrelated application module that does not import domain: a directional boundary should not force every file in a higher layer to depend on the lower one.

Required edges are narrower#

C2 means every importer in its scope must carry the named direct edge. Use it only when that is the actual invariant—for example, one adapter entry point that must delegate to domain:

{
  "id": "checkout-entry-uses-domain",
  "type": "requiredDependency",
  "severity": "high",
  "component": "typescriptModule",
  "to": "module:packages/domain/**",
  "scopePaths": ["packages/app/checkout.ts"]
}

Target selectors#

SelectorMatchesExample
module:<repo-path-or-glob>a resolved file inside the repositorymodule:src/server/**
package:<npm-root>one normalized package root, including all subpath importspackage:@aws-sdk/client-s3
builtin:<node-name>one Node built-in, with node: normalized awaybuiltin:fs/promises

These selectors support three high-return boundaries:

Resolution#

Without extraction.tsconfig, relative imports resolve lexically with a fixed extension and index.* order. JavaScript specifiers such as ./order.js resolve to authored TypeScript files such as order.ts. A bare dependency becomes package:<root> only when an enclosing package.json declares that root; otherwise BCE records the specifier as unresolved because it may be a project alias. node: imports (including mandatory-prefix modules) and the public Node 22 unprefixed built-ins normalize to builtin: targets from a source-pinned vocabulary, so Node 22 and Node 24 produce the same graph.

Set extraction.tsconfig to one repository-relative config file when the repository uses baseUrl, paths, or extended TypeScript configuration:

{
  "extraction": {
    "profile": "typescript-module-graph",
    "paths": ["src/**/*.ts", "src/**/*.tsx"],
    "minFiles": 12,
    "tsconfig": "tsconfig.json"
  }
}

The BCE glob syntax does not support brace expansion, so each extension is a separate entry. Extended configs must be repository-owned relative files. BCE refuses configs outside the repository or under node_modules so module resolution cannot change independently of the gated revision.

What is observed#

The AST provider records static imports, type-only imports, re-exports, JavaScript JSDoc @import, TypeScript import = require() and import("...") types, unshadowed literal require() and require.resolve(), and literal dynamic imports. Relative targets may resolve to repository modules outside the scanned component set; that preserves the observed edge while scopePaths controls which importers are governed.

Computed imports, undeclared bare specifiers, and unresolved aliases are located under coverage.unresolvedImports. A C2 or C3 boundary whose importer scope includes one of those facts fails closed: an unknown target is not evidence that the required edge exists or the forbidden edge does not. Invalid source syntax, invalid or escaping tsconfig resolution, imports escaping the repository, and line-scan requests are refusals.

Honest limits#

This is a direct-import graph. It does not claim transitive reachability, cycle detection, runtime-only loading, bundler plugin resolution, ownership inference, package-tag inference, or automatic workspace expansion. Glob the files you intend to scan, set a real minFiles floor, and use a tsconfig when aliases matter.

Next: choose another executable boundary, read C1–C4, or install the gate in advisory mode.

Read Markdown · Source: docs/typescript-module-graph.md