@comity-dev/dependency-rules 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Filippo Bovo and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @comity-dev/dependency-rules
2
+
3
+ ## Purpose
4
+
5
+ Canonical dependency-cruiser rule generator for Comity layering. This package produces the JSON configuration consumed by the validator; it does **not** invoke dependency-cruiser itself.
6
+
7
+ ## Scope
8
+
9
+ - Builds a deterministic `dependency-cruiser` config from repository-supplied facts (a `Map<packageName, layer>`).
10
+ - Encodes the canonical layering model:
11
+ - `primitives` MUST NOT depend on any internal package.
12
+ - `kernel` MAY only depend on `primitives`.
13
+ - `composition` MAY only depend on `kernel` and `primitives`.
14
+ - `core` MAY depend on `primitives`, `kernel`, and other `core` modules (gated by ADR-008).
15
+ - Adapters: cross-adapter edges forbidden except Integration Adapter → Technology Adapter (ADR-007).
16
+ - Primitives/Kernel/Composition MUST NOT depend on Core.
17
+
18
+ ## Ownership
19
+
20
+ Owned by `comity-development`. Consumed by `@comity-dev/validate` which actually invokes dependency-cruiser.
21
+
22
+ ## Public API
23
+
24
+ ```ts
25
+ import {
26
+ buildDependencyRules,
27
+ buildDependencyRulesForRoot,
28
+ classifyByLayer,
29
+ layerSetsFromPackages,
30
+ LAYER_PROFILES,
31
+ DEFAULT_REPOSITORY_FACTS,
32
+ } from "@comity-dev/dependency-rules";
33
+ ```
34
+
35
+ A dedicated subpath exposes the rule builder:
36
+
37
+ ```ts
38
+ import { buildDependencyRules } from "@comity-dev/dependency-rules/build-config";
39
+ ```
40
+
41
+ ## Relationship to Comity Standards
42
+
43
+ - `layering-policy.md` §2 — canonical layering model.
44
+ - `ADR-007` — adapter categories.
45
+ - `ADR-008` — Core-to-Core explicit exception register.
46
+ - `architecture-validation.md` §7 — dependency validation.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ pnpm build
52
+ pnpm test
53
+ ```
@@ -0,0 +1,63 @@
1
+ import type { LayerName } from "./constants.js";
2
+ export interface DependencyRule {
3
+ /** The name of the dependency rule */
4
+ name: string;
5
+ /** The severity of the dependency rule */
6
+ severity: "error" | "warn" | "info";
7
+ /** The comment associated with the dependency rule */
8
+ comment: string;
9
+ /** The source module of the dependency */
10
+ from: {
11
+ path: string;
12
+ };
13
+ /** The target module of the dependency */
14
+ to: {
15
+ path: string;
16
+ };
17
+ }
18
+ export interface DependencyCruiserConfig {
19
+ /** The list of forbidden dependency rules */
20
+ forbidden: DependencyRule[];
21
+ /** The options for the dependency-cruiser configuration */
22
+ options: {
23
+ /** The prefix for module paths */
24
+ prefix?: string;
25
+ /** The options for the dependency-cruiser configuration */
26
+ doNotFollow?: {
27
+ /** The path pattern for modules that should not be followed */
28
+ path?: string;
29
+ /** The dependency types that should not be followed */
30
+ dependencyTypes?: string[];
31
+ };
32
+ /** The TypeScript configuration file */
33
+ tsConfig?: {
34
+ fileName?: string;
35
+ };
36
+ };
37
+ }
38
+ export interface BuildOptions {
39
+ /** The path to the TypeScript configuration file */
40
+ tsConfigFileName?: string;
41
+ /** Additional dependency rules to be applied */
42
+ extraRules?: Array<{
43
+ /** The name of the extra rule */
44
+ name?: string;
45
+ /** The source layer of the extra rule */
46
+ fromLayer: LayerName;
47
+ /** The target layer of the extra rule */
48
+ toLayer: LayerName;
49
+ /** The severity of the extra rule */
50
+ severity?: "error" | "warn" | "info";
51
+ /** The comment associated with the extra rule */
52
+ comment?: string;
53
+ }>;
54
+ /** Explicitly registered Core-to-Core edges (ADR-008) */
55
+ registeredCoreEdges?: string[];
56
+ }
57
+ /**
58
+ * @param classification — Map<packageName, layer>
59
+ * @param options — repository-supplied facts
60
+ * @returns a dependency-cruiser config object
61
+ */
62
+ export declare function buildDependencyRules(classification: Map<string, string>, options?: BuildOptions): DependencyCruiserConfig;
63
+ export declare function buildDependencyRulesForRoot(classification: Map<string, string>, repoRoot: string, options?: Omit<BuildOptions, "tsConfigFileName">): DependencyCruiserConfig;
@@ -0,0 +1,162 @@
1
+ import { resolve } from "node:path";
2
+ import { layerSetsFromPackages } from "./classify.js";
3
+ import { DEFAULT_REPOSITORY_FACTS, LAYER_PROFILES } from "./constants.js";
4
+ const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+ /**
6
+ * @param classification — Map<packageName, layer>
7
+ * @param options — repository-supplied facts
8
+ * @returns a dependency-cruiser config object
9
+ */
10
+ export function buildDependencyRules(classification, options = {}) {
11
+ const { tsConfigFileName = DEFAULT_REPOSITORY_FACTS.tsConfigFileName, extraRules = [], registeredCoreEdges = [], } = options;
12
+ if (!(classification instanceof Map)) {
13
+ throw new TypeError("classification must be a Map<packageName, layer>");
14
+ }
15
+ const sets = layerSetsFromPackages(classification);
16
+ const primitives = [...sets.primitives];
17
+ const kernel = [...sets.kernel];
18
+ const composition = [...sets.composition];
19
+ const core = [...sets.core];
20
+ const techAdapter = [...sets["technology-adapter"]];
21
+ const integrationAdapter = [...sets["integration-adapter"]];
22
+ const adapters = [...techAdapter, ...integrationAdapter];
23
+ const forbidden = [];
24
+ // Build the forbidden rules based on the canonical layer profiles and the repository-supplied classification.
25
+ if (primitives.length > 0) {
26
+ forbidden.push({
27
+ name: "primitives-no-internal",
28
+ severity: "error",
29
+ comment: LAYER_PROFILES.primitives.comment,
30
+ from: { path: `^(${primitives.map(escapeForRegex).join("|")})$` },
31
+ to: { path: "^@comity/" },
32
+ });
33
+ }
34
+ // Kernel may depend on primitives, but nothing else.
35
+ if (kernel.length > 0) {
36
+ const allowedKernel = [...new Set([...primitives, ...kernel])];
37
+ forbidden.push({
38
+ name: "kernel-only-primitives",
39
+ severity: "error",
40
+ comment: LAYER_PROFILES.kernel.comment,
41
+ from: { path: `^(${kernel.map(escapeForRegex).join("|")})$` },
42
+ to: { path: `^(?!(${allowedKernel.map(escapeForRegex).join("|")})$).*$` },
43
+ });
44
+ }
45
+ // Composition may depend on kernel and primitives, but nothing else.
46
+ if (composition.length > 0) {
47
+ const allowedComposition = [
48
+ ...new Set([...primitives, ...kernel, ...composition]),
49
+ ];
50
+ forbidden.push({
51
+ name: "composition-only-kernel-primitives",
52
+ severity: "error",
53
+ comment: LAYER_PROFILES.composition.comment,
54
+ from: { path: `^(${composition.map(escapeForRegex).join("|")})$` },
55
+ to: {
56
+ path: `^(?!(${allowedComposition.map(escapeForRegex).join("|")})$).*$`,
57
+ },
58
+ });
59
+ }
60
+ // Core Modules may depend on primitives, kernel, and other Core Modules explicitly permitted via ADR-008.
61
+ if (core.length > 0) {
62
+ const allowedCorePeers = [...primitives, ...kernel, ...core];
63
+ forbidden.push({
64
+ name: "core-only-kernel-primitives-or-core-peer",
65
+ severity: "error",
66
+ comment: LAYER_PROFILES.core.comment,
67
+ from: { path: `^(${core.map(escapeForRegex).join("|")})$` },
68
+ to: {
69
+ path: `^(?!(${allowedCorePeers.map(escapeForRegex).join("|")})$).*$`,
70
+ },
71
+ });
72
+ }
73
+ // Technology Adapters may not depend on any other Adapters except Integration Adapter → Technology Adapter (ADR-007).
74
+ if (core.length > 0 && adapters.length > 0) {
75
+ forbidden.push({
76
+ name: "contracts-never-depend-on-adapters",
77
+ severity: "error",
78
+ comment: "Core Modules MUST NOT depend on Adapters (layering-policy.md §2.3, architecture-validation.md §7.3).",
79
+ from: { path: `^(${core.map(escapeForRegex).join("|")})$` },
80
+ to: { path: `^(${adapters.map(escapeForRegex).join("|")})$` },
81
+ });
82
+ }
83
+ // Adapter to Adapter dependencies are forbidden except Integration Adapter → Technology Adapter (ADR-007).
84
+ if (adapters.length > 0) {
85
+ forbidden.push({
86
+ name: "no-cross-adapter-deps",
87
+ severity: "error",
88
+ comment: "Adapter to Adapter dependencies are forbidden except Integration Adapter → Technology Adapter (ADR-007).",
89
+ from: { path: `^(${adapters.map(escapeForRegex).join("|")})$` },
90
+ to: { path: `^(${adapters.map(escapeForRegex).join("|")})$` },
91
+ });
92
+ }
93
+ // Primitives, Kernel, and Composition may not depend on Core Modules.
94
+ if ((primitives.length || kernel.length || composition.length) &&
95
+ core.length > 0) {
96
+ const inner = [...primitives, ...kernel, ...composition];
97
+ forbidden.push({
98
+ name: "kernel-no-core-deps",
99
+ severity: "error",
100
+ comment: "Primitives, Kernel, and Composition MUST NOT depend on Core Modules (layering-policy.md §2.1).",
101
+ from: { path: `^(${inner.map(escapeForRegex).join("|")})$` },
102
+ to: { path: `^(${core.map(escapeForRegex).join("|")})$` },
103
+ });
104
+ }
105
+ // Explicitly registered Core-to-Core edges (ADR-008) are allowed, so we need to add them to the forbidden list with a comment indicating that they are exceptions.
106
+ for (const edge of registeredCoreEdges) {
107
+ const [from, to] = edge.split(" -> ").map((s) => s.trim());
108
+ if (!from || !to)
109
+ continue;
110
+ forbidden.push({
111
+ name: `adr-008-edge-${from}-${to}`,
112
+ severity: "error",
113
+ comment: `ADR-008 explicit Core-to-Core exception: ${from} → ${to}.`,
114
+ from: { path: `^${escapeForRegex(from)}$` },
115
+ to: { path: `^${escapeForRegex(to)}$` },
116
+ });
117
+ }
118
+ // Apply any extra rules supplied by the repository. These are additional constraints that the repository wants to enforce, beyond the canonical layering policy.
119
+ for (const rule of extraRules) {
120
+ const fromSet = sets[rule.fromLayer];
121
+ const toSet = sets[rule.toLayer];
122
+ if (!fromSet || !toSet)
123
+ continue;
124
+ forbidden.push({
125
+ name: rule.name ?? `extra-${rule.fromLayer}-no-${rule.toLayer}`,
126
+ severity: rule.severity ?? "error",
127
+ comment: rule.comment ?? `Disallow ${rule.fromLayer} → ${rule.toLayer}`,
128
+ from: {
129
+ path: `^(${Array.from(fromSet).map(escapeForRegex).join("|")})$`,
130
+ },
131
+ to: {
132
+ path: `^(${Array.from(toSet).map(escapeForRegex).join("|")})$`,
133
+ },
134
+ });
135
+ }
136
+ return {
137
+ forbidden,
138
+ options: {
139
+ prefix: "^(@comity|@comity-dev)/",
140
+ doNotFollow: {
141
+ path: "node_modules",
142
+ dependencyTypes: [
143
+ "npm",
144
+ "npm-dev",
145
+ "npm-optional",
146
+ "npm-peer",
147
+ "npm-bundled",
148
+ "npm-no-pkg",
149
+ ],
150
+ },
151
+ },
152
+ };
153
+ }
154
+ // Re-export a helper that returns the config with an absolute tsconfig path
155
+ // pinned to a specific repo root. Consumers call this when the engine's CWD
156
+ // differs from the repo root.
157
+ export function buildDependencyRulesForRoot(classification, repoRoot, options = {}) {
158
+ return buildDependencyRules(classification, {
159
+ ...options,
160
+ tsConfigFileName: resolve(repoRoot, "tsconfig.json"),
161
+ });
162
+ }
@@ -0,0 +1,25 @@
1
+ import type { LayerSets } from "./constants.js";
2
+ /**
3
+ * Pure helpers to classify packages and build layer sets from a parsed
4
+ * classification map. No I/O.
5
+ */
6
+ export interface PackageRecord {
7
+ /** The name of the package */
8
+ name: string;
9
+ /** The layer of the package, if known */
10
+ layer?: string | null;
11
+ }
12
+ /**
13
+ * Returns a Map<name, layer> from an array of package records.
14
+ *
15
+ * @param packages — package records with optional layer
16
+ * @returns classification map
17
+ */
18
+ export declare function classifyByLayer(packages: PackageRecord[]): Map<string, string>;
19
+ /**
20
+ * Builds per-layer Sets from a classification map.
21
+ *
22
+ * @param classification — Map<packageName, layer>
23
+ * @returns layer sets
24
+ */
25
+ export declare function layerSetsFromPackages(classification: Map<string, string>): LayerSets;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Returns a Map<name, layer> from an array of package records.
3
+ *
4
+ * @param packages — package records with optional layer
5
+ * @returns classification map
6
+ */
7
+ export function classifyByLayer(packages) {
8
+ const out = new Map();
9
+ for (const pkg of packages) {
10
+ if (pkg.layer)
11
+ out.set(pkg.name, pkg.layer);
12
+ }
13
+ return out;
14
+ }
15
+ /**
16
+ * Builds per-layer Sets from a classification map.
17
+ *
18
+ * @param classification — Map<packageName, layer>
19
+ * @returns layer sets
20
+ */
21
+ export function layerSetsFromPackages(classification) {
22
+ const sets = {
23
+ primitives: new Set(),
24
+ kernel: new Set(),
25
+ composition: new Set(),
26
+ core: new Set(),
27
+ "technology-adapter": new Set(),
28
+ "integration-adapter": new Set(),
29
+ "dev-tooling": new Set(),
30
+ };
31
+ for (const [name, layer] of classification) {
32
+ if (layer === "primitives" ||
33
+ layer === "kernel" ||
34
+ layer === "composition" ||
35
+ layer === "core" ||
36
+ layer === "technology-adapter" ||
37
+ layer === "integration-adapter" ||
38
+ layer === "dev-tooling") {
39
+ sets[layer].add(name);
40
+ }
41
+ }
42
+ return sets;
43
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Default repository facts used by the dependency-cruiser rule builder
3
+ * when no overrides are supplied.
4
+ *
5
+ * Repositories MAY override these in their `comity.config.json` or via
6
+ * the `buildDependencyRules` options. They MUST NOT alter layering policy
7
+ * through configuration.
8
+ */
9
+ export declare const DEFAULT_REPOSITORY_FACTS: Readonly<{
10
+ packageRoots: string[];
11
+ tsConfigFileName: "tsconfig.json";
12
+ }>;
13
+ /**
14
+ * Canonical layer profile comments referenced by the dependency rule
15
+ * builder. Keeping the prose here makes rule generation deterministic and
16
+ * prevents duplication across repositories.
17
+ */
18
+ export declare const LAYER_PROFILES: Readonly<{
19
+ readonly primitives: {
20
+ readonly comment: "Primitives MUST NOT depend on any internal package (layering-policy.md §2.1).";
21
+ };
22
+ readonly kernel: {
23
+ readonly comment: "Kernel MUST only depend on primitives (layering-policy.md §2.1).";
24
+ };
25
+ readonly composition: {
26
+ readonly comment: "Composition MUST only depend on kernel and primitives (layering-policy.md §2.1).";
27
+ };
28
+ readonly core: {
29
+ readonly comment: "Core Modules MAY depend on primitives, kernel, and other Core Modules explicitly permitted via ADR-008.";
30
+ };
31
+ readonly "technology-adapter": {
32
+ readonly comment: "Technology Adapters MUST NOT depend on Adapters except Integration Adapter → Technology Adapter (ADR-007).";
33
+ };
34
+ readonly "integration-adapter": {
35
+ readonly comment: "Integration Adapters may depend on primitives, kernel, core, and technology adapters (ADR-007).";
36
+ };
37
+ }>;
38
+ export type LayerName = "primitives" | "kernel" | "composition" | "core" | "technology-adapter" | "integration-adapter" | "dev-tooling";
39
+ export interface LayerSets {
40
+ /** The set of package names in each layer */
41
+ primitives: Set<string>;
42
+ /** The set of package names in each layer */
43
+ kernel: Set<string>;
44
+ /** The set of package names in each layer */
45
+ composition: Set<string>;
46
+ /** The set of package names in each layer */
47
+ core: Set<string>;
48
+ /** The set of package names in each layer */
49
+ "technology-adapter": Set<string>;
50
+ /** The set of package names in each layer */
51
+ "integration-adapter": Set<string>;
52
+ /** The set of package names in each layer */
53
+ "dev-tooling": Set<string>;
54
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Default repository facts used by the dependency-cruiser rule builder
3
+ * when no overrides are supplied.
4
+ *
5
+ * Repositories MAY override these in their `comity.config.json` or via
6
+ * the `buildDependencyRules` options. They MUST NOT alter layering policy
7
+ * through configuration.
8
+ */
9
+ export const DEFAULT_REPOSITORY_FACTS = Object.freeze({
10
+ packageRoots: ["packages"],
11
+ tsConfigFileName: "tsconfig.json",
12
+ });
13
+ /**
14
+ * Canonical layer profile comments referenced by the dependency rule
15
+ * builder. Keeping the prose here makes rule generation deterministic and
16
+ * prevents duplication across repositories.
17
+ */
18
+ export const LAYER_PROFILES = Object.freeze({
19
+ primitives: {
20
+ comment: "Primitives MUST NOT depend on any internal package (layering-policy.md §2.1).",
21
+ },
22
+ kernel: {
23
+ comment: "Kernel MUST only depend on primitives (layering-policy.md §2.1).",
24
+ },
25
+ composition: {
26
+ comment: "Composition MUST only depend on kernel and primitives (layering-policy.md §2.1).",
27
+ },
28
+ core: {
29
+ comment: "Core Modules MAY depend on primitives, kernel, and other Core Modules explicitly permitted via ADR-008.",
30
+ },
31
+ "technology-adapter": {
32
+ comment: "Technology Adapters MUST NOT depend on Adapters except Integration Adapter → Technology Adapter (ADR-007).",
33
+ },
34
+ "integration-adapter": {
35
+ comment: "Integration Adapters may depend on primitives, kernel, core, and technology adapters (ADR-007).",
36
+ },
37
+ });
@@ -0,0 +1,6 @@
1
+ export type { BuildOptions, DependencyCruiserConfig, DependencyRule, } from "./build-config.js";
2
+ export type { PackageRecord } from "./classify.js";
3
+ export type { LayerName, LayerSets } from "./constants.js";
4
+ export { buildDependencyRules, buildDependencyRulesForRoot, } from "./build-config.js";
5
+ export { classifyByLayer, layerSetsFromPackages } from "./classify.js";
6
+ export { DEFAULT_REPOSITORY_FACTS, LAYER_PROFILES } from "./constants.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { buildDependencyRules, buildDependencyRulesForRoot, } from "./build-config.js";
2
+ export { classifyByLayer, layerSetsFromPackages } from "./classify.js";
3
+ export { DEFAULT_REPOSITORY_FACTS, LAYER_PROFILES } from "./constants.js";
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@comity-dev/dependency-rules",
3
+ "version": "0.1.0",
4
+ "description": "Reusable dependency-cruiser rule generator for Comity layering. Development tooling.",
5
+ "type": "module",
6
+ "private": false,
7
+ "license": "MIT",
8
+ "comity": {
9
+ "layer": "dev-tooling"
10
+ },
11
+ "engines": {
12
+ "node": ">=24.0.0"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./build-config": {
22
+ "types": "./dist/build-config.d.ts",
23
+ "default": "./dist/build-config.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "./dist"
28
+ ],
29
+ "dependencies": {},
30
+ "devDependencies": {
31
+ "@types/node": "^24.13.3",
32
+ "dependency-cruiser": "^16.10.0",
33
+ "typescript": "^5.9.3"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "test": "vitest run"
38
+ }
39
+ }