@comity-dev/eslint-plugin 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,46 @@
1
+ # @comity-dev/eslint-plugin
2
+
3
+ ## Purpose
4
+
5
+ Reusable ESLint rules for Comity-wide source-code policy. This package owns the source-code lint policy that every Comity repository MUST apply consistently.
6
+
7
+ ## Scope
8
+
9
+ Six rules, each addressing a Comity-wide concern:
10
+
11
+ | Rule | Concern |
12
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `no-forbidden-deep-import` | Disallow deep imports into another package's internals (`@comity/x/src/foo`, `@comity/x/internal/*`, `@comity/x/dist/*`, `@comity/x/lazy/*`). |
14
+ | `no-layer-violation` | Forbid imports that violate Comity's canonical layering model. |
15
+ | `no-date-in-core` | Forbid `Date` in Core Modules (domain must be deterministic). |
16
+ | `no-crypto-in-core` | Forbid `crypto` in Core Modules (domain must be deterministic). |
17
+ | `no-forbidden-subpath-import` | Disallow imports from forbidden package subpaths (`utils`, `helpers`, `shared`, `internal`, `lazy`). |
18
+ | `no-render-core-http-runtime` | Forbid render Core Modules from depending on HTTP runtime. |
19
+
20
+ ## Ownership
21
+
22
+ Owned by `comity-development`. Consumed by Comity repositories (Community, Enterprise) via ESLint flat config.
23
+
24
+ ## Public API
25
+
26
+ ```ts
27
+ // Default export: the ESLint plugin (loaded into flat-config)
28
+ import comityPlugin from "@comity-dev/eslint-plugin";
29
+
30
+ // Root exports: stable identifier list and the recommended rule set
31
+ import { recommended, RULE_IDS } from "@comity-dev/eslint-plugin";
32
+ ```
33
+
34
+ ## Relationship to Comity Standards
35
+
36
+ - `architecture-validation.md` §4 — source-code lint policy.
37
+ - `public-api.md` §3.2 — forbidden subpath imports.
38
+ - `layering-policy.md` §8 — rendering core MUST NOT depend on HTTP runtime.
39
+ - `coding.md` — general style policy.
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ pnpm build
45
+ pnpm test
46
+ ```
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
3
+ *
4
+ * This package owns the canonical ESLint rules for Comity. Repositories
5
+ * consume these rules via the shared orchestrator; they MUST NOT redefine
6
+ * source-code policy.
7
+ *
8
+ * Standard references:
9
+ * - architecture-validation.md §4
10
+ * - public-api.md §3.2 (forbidden subpath imports)
11
+ * - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
12
+ * - coding.md (general style policy)
13
+ *
14
+ * Public surface (intentional):
15
+ * - default export: the ESLint plugin (loaded into flat-config)
16
+ * - `recommended`: the canonical Comity rule set
17
+ * - `RULE_IDS`: stable list of rule identifiers
18
+ */
19
+ export { plugin as default, RULE_IDS } from "./plugin.js";
20
+ export { recommended } from "./recommended.js";
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
3
+ *
4
+ * This package owns the canonical ESLint rules for Comity. Repositories
5
+ * consume these rules via the shared orchestrator; they MUST NOT redefine
6
+ * source-code policy.
7
+ *
8
+ * Standard references:
9
+ * - architecture-validation.md §4
10
+ * - public-api.md §3.2 (forbidden subpath imports)
11
+ * - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
12
+ * - coding.md (general style policy)
13
+ *
14
+ * Public surface (intentional):
15
+ * - default export: the ESLint plugin (loaded into flat-config)
16
+ * - `recommended`: the canonical Comity rule set
17
+ * - `RULE_IDS`: stable list of rule identifiers
18
+ */
19
+ export { plugin as default, RULE_IDS } from "./plugin.js";
20
+ export { recommended } from "./recommended.js";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
3
+ *
4
+ * This package owns the canonical ESLint rules for Comity. Repositories
5
+ * consume these rules via the shared orchestrator; they MUST NOT redefine
6
+ * source-code policy.
7
+ *
8
+ * Standard references:
9
+ * - architecture-validation.md §4
10
+ * - public-api.md §3.2 (forbidden subpath imports)
11
+ * - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
12
+ * - coding.md (general style policy)
13
+ *
14
+ * Public surface (intentional):
15
+ * - default export: the ESLint plugin (loaded into flat-config)
16
+ * - `recommended`: the canonical Comity rule set
17
+ * - `RULE_IDS`: stable list of rule identifiers
18
+ */
19
+ export interface BuiltinRule {
20
+ /** The stable identifier of the rule */
21
+ name: string;
22
+ /** The rule implementation */
23
+ rule: unknown;
24
+ }
25
+ export declare const BUILTIN_RULES: BuiltinRule[];
26
+ export declare const plugin: import("eslint").ESLint.Plugin;
27
+ /** Stable list of rule identifiers exported by this plugin. */
28
+ export declare const RULE_IDS: string[];
package/dist/plugin.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
3
+ *
4
+ * This package owns the canonical ESLint rules for Comity. Repositories
5
+ * consume these rules via the shared orchestrator; they MUST NOT redefine
6
+ * source-code policy.
7
+ *
8
+ * Standard references:
9
+ * - architecture-validation.md §4
10
+ * - public-api.md §3.2 (forbidden subpath imports)
11
+ * - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
12
+ * - coding.md (general style policy)
13
+ *
14
+ * Public surface (intentional):
15
+ * - default export: the ESLint plugin (loaded into flat-config)
16
+ * - `recommended`: the canonical Comity rule set
17
+ * - `RULE_IDS`: stable list of rule identifiers
18
+ */
19
+ import noCryptoInCore from "./rules/no-crypto-in-core.js";
20
+ import noDateInCore from "./rules/no-date-in-core.js";
21
+ import noForbiddenDeepImport from "./rules/no-forbidden-deep-import.js";
22
+ import noForbiddenSubpathImport from "./rules/no-forbidden-subpath-import.js";
23
+ import noLayerViolation from "./rules/no-layer-violation.js";
24
+ import noRenderCoreHttpRuntime from "./rules/no-render-core-http-runtime.js";
25
+ export const BUILTIN_RULES = [
26
+ { name: "no-forbidden-deep-import", rule: noForbiddenDeepImport },
27
+ { name: "no-layer-violation", rule: noLayerViolation },
28
+ { name: "no-date-in-core", rule: noDateInCore },
29
+ { name: "no-crypto-in-core", rule: noCryptoInCore },
30
+ { name: "no-forbidden-subpath-import", rule: noForbiddenSubpathImport },
31
+ { name: "no-render-core-http-runtime", rule: noRenderCoreHttpRuntime },
32
+ ];
33
+ export const plugin = {
34
+ meta: {
35
+ name: "@comity-dev/eslint-plugin",
36
+ version: "0.1.0",
37
+ },
38
+ rules: Object.fromEntries(BUILTIN_RULES.map((r) => [r.name, r.rule])),
39
+ };
40
+ /** Stable list of rule identifiers exported by this plugin. */
41
+ export const RULE_IDS = BUILTIN_RULES.map((r) => r.name);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Recommended Comity ESLint rule set.
3
+ *
4
+ * The "recommended" preset bundles the canonical Comity rules with their
5
+ * default severities. Repositories MAY override severities; they MUST NOT
6
+ * disable rules silently.
7
+ */
8
+ import type { Linter } from "eslint";
9
+ export declare const recommended: Linter.Config;
@@ -0,0 +1,10 @@
1
+ export const recommended = {
2
+ rules: {
3
+ "@comity-dev/no-forbidden-deep-import": "error",
4
+ "@comity-dev/no-layer-violation": "error",
5
+ "@comity-dev/no-date-in-core": "error",
6
+ "@comity-dev/no-crypto-in-core": "error",
7
+ "@comity-dev/no-forbidden-subpath-import": "error",
8
+ "@comity-dev/no-render-core-http-runtime": "error",
9
+ },
10
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @comity-dev/no-crypto-in-core
3
+ *
4
+ * Forbid Node's `crypto` and WebCrypto (`globalThis.crypto`) modules in Core
5
+ * packages. Core must be deterministic and side-effect free; cryptography
6
+ * belongs in Adapters that own runtime concerns.
7
+ *
8
+ * Standard references:
9
+ * - layering-policy.md §2.2 (Core boundaries)
10
+ * - adapters.md
11
+ */
12
+ type Rule = unknown;
13
+ declare const rule: Rule;
14
+ export default rule;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @comity-dev/no-crypto-in-core
3
+ *
4
+ * Forbid Node's `crypto` and WebCrypto (`globalThis.crypto`) modules in Core
5
+ * packages. Core must be deterministic and side-effect free; cryptography
6
+ * belongs in Adapters that own runtime concerns.
7
+ *
8
+ * Standard references:
9
+ * - layering-policy.md §2.2 (Core boundaries)
10
+ * - adapters.md
11
+ */
12
+ const FORBIDDEN_SPECIFIERS = new Set(["crypto", "node:crypto", "webcrypto"]);
13
+ const messages = {
14
+ forbiddenImport: "Importing '{{specifier}}' is forbidden in Core (layering-policy.md §2.2). Use a Technology Adapter instead.",
15
+ forbiddenGlobal: "Accessing the `crypto` global is forbidden in Core (layering-policy.md §2.2). Use a Technology Adapter instead.",
16
+ };
17
+ function isInCorePackage(filename) {
18
+ return (filename.includes("/packages/") &&
19
+ (filename.includes("/primitives/") ||
20
+ filename.includes("/kernel/") ||
21
+ filename.includes("/composition/") ||
22
+ filename.includes("/http/") ||
23
+ filename.includes("/router/") ||
24
+ filename.includes("/html/") ||
25
+ filename.includes("/hydration/") ||
26
+ filename.includes("/seo/") ||
27
+ filename.includes("/content/") ||
28
+ filename.includes("/media/") ||
29
+ filename.includes("/search/") ||
30
+ filename.includes("/cache/") ||
31
+ filename.includes("/auth/") ||
32
+ filename.includes("/catalog/") ||
33
+ filename.includes("/customer/") ||
34
+ filename.includes("/inventory/") ||
35
+ filename.includes("/order/") ||
36
+ filename.includes("/payment/") ||
37
+ filename.includes("/pricing/") ||
38
+ filename.includes("/storefront/") ||
39
+ filename.includes("/taxonomy/") ||
40
+ filename.includes("/validation/") ||
41
+ filename.includes("/address/") ||
42
+ filename.includes("/geography/") ||
43
+ filename.includes("/identity/") ||
44
+ filename.includes("/organization/")));
45
+ }
46
+ const rule = {
47
+ meta: {
48
+ type: "problem",
49
+ docs: {
50
+ description: "Forbid `crypto` imports in Core packages — cryptography belongs in Adapters.",
51
+ category: "Comity Architecture",
52
+ recommended: true,
53
+ },
54
+ schema: [
55
+ {
56
+ type: "object",
57
+ properties: {
58
+ sourceName: {
59
+ type: "string",
60
+ description: "Name of the source package (e.g., '@comity/auth'). Used to scope the rule.",
61
+ },
62
+ },
63
+ additionalProperties: false,
64
+ },
65
+ ],
66
+ messages,
67
+ },
68
+ create(context) {
69
+ const opts = (context.options[0] ?? {});
70
+ const sourceName = opts.sourceName ?? null;
71
+ // If sourceName provided, use it for classification; otherwise fall back to filename
72
+ const inCore = sourceName
73
+ ? sourceName.startsWith("@comity/") &&
74
+ !["@comity/cli", "@comity/graphql-builder"].includes(sourceName)
75
+ : isInCorePackage(context.filename ?? context.getFilename());
76
+ // Disable this rule outside Core packages
77
+ if (!inCore) {
78
+ return {};
79
+ }
80
+ function checkSpecifier(value) {
81
+ if (!value)
82
+ return false;
83
+ return FORBIDDEN_SPECIFIERS.has(value);
84
+ }
85
+ return {
86
+ ImportDeclaration(node) {
87
+ if (checkSpecifier(node.source.value)) {
88
+ context.report({
89
+ node,
90
+ messageId: "forbiddenImport",
91
+ data: { specifier: node.source.value },
92
+ });
93
+ }
94
+ },
95
+ ImportExpression(node) {
96
+ // dynamic import()
97
+ if (node.source.type === "Literal" &&
98
+ typeof node.source.value === "string" &&
99
+ checkSpecifier(node.source.value)) {
100
+ context.report({
101
+ node,
102
+ messageId: "forbiddenImport",
103
+ data: { specifier: node.source.value },
104
+ });
105
+ }
106
+ },
107
+ Identifier(node) {
108
+ if (node.name !== "crypto")
109
+ return;
110
+ // Top-level access of the `crypto` global
111
+ const parent = node.parent;
112
+ if (!parent)
113
+ return;
114
+ if (parent.type === "ExpressionStatement" ||
115
+ parent.type === "CallExpression" ||
116
+ parent.type === "MemberExpression") {
117
+ context.report({ node, messageId: "forbiddenGlobal" });
118
+ }
119
+ },
120
+ };
121
+ },
122
+ };
123
+ export default rule;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @comity-dev/no-date-in-core
3
+ *
4
+ * Forbid `Date`, `performance.now`, and `Math.random` access in Core and
5
+ * Kernel packages — they introduce non-deterministic behavior that breaks
6
+ * the Comity reproducibility contract.
7
+ *
8
+ * Legitimate exceptions:
9
+ * - Telemetry/timing measurements using `performance.now()` (observational, not domain logic)
10
+ * - Timestamp generation using `Date.now()` or `Date` constructor for logging/metadata
11
+ * - Time primitives in `@comity/primitives/time` (explicitly allowed)
12
+ *
13
+ * Standard references:
14
+ * - layering-policy.md §6 (determinism)
15
+ * - domain-modeling.md
16
+ */
17
+ type Rule = unknown;
18
+ declare const rule: Rule;
19
+ export default rule;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @comity-dev/no-date-in-core
3
+ *
4
+ * Forbid `Date`, `performance.now`, and `Math.random` access in Core and
5
+ * Kernel packages — they introduce non-deterministic behavior that breaks
6
+ * the Comity reproducibility contract.
7
+ *
8
+ * Legitimate exceptions:
9
+ * - Telemetry/timing measurements using `performance.now()` (observational, not domain logic)
10
+ * - Timestamp generation using `Date.now()` or `Date` constructor for logging/metadata
11
+ * - Time primitives in `@comity/primitives/time` (explicitly allowed)
12
+ *
13
+ * Standard references:
14
+ * - layering-policy.md §6 (determinism)
15
+ * - domain-modeling.md
16
+ */
17
+ const FORBIDDEN_GLOBALS = new Set(["Date", "performance"]);
18
+ const FORBIDDEN_PROPERTIES = {
19
+ Math: ["random"],
20
+ };
21
+ // Package names that are explicitly allowed to use Date/performance for telemetry
22
+ const TELEMETRY_ALLOWLIST = new Set([
23
+ "@comity/primitives", // time/instant primitives
24
+ "@comity/http", // facade timing
25
+ "@comity/html", // renderer pipeline timing
26
+ "@comity/hydration", // browser capability implementations (createBrowserHydrationCapabilities, createNoopHydrationCapabilities)
27
+ ]);
28
+ // Allowlist for specific property access patterns that are legitimate telemetry
29
+ const TELEMETRY_PROPERTY_ALLOWLIST = new Set([
30
+ "performance.now",
31
+ "Date.now",
32
+ ]);
33
+ const messages = {
34
+ forbiddenGlobal: "Non-deterministic global '{{name}}' is forbidden in Core / Kernel (layering-policy.md §6). Use telemetry allowlist or primitives/time for timing.",
35
+ forbiddenProperty: "Non-deterministic '{{object}}.{{property}}' is forbidden in Core / Kernel (layering-policy.md §6).",
36
+ };
37
+ function isInCoreOrKernelPackage(filename) {
38
+ return (filename.includes("/packages/") &&
39
+ (filename.includes("/primitives/") ||
40
+ filename.includes("/kernel/") ||
41
+ filename.includes("/composition/") ||
42
+ filename.includes("/http/") ||
43
+ filename.includes("/router/") ||
44
+ filename.includes("/html/") ||
45
+ filename.includes("/hydration/") ||
46
+ filename.includes("/seo/") ||
47
+ filename.includes("/content/") ||
48
+ filename.includes("/media/") ||
49
+ filename.includes("/search/")));
50
+ }
51
+ function isTelemetryAllowedPackage(filename) {
52
+ return Array.from(TELEMETRY_ALLOWLIST).some((pkg) => filename.includes(`/packages/${pkg.replace("@comity/", "")}/`) || filename.includes(`/community/packages/${pkg.replace("@comity/", "")}/`));
53
+ }
54
+ function isTelemetryPropertyAccess(objectName, propertyName) {
55
+ return TELEMETRY_PROPERTY_ALLOWLIST.has(`${objectName}.${propertyName}`);
56
+ }
57
+ const rule = {
58
+ meta: {
59
+ type: "problem",
60
+ docs: {
61
+ description: "Forbid Date / performance.now / Math.random in Core and Kernel packages.",
62
+ category: "Comity Architecture",
63
+ recommended: true,
64
+ },
65
+ schema: [
66
+ {
67
+ type: "object",
68
+ properties: {
69
+ sourceName: {
70
+ type: "string",
71
+ description: "Name of the source package (e.g., '@comity/http'). Used to scope the rule.",
72
+ },
73
+ },
74
+ additionalProperties: false,
75
+ },
76
+ ],
77
+ messages,
78
+ },
79
+ create(context) {
80
+ const opts = (context.options[0] ?? {});
81
+ const sourceName = opts.sourceName ?? null;
82
+ // If sourceName provided, use it for classification; otherwise fall back to filename
83
+ const inCoreOrKernel = sourceName
84
+ ? sourceName.startsWith("@comity/") &&
85
+ !["@comity/cli", "@comity/graphql-builder"].includes(sourceName) // these are not core/kernel
86
+ : isInCoreOrKernelPackage(context.filename ?? context.getFilename());
87
+ // Disable this rule outside Core/Kernel packages
88
+ if (!inCoreOrKernel) {
89
+ return {};
90
+ }
91
+ // Check if this package is allowed to use telemetry patterns
92
+ const allowTelemetry = sourceName
93
+ ? TELEMETRY_ALLOWLIST.has(sourceName)
94
+ : isTelemetryAllowedPackage(context.filename ?? context.getFilename());
95
+ return {
96
+ Identifier(node) {
97
+ // Detect globals like `Date` and `performance`
98
+ if ((node.name === "Date" || node.name === "performance") &&
99
+ // Only top-level usage (not as property of another object)
100
+ node.parent &&
101
+ (node.parent.type === "ExpressionStatement" ||
102
+ node.parent.type === "VariableDeclarator" ||
103
+ node.parent.type === "CallExpression" ||
104
+ node.parent.type === "NewExpression" ||
105
+ node.parent.type === "MemberExpression")) {
106
+ // Allow telemetry patterns in allowlisted packages
107
+ if (allowTelemetry && isTelemetryPropertyAccess(node.parent.object?.name ?? "", node.name)) {
108
+ return;
109
+ }
110
+ // Allow Date.now() and performance.now() in telemetry allowlisted packages
111
+ if (allowTelemetry &&
112
+ ((node.name === "Date" && node.parent.type === "MemberExpression" && node.parent.property?.name === "now") ||
113
+ (node.name === "performance" && node.parent.type === "MemberExpression" && node.parent.property?.name === "now"))) {
114
+ return;
115
+ }
116
+ // Also handle optional chaining: window?.performance?.now()
117
+ // Walk up the ancestor chain to find a MemberExpression with property 'now'
118
+ if (allowTelemetry && (node.name === "Date" || node.name === "performance")) {
119
+ let current = node.parent;
120
+ while (current) {
121
+ if (current.type === "MemberExpression" && current.property?.name === "now") {
122
+ return;
123
+ }
124
+ if (current.type === "ChainExpression") {
125
+ current = current.expression;
126
+ continue;
127
+ }
128
+ current = current.parent;
129
+ }
130
+ }
131
+ if (FORBIDDEN_GLOBALS.has(node.name)) {
132
+ context.report({
133
+ node,
134
+ messageId: "forbiddenGlobal",
135
+ data: { name: node.name },
136
+ });
137
+ }
138
+ }
139
+ // Detect property access like Math.random
140
+ if (node.parent &&
141
+ node.parent.type === "MemberExpression" &&
142
+ node.parent.property === node) {
143
+ const obj = node.parent.object;
144
+ if (obj.type === "Identifier") {
145
+ const allowed = FORBIDDEN_PROPERTIES[obj.name];
146
+ if (allowed && allowed.includes(node.name)) {
147
+ // Allow Math.random in primitives/time (explicit time primitive)
148
+ if (allowTelemetry && sourceName === "@comity/primitives") {
149
+ return;
150
+ }
151
+ context.report({
152
+ node,
153
+ messageId: "forbiddenProperty",
154
+ data: { object: obj.name, property: node.name },
155
+ });
156
+ }
157
+ }
158
+ }
159
+ },
160
+ };
161
+ },
162
+ };
163
+ export default rule;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @comity-dev/no-forbidden-deep-import
3
+ *
4
+ * Forbid deep imports into another `@comity/*` package's internals.
5
+ *
6
+ * Examples:
7
+ * import { foo } from "@comity/pricing/src/foo" // ❌
8
+ * import { foo } from "@comity/pricing/dist/foo.js" // ❌
9
+ * import { foo } from "@comity/pricing/internal/x" // ❌
10
+ * import { foo } from "@comity/pricing/lazy/x" // ❌
11
+ * import { foo } from "@comity/pricing" // ✅
12
+ * import { foo } from "@comity/pricing/exports/foo" // ✅
13
+ *
14
+ * Standard references:
15
+ * - public-api.md §3.2
16
+ * - architecture-validation.md §4.1
17
+ */
18
+ type Rule = unknown;
19
+ declare const rule: Rule;
20
+ export default rule;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @comity-dev/no-forbidden-deep-import
3
+ *
4
+ * Forbid deep imports into another `@comity/*` package's internals.
5
+ *
6
+ * Examples:
7
+ * import { foo } from "@comity/pricing/src/foo" // ❌
8
+ * import { foo } from "@comity/pricing/dist/foo.js" // ❌
9
+ * import { foo } from "@comity/pricing/internal/x" // ❌
10
+ * import { foo } from "@comity/pricing/lazy/x" // ❌
11
+ * import { foo } from "@comity/pricing" // ✅
12
+ * import { foo } from "@comity/pricing/exports/foo" // ✅
13
+ *
14
+ * Standard references:
15
+ * - public-api.md §3.2
16
+ * - architecture-validation.md §4.1
17
+ */
18
+ const FORBIDDEN_PATTERNS = [
19
+ /^@comity\/[^/]+\/src\//,
20
+ /^@comity\/[^/]+\/dist\//,
21
+ /^@comity\/[^/]+\/internal/,
22
+ /^@comity\/[^/]+\/lazy/,
23
+ ];
24
+ function isForbidden(specifier) {
25
+ return FORBIDDEN_PATTERNS.some((re) => re.test(specifier));
26
+ }
27
+ const messages = {
28
+ forbidden: "Deep imports into another package's internals are forbidden (public-api.md §3.2). Import only from declared public subpaths.",
29
+ };
30
+ const rule = {
31
+ meta: {
32
+ type: "problem",
33
+ docs: {
34
+ description: "Forbid deep imports into other `@comity/*` packages' internals (src/, dist/, internal/, lazy/).",
35
+ category: "Comity Architecture",
36
+ recommended: true,
37
+ },
38
+ schema: [],
39
+ messages,
40
+ },
41
+ create(context) {
42
+ return {
43
+ ImportDeclaration(node) {
44
+ const specifier = node.source.value;
45
+ if (!specifier || !specifier.startsWith("@comity/"))
46
+ return;
47
+ if (isForbidden(specifier)) {
48
+ context.report({ node, messageId: "forbidden" });
49
+ }
50
+ },
51
+ ExportNamedDeclaration(node) {
52
+ if (!node.source || !node.source.value)
53
+ return;
54
+ const specifier = node.source.value;
55
+ if (!specifier.startsWith("@comity/"))
56
+ return;
57
+ if (isForbidden(specifier)) {
58
+ context.report({ node, messageId: "forbidden" });
59
+ }
60
+ },
61
+ ExportAllDeclaration(node) {
62
+ const specifier = node.source.value;
63
+ if (!specifier || !specifier.startsWith("@comity/"))
64
+ return;
65
+ if (isForbidden(specifier)) {
66
+ context.report({ node, messageId: "forbidden" });
67
+ }
68
+ },
69
+ };
70
+ },
71
+ };
72
+ export default rule;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @comity-dev/no-forbidden-subpath-import
3
+ *
4
+ * Forbid exports to forbidden subpaths (`utils`, `helpers`, `shared`,
5
+ * `internal`, `lazy`) per `public-api.md §3.2`.
6
+ *
7
+ * Standard references:
8
+ * - public-api.md §3.2
9
+ * - architecture-validation.md §4.2
10
+ */
11
+ type Rule = unknown;
12
+ declare const rule: Rule;
13
+ export default rule;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @comity-dev/no-forbidden-subpath-import
3
+ *
4
+ * Forbid exports to forbidden subpaths (`utils`, `helpers`, `shared`,
5
+ * `internal`, `lazy`) per `public-api.md §3.2`.
6
+ *
7
+ * Standard references:
8
+ * - public-api.md §3.2
9
+ * - architecture-validation.md §4.2
10
+ */
11
+ const FORBIDDEN_SUBPATHS = new Set([
12
+ "utils",
13
+ "helpers",
14
+ "shared",
15
+ "internal",
16
+ "lazy",
17
+ ]);
18
+ const messages = {
19
+ forbiddenSubpath: "Exports subpath './{{segment}}' is forbidden (public-api.md §3.2).",
20
+ };
21
+ const rule = {
22
+ meta: {
23
+ type: "problem",
24
+ docs: {
25
+ description: "Forbid exports subpaths `utils`, `helpers`, `shared`, `internal`, `lazy`.",
26
+ category: "Comity Architecture",
27
+ recommended: true,
28
+ },
29
+ schema: [],
30
+ messages,
31
+ },
32
+ create(context) {
33
+ function isComityPackage(pkgName) {
34
+ return pkgName.startsWith("@comity/");
35
+ }
36
+ return {
37
+ ExportNamedDeclaration(node) {
38
+ // No source = local export; not relevant
39
+ if (!node.source || !node.source.value)
40
+ return;
41
+ const specifier = node.source.value;
42
+ if (!specifier.startsWith("@comity/"))
43
+ return;
44
+ // Only check imports/exports OF other @comity packages
45
+ const pkgMatch = specifier.match(/^@comity\/[^/]+/);
46
+ if (!pkgMatch || !isComityPackage(pkgMatch[0]))
47
+ return;
48
+ // Subpath after the package name
49
+ const subpath = specifier.slice(pkgMatch[0].length).replace(/^\//, "");
50
+ const segment = subpath.split("/")[0];
51
+ if (segment && FORBIDDEN_SUBPATHS.has(segment)) {
52
+ context.report({
53
+ node,
54
+ messageId: "forbiddenSubpath",
55
+ data: { segment },
56
+ });
57
+ }
58
+ },
59
+ ImportDeclaration(node) {
60
+ const specifier = node.source.value;
61
+ if (!specifier || !specifier.startsWith("@comity/"))
62
+ return;
63
+ const pkgMatch = specifier.match(/^@comity\/[^/]+/);
64
+ if (!pkgMatch || !isComityPackage(pkgMatch[0]))
65
+ return;
66
+ const subpath = specifier.slice(pkgMatch[0].length).replace(/^\//, "");
67
+ const segment = subpath.split("/")[0];
68
+ if (segment && FORBIDDEN_SUBPATHS.has(segment)) {
69
+ context.report({
70
+ node,
71
+ messageId: "forbiddenSubpath",
72
+ data: { segment },
73
+ });
74
+ }
75
+ },
76
+ };
77
+ },
78
+ };
79
+ export default rule;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @comity-dev/no-layer-violation
3
+ *
4
+ * Forbid imports that violate Comity's canonical layering policy.
5
+ *
6
+ * Disallowed edges (default):
7
+ * - primitives → any @comity/*
8
+ * - kernel → anything except primitives
9
+ * - composition → anything except primitives, kernel
10
+ * - core → adapters
11
+ * - adapters → adapters (except integration-adapter → technology-adapter)
12
+ *
13
+ * Technology adapters MAY import the core contract they implement
14
+ * (declared via `implements` metadata).
15
+ *
16
+ * Allowed Core-to-Core edges are gated by ADR-008; this rule does NOT
17
+ * evaluate that register (it is enforced separately by the dependency
18
+ * rules / orchestrator). Use this rule only to catch the obvious violations.
19
+ *
20
+ * Standard references:
21
+ * - layering-policy.md §2 (canonical layering model)
22
+ * - architecture-validation.md §7
23
+ * - ADR-007 (adapter categories)
24
+ */
25
+ type Rule = unknown;
26
+ declare const rule: Rule;
27
+ export default rule;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @comity-dev/no-layer-violation
3
+ *
4
+ * Forbid imports that violate Comity's canonical layering policy.
5
+ *
6
+ * Disallowed edges (default):
7
+ * - primitives → any @comity/*
8
+ * - kernel → anything except primitives
9
+ * - composition → anything except primitives, kernel
10
+ * - core → adapters
11
+ * - adapters → adapters (except integration-adapter → technology-adapter)
12
+ *
13
+ * Technology adapters MAY import the core contract they implement
14
+ * (declared via `implements` metadata).
15
+ *
16
+ * Allowed Core-to-Core edges are gated by ADR-008; this rule does NOT
17
+ * evaluate that register (it is enforced separately by the dependency
18
+ * rules / orchestrator). Use this rule only to catch the obvious violations.
19
+ *
20
+ * Standard references:
21
+ * - layering-policy.md §2 (canonical layering model)
22
+ * - architecture-validation.md §7
23
+ * - ADR-007 (adapter categories)
24
+ */
25
+ const messages = {
26
+ forbidden: "This import violates Comity's layering policy (layering-policy.md §2).",
27
+ };
28
+ const LAYER_PROFILES = {
29
+ primitives: { allowed: new Set() },
30
+ kernel: { allowed: new Set(["primitives"]) },
31
+ composition: { allowed: new Set(["primitives", "kernel"]) },
32
+ core: { allowed: new Set(["primitives", "kernel", "core"]) },
33
+ "technology-adapter": {
34
+ allowed: new Set(["primitives", "kernel"]),
35
+ },
36
+ "integration-adapter": {
37
+ allowed: new Set(["primitives", "kernel", "core", "technology-adapter"]),
38
+ },
39
+ };
40
+ function classifyTarget(specifier) {
41
+ const m = specifier.match(/^(@comity\/[^/]+)/);
42
+ if (!m || !m[1])
43
+ return null;
44
+ return m[1];
45
+ }
46
+ function inferLayerFromFilename(filename) {
47
+ if (!filename)
48
+ return null;
49
+ if (filename.includes("/primitives/"))
50
+ return "primitives";
51
+ if (filename.includes("/kernel/"))
52
+ return "kernel";
53
+ if (filename.includes("/composition/"))
54
+ return "composition";
55
+ if (filename.includes("/packages/")) {
56
+ // Check for core packages
57
+ const corePackages = [
58
+ "http", "router", "html", "hydration", "seo", "content", "media", "search",
59
+ "cache", "auth", "catalog", "customer", "inventory", "order", "payment",
60
+ "pricing", "storefront", "taxonomy", "validation", "address", "geography",
61
+ "identity", "organization", "acl", "auth-tokens", "graphql-builder",
62
+ "graphql-client", "i18n", "sql", "storage"
63
+ ];
64
+ for (const pkg of corePackages) {
65
+ if (filename.includes(`/packages/${pkg}/`))
66
+ return "core";
67
+ }
68
+ // Check for technology adapters
69
+ const adapterPackages = [
70
+ "html-preact", "html-react", "hydration-preact", "hydration-react",
71
+ "http-hono", "http-fetch", "router-path-to-regexp",
72
+ "acl-casl", "auth-jose", "cache-kv", "cache-redis",
73
+ "cli-commander", "graphql-client-fetch", "graphql-client-ws",
74
+ "i18n-typesafe", "sql-kysely", "validation-zod"
75
+ ];
76
+ for (const pkg of adapterPackages) {
77
+ if (filename.includes(`/packages/${pkg}/`))
78
+ return "technology-adapter";
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ const rule = {
84
+ meta: {
85
+ type: "problem",
86
+ docs: {
87
+ description: "Forbid imports that violate Comity's canonical layering policy.",
88
+ category: "Comity Architecture",
89
+ recommended: true,
90
+ },
91
+ schema: [
92
+ {
93
+ type: "object",
94
+ properties: {
95
+ sourceName: {
96
+ type: "string",
97
+ description: "Name of the source package (e.g., '@comity/pricing'). Required.",
98
+ },
99
+ classification: {
100
+ type: "object",
101
+ description: "Map of package name → layer. The plugin receives this via configuration; it MUST NOT hardcode layers.",
102
+ },
103
+ implements: {
104
+ type: "object",
105
+ description: "Map of package name → implemented core package (for technology-adapter packages).",
106
+ },
107
+ },
108
+ required: ["sourceName", "classification"],
109
+ additionalProperties: false,
110
+ },
111
+ ],
112
+ messages,
113
+ },
114
+ create(context) {
115
+ const opts = (context.options[0] ?? {});
116
+ const classification = opts.classification ?? {};
117
+ const implementsMap = opts.implements ?? {};
118
+ const sourceName = opts.sourceName ?? null;
119
+ // Fallback: infer source layer from filename if sourceName not provided
120
+ let sourceLayer = sourceName ? classification[sourceName] : inferLayerFromFilename(context.filename ?? context.getFilename());
121
+ if (!sourceLayer)
122
+ return {};
123
+ const profile = LAYER_PROFILES[sourceLayer];
124
+ if (!profile)
125
+ return {};
126
+ // If sourceName not provided, we can't check implementsMap, so skip that allowance
127
+ const effectiveSourceName = sourceName;
128
+ return {
129
+ ImportDeclaration(node) {
130
+ const specifier = node.source.value;
131
+ if (!specifier)
132
+ return;
133
+ const target = classifyTarget(specifier);
134
+ if (!target)
135
+ return;
136
+ // Look up the target package's layer
137
+ const targetLayer = classification[target];
138
+ if (!targetLayer)
139
+ return; // unknown package — outside this rule's scope
140
+ // Allow technology-adapter to import its implemented core contract
141
+ if (effectiveSourceName &&
142
+ sourceLayer === "technology-adapter" &&
143
+ targetLayer === "core" &&
144
+ implementsMap[effectiveSourceName] === target) {
145
+ return; // Allowed: adapter implements this core contract
146
+ }
147
+ if (!profile.allowed.has(targetLayer)) {
148
+ context.report({ node, messageId: "forbidden" });
149
+ }
150
+ },
151
+ };
152
+ },
153
+ };
154
+ export default rule;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @comity-dev/no-render-core-http-runtime
3
+ *
4
+ * Forbid Rendering Core Modules (html, hydration, search, seo, content)
5
+ * from importing HTTP runtime implementations (http-hono, http-fetch,
6
+ * html-preact, html-react, hydration-preact, hydration-react).
7
+ *
8
+ * The rule is scoped to only apply to files within the declared Rendering
9
+ * Core packages. Technology adapters and client entry points are permitted
10
+ * to import their respective runtimes.
11
+ *
12
+ * Standard references:
13
+ * - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
14
+ * - architecture-validation.md §4.3
15
+ */
16
+ type Rule = unknown;
17
+ declare const rule: Rule;
18
+ export default rule;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @comity-dev/no-render-core-http-runtime
3
+ *
4
+ * Forbid Rendering Core Modules (html, hydration, search, seo, content)
5
+ * from importing HTTP runtime implementations (http-hono, http-fetch,
6
+ * html-preact, html-react, hydration-preact, hydration-react).
7
+ *
8
+ * The rule is scoped to only apply to files within the declared Rendering
9
+ * Core packages. Technology adapters and client entry points are permitted
10
+ * to import their respective runtimes.
11
+ *
12
+ * Standard references:
13
+ * - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
14
+ * - architecture-validation.md §4.3
15
+ */
16
+ const RENDERING_CORE_PACKAGES = new Set([
17
+ "@comity/html",
18
+ "@comity/hydration",
19
+ "@comity/search",
20
+ "@comity/seo",
21
+ "@comity/content",
22
+ ]);
23
+ const FORBIDDEN_RUNTIME_PACKAGES = new Set([
24
+ "@comity/http-hono",
25
+ "@comity/http-fetch",
26
+ "@comity/html-preact",
27
+ "@comity/html-react",
28
+ "@comity/hydration-preact",
29
+ "@comity/hydration-react",
30
+ ]);
31
+ const messages = {
32
+ forbiddenRuntime: "Rendering Core Module MUST NOT import HTTP/Rendering runtime '{{target}}' (layering-policy.md §8).",
33
+ };
34
+ function isInRenderingCorePackage(filename) {
35
+ return Array.from(RENDERING_CORE_PACKAGES).some((pkg) => filename.includes(`/packages/${pkg.replace("@comity/", "")}/`));
36
+ }
37
+ const rule = {
38
+ meta: {
39
+ type: "problem",
40
+ docs: {
41
+ description: "Forbid Rendering Core Modules from importing HTTP / Rendering runtimes.",
42
+ category: "Comity Architecture",
43
+ recommended: true,
44
+ },
45
+ schema: [
46
+ {
47
+ type: "object",
48
+ properties: {
49
+ sourceName: {
50
+ type: "string",
51
+ description: "Name of the source package (e.g., '@comity/html'). Used to scope the rule.",
52
+ },
53
+ },
54
+ additionalProperties: false,
55
+ },
56
+ ],
57
+ messages,
58
+ },
59
+ create(context) {
60
+ const opts = (context.options[0] ?? {});
61
+ const sourceName = opts.sourceName ?? null;
62
+ // If no sourceName provided, fall back to filename heuristic
63
+ const inRenderingCore = sourceName
64
+ ? RENDERING_CORE_PACKAGES.has(sourceName)
65
+ : isInRenderingCorePackage(context.filename ?? context.getFilename());
66
+ // Disable this rule outside rendering core packages
67
+ if (!inRenderingCore) {
68
+ return {};
69
+ }
70
+ function checkImport(specifier, node) {
71
+ if (!specifier.startsWith("@comity/"))
72
+ return;
73
+ if (FORBIDDEN_RUNTIME_PACKAGES.has(specifier)) {
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ context.report({
76
+ node: node,
77
+ messageId: "forbiddenRuntime",
78
+ data: { target: specifier },
79
+ });
80
+ }
81
+ }
82
+ return {
83
+ ImportDeclaration(node) {
84
+ checkImport(node.source.value, node);
85
+ },
86
+ ImportExpression(node) {
87
+ if (node.source.type === "Literal" &&
88
+ typeof node.source.value === "string") {
89
+ checkImport(node.source.value, node);
90
+ }
91
+ },
92
+ ExportNamedDeclaration(node) {
93
+ if (node.source && node.source.value) {
94
+ checkImport(node.source.value, node);
95
+ }
96
+ },
97
+ };
98
+ },
99
+ };
100
+ export default rule;
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@comity-dev/eslint-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Reusable ESLint rules for Comity source-code policy. 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
+ "./recommended": {
22
+ "types": "./dist/recommended.d.ts",
23
+ "default": "./dist/recommended.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "./dist"
28
+ ],
29
+ "peerDependencies": {
30
+ "eslint": "^9.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "eslint": "^9.39.5",
34
+ "typescript": "^5.9.3"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "test": "vitest run"
39
+ }
40
+ }