oxlint-architecture-rules 0.1.0-beta.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/build/dts/adapters/cli/main.d.ts +3 -0
  3. package/build/dts/adapters/cli/main.d.ts.map +1 -0
  4. package/build/dts/adapters/cli/run.d.ts +19 -0
  5. package/build/dts/adapters/cli/run.d.ts.map +1 -0
  6. package/build/dts/adapters/cli/source-facts.d.ts +9 -0
  7. package/build/dts/adapters/cli/source-facts.d.ts.map +1 -0
  8. package/build/dts/adapters/cli/source-files.d.ts +2 -0
  9. package/build/dts/adapters/cli/source-files.d.ts.map +1 -0
  10. package/build/dts/adapters/oxlint/config-loader.d.ts +23 -0
  11. package/build/dts/adapters/oxlint/config-loader.d.ts.map +1 -0
  12. package/build/dts/adapters/oxlint/exports-rule.d.ts +4 -0
  13. package/build/dts/adapters/oxlint/exports-rule.d.ts.map +1 -0
  14. package/build/dts/adapters/oxlint/imports-rule.d.ts +4 -0
  15. package/build/dts/adapters/oxlint/imports-rule.d.ts.map +1 -0
  16. package/build/dts/adapters/oxlint/members-rule.d.ts +4 -0
  17. package/build/dts/adapters/oxlint/members-rule.d.ts.map +1 -0
  18. package/build/dts/adapters/oxlint/oxlint-api.d.ts +19 -0
  19. package/build/dts/adapters/oxlint/oxlint-api.d.ts.map +1 -0
  20. package/build/dts/adapters/oxlint/plugin.d.ts +15 -0
  21. package/build/dts/adapters/oxlint/plugin.d.ts.map +1 -0
  22. package/build/dts/adapters/oxlint/structure-rule.d.ts +4 -0
  23. package/build/dts/adapters/oxlint/structure-rule.d.ts.map +1 -0
  24. package/build/dts/core/baseline.d.ts +16 -0
  25. package/build/dts/core/baseline.d.ts.map +1 -0
  26. package/build/dts/core/exports.d.ts +39 -0
  27. package/build/dts/core/exports.d.ts.map +1 -0
  28. package/build/dts/core/imports.d.ts +27 -0
  29. package/build/dts/core/imports.d.ts.map +1 -0
  30. package/build/dts/core/members.d.ts +28 -0
  31. package/build/dts/core/members.d.ts.map +1 -0
  32. package/build/dts/core/patterns.d.ts +19 -0
  33. package/build/dts/core/patterns.d.ts.map +1 -0
  34. package/build/dts/core/structure.d.ts +48 -0
  35. package/build/dts/core/structure.d.ts.map +1 -0
  36. package/build/dts/domain/architecture-config.d.ts +177 -0
  37. package/build/dts/domain/architecture-config.d.ts.map +1 -0
  38. package/build/dts/domain/architecture-error.d.ts +27 -0
  39. package/build/dts/domain/architecture-error.d.ts.map +1 -0
  40. package/build/dts/domain/violation.d.ts +11 -0
  41. package/build/dts/domain/violation.d.ts.map +1 -0
  42. package/build/dts/index.d.ts +10 -0
  43. package/build/dts/index.d.ts.map +1 -0
  44. package/build/dts/infrastructure/file-system-fake.d.ts +3 -0
  45. package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -0
  46. package/build/dts/infrastructure/file-system-live.d.ts +3 -0
  47. package/build/dts/infrastructure/file-system-live.d.ts.map +1 -0
  48. package/build/dts/infrastructure/module-resolver-fake.d.ts +3 -0
  49. package/build/dts/infrastructure/module-resolver-fake.d.ts.map +1 -0
  50. package/build/dts/infrastructure/module-resolver-live.d.ts +4 -0
  51. package/build/dts/infrastructure/module-resolver-live.d.ts.map +1 -0
  52. package/build/dts/manifest/compile.d.ts +16 -0
  53. package/build/dts/manifest/compile.d.ts.map +1 -0
  54. package/build/dts/manifest/glob.d.ts +14 -0
  55. package/build/dts/manifest/glob.d.ts.map +1 -0
  56. package/build/dts/manifest/manifest.d.ts +96 -0
  57. package/build/dts/manifest/manifest.d.ts.map +1 -0
  58. package/build/dts/ports/file-system.d.ts +4 -0
  59. package/build/dts/ports/file-system.d.ts.map +1 -0
  60. package/build/dts/ports/module-resolver.d.ts +11 -0
  61. package/build/dts/ports/module-resolver.d.ts.map +1 -0
  62. package/build/esm/adapters/cli/main.js +17 -0
  63. package/build/esm/adapters/cli/main.js.map +1 -0
  64. package/build/esm/adapters/cli/run.js +188 -0
  65. package/build/esm/adapters/cli/run.js.map +1 -0
  66. package/build/esm/adapters/cli/source-facts.js +117 -0
  67. package/build/esm/adapters/cli/source-facts.js.map +1 -0
  68. package/build/esm/adapters/cli/source-files.js +40 -0
  69. package/build/esm/adapters/cli/source-files.js.map +1 -0
  70. package/build/esm/adapters/oxlint/config-loader.js +81 -0
  71. package/build/esm/adapters/oxlint/config-loader.js.map +1 -0
  72. package/build/esm/adapters/oxlint/exports-rule.js +104 -0
  73. package/build/esm/adapters/oxlint/exports-rule.js.map +1 -0
  74. package/build/esm/adapters/oxlint/imports-rule.js +55 -0
  75. package/build/esm/adapters/oxlint/imports-rule.js.map +1 -0
  76. package/build/esm/adapters/oxlint/members-rule.js +75 -0
  77. package/build/esm/adapters/oxlint/members-rule.js.map +1 -0
  78. package/build/esm/adapters/oxlint/oxlint-api.js +7 -0
  79. package/build/esm/adapters/oxlint/oxlint-api.js.map +1 -0
  80. package/build/esm/adapters/oxlint/plugin.js +23 -0
  81. package/build/esm/adapters/oxlint/plugin.js.map +1 -0
  82. package/build/esm/adapters/oxlint/structure-rule.js +37 -0
  83. package/build/esm/adapters/oxlint/structure-rule.js.map +1 -0
  84. package/build/esm/core/baseline.js +30 -0
  85. package/build/esm/core/baseline.js.map +1 -0
  86. package/build/esm/core/exports.js +96 -0
  87. package/build/esm/core/exports.js.map +1 -0
  88. package/build/esm/core/imports.js +85 -0
  89. package/build/esm/core/imports.js.map +1 -0
  90. package/build/esm/core/members.js +84 -0
  91. package/build/esm/core/members.js.map +1 -0
  92. package/build/esm/core/patterns.js +78 -0
  93. package/build/esm/core/patterns.js.map +1 -0
  94. package/build/esm/core/structure.js +256 -0
  95. package/build/esm/core/structure.js.map +1 -0
  96. package/build/esm/domain/architecture-config.js +172 -0
  97. package/build/esm/domain/architecture-config.js.map +1 -0
  98. package/build/esm/domain/architecture-error.js +29 -0
  99. package/build/esm/domain/architecture-error.js.map +1 -0
  100. package/build/esm/domain/violation.js +8 -0
  101. package/build/esm/domain/violation.js.map +1 -0
  102. package/build/esm/index.js +8 -0
  103. package/build/esm/index.js.map +1 -0
  104. package/build/esm/infrastructure/file-system-fake.js +5 -0
  105. package/build/esm/infrastructure/file-system-fake.js.map +1 -0
  106. package/build/esm/infrastructure/file-system-live.js +19 -0
  107. package/build/esm/infrastructure/file-system-live.js.map +1 -0
  108. package/build/esm/infrastructure/module-resolver-fake.js +23 -0
  109. package/build/esm/infrastructure/module-resolver-fake.js.map +1 -0
  110. package/build/esm/infrastructure/module-resolver-live.js +71 -0
  111. package/build/esm/infrastructure/module-resolver-live.js.map +1 -0
  112. package/build/esm/manifest/compile.js +503 -0
  113. package/build/esm/manifest/compile.js.map +1 -0
  114. package/build/esm/manifest/glob.js +77 -0
  115. package/build/esm/manifest/glob.js.map +1 -0
  116. package/build/esm/manifest/manifest.js +120 -0
  117. package/build/esm/manifest/manifest.js.map +1 -0
  118. package/build/esm/ports/file-system.js +2 -0
  119. package/build/esm/ports/file-system.js.map +1 -0
  120. package/build/esm/ports/module-resolver.js +2 -0
  121. package/build/esm/ports/module-resolver.js.map +1 -0
  122. package/package.json +73 -0
  123. package/src/adapters/cli/main.ts +20 -0
  124. package/src/adapters/cli/run.ts +270 -0
  125. package/src/adapters/cli/source-facts.ts +137 -0
  126. package/src/adapters/cli/source-files.ts +42 -0
  127. package/src/adapters/oxlint/config-loader.ts +136 -0
  128. package/src/adapters/oxlint/exports-rule.ts +144 -0
  129. package/src/adapters/oxlint/imports-rule.ts +67 -0
  130. package/src/adapters/oxlint/members-rule.ts +114 -0
  131. package/src/adapters/oxlint/oxlint-api.ts +25 -0
  132. package/src/adapters/oxlint/plugin.ts +35 -0
  133. package/src/adapters/oxlint/structure-rule.ts +39 -0
  134. package/src/core/baseline.ts +62 -0
  135. package/src/core/exports.ts +164 -0
  136. package/src/core/imports.ts +138 -0
  137. package/src/core/members.ts +131 -0
  138. package/src/core/patterns.ts +122 -0
  139. package/src/core/structure.ts +348 -0
  140. package/src/domain/architecture-config.ts +206 -0
  141. package/src/domain/architecture-error.ts +43 -0
  142. package/src/domain/violation.ts +24 -0
  143. package/src/index.ts +28 -0
  144. package/src/infrastructure/file-system-fake.ts +6 -0
  145. package/src/infrastructure/file-system-live.ts +20 -0
  146. package/src/infrastructure/module-resolver-fake.ts +29 -0
  147. package/src/infrastructure/module-resolver-live.ts +100 -0
  148. package/src/manifest/compile.ts +736 -0
  149. package/src/manifest/glob.ts +108 -0
  150. package/src/manifest/manifest.ts +178 -0
  151. package/src/ports/file-system.ts +5 -0
  152. package/src/ports/module-resolver.ts +23 -0
@@ -0,0 +1,120 @@
1
+ import * as Result from "effect/Result";
2
+ import * as Schema from "effect/Schema";
3
+ import { ResolveConfig } from "../domain/architecture-config.js";
4
+ import { ConfigInvalid } from "../domain/architecture-error.js";
5
+ // A manifest is a tree of nodes keyed by path pattern, where everything the
6
+ // architecture says about a part of the tree is written at that part of the tree.
7
+ // A node is a folder (it has `children` and/or `files`) or a file (its key names
8
+ // a filename pattern).
9
+ //
10
+ // The default is tight: a folder admits only the children it lists, and a node
11
+ // may import only what it or an ancestor allows. Laxity is opted into, by name,
12
+ // at the node that wants it.
13
+ const Globs = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
14
+ // A prohibition. `match`/`matchNot` describe the target; `except` names the
15
+ // importers it does not apply to. The exemption is declared by the author of the
16
+ // prohibition, in the same breath — a descendant still cannot opt itself out.
17
+ const Denial = Schema.Struct({
18
+ match: Globs,
19
+ matchNot: Schema.optionalKey(Globs),
20
+ except: Schema.optionalKey(Globs),
21
+ message: Schema.String,
22
+ });
23
+ // Outbound: what this part of the tree may reach.
24
+ const Imports = Schema.Struct({
25
+ message: Schema.optionalKey(Schema.String),
26
+ // Repo-relative globs, after alias expansion. A target matching none of these
27
+ // is the violation — so widening is something you do by name, in the open.
28
+ allow: Schema.optionalKey(Globs),
29
+ // npm package names. Omit to inherit; `[]` to forbid every external.
30
+ external: Schema.optionalKey(Schema.Array(Schema.String)),
31
+ // Checked before `allow`, and wins over it. This is how a rule keeps a
32
+ // specific, better message for a mistake the allowlist would also catch.
33
+ deny: Schema.optionalKey(Schema.Array(Denial)),
34
+ // The one widening mechanism: stop inheriting ancestors' allowances. Inherited
35
+ // `deny` entries are unaffected — a prohibition only ever accumulates.
36
+ reset: Schema.optionalKey(Schema.Boolean),
37
+ // "This tier has no allowlist yet; only the prohibitions below apply." Implies
38
+ // `reset`. Required whenever a node states `imports` without an `allow`, so
39
+ // that an untightened tier is a sentence someone wrote rather than a gap in
40
+ // the config — and so `grep unrestricted` is the adoption backlog.
41
+ unrestricted: Schema.optionalKey(Schema.Boolean),
42
+ });
43
+ // Inbound: who may reach this part of the tree. Four of the rules governing
44
+ // `domain/` are of this shape — "*.root-ops.ts is private to command handlers" —
45
+ // and stating them here is what stops them being a distant rule with a growing
46
+ // exclusion list on its `from` side.
47
+ const ImportedBy = Schema.Struct({
48
+ message: Schema.String,
49
+ allow: Globs,
50
+ // Targets inside this node's subtree the restriction does not cover — the
51
+ // module barrel is the obvious one: a module is private except through it.
52
+ matchNot: Schema.optionalKey(Globs),
53
+ });
54
+ // What shape the variable part of a name must have. A folder's `children` keys
55
+ // already say which stereotypes it admits; this says what the concept name in
56
+ // front of the stereotype may look like, which is the degree of freedom a
57
+ // taxonomy alone leaves open.
58
+ //
59
+ // `like` names an ancestor capture the name must equal — a subdomain folder's
60
+ // root is named after the folder, and nothing else could say so.
61
+ const Naming = Schema.Union([
62
+ Schema.Literals(["kebab-case", "camelCase", "PascalCase", "snake_case"]),
63
+ Schema.Struct({ regex: Schema.String, message: Schema.optionalKey(Schema.String) }),
64
+ Schema.Struct({ like: Schema.String, message: Schema.optionalKey(Schema.String) }),
65
+ ]);
66
+ const Members = Schema.Struct({
67
+ message: Schema.String,
68
+ subject: Schema.Literals(["type-members", "calls"]),
69
+ in: Schema.optionalKey(Globs),
70
+ match: Schema.optionalKey(Globs),
71
+ matchNot: Schema.optionalKey(Globs),
72
+ allow: Schema.optionalKey(Globs),
73
+ });
74
+ // Which importers may name a given exported symbol. A path rule cannot say
75
+ // this: every importer of a barrel resolves to the same file, so only the
76
+ // imported name separates a bus factory from the Tag beside it.
77
+ const ExportRestriction = Schema.Struct({
78
+ name: Schema.String,
79
+ message: Schema.String,
80
+ // The module the symbols come from, matched against its RESOLVED path.
81
+ module: Globs,
82
+ // Exact exported names. Omit to mean "any named import from that module",
83
+ // which is how a rule bans a binding form rather than a name.
84
+ symbols: Schema.optionalKey(Schema.Array(Schema.String)),
85
+ except: Schema.optionalKey(Globs),
86
+ fix: Schema.optionalKey(Schema.Literal("subpath-namespace-import")),
87
+ });
88
+ const ManifestNodeSchema = Schema.suspend(() => Schema.Struct({
89
+ message: Schema.optionalKey(Schema.String),
90
+ partial: Schema.optionalKey(Schema.Boolean),
91
+ layout: Schema.optionalKey(Schema.Literal("open")),
92
+ name: Schema.optionalKey(Naming),
93
+ imports: Schema.optionalKey(Imports),
94
+ importedBy: Schema.optionalKey(ImportedBy),
95
+ members: Schema.optionalKey(Schema.Array(Members)),
96
+ requires: Schema.optionalKey(Schema.Array(Schema.String)),
97
+ requiresNot: Schema.optionalKey(Schema.Array(Schema.String)),
98
+ children: Schema.optionalKey(Schema.Record(Schema.String, ManifestNodeSchema)),
99
+ }));
100
+ export const Manifest = Schema.Struct({
101
+ // How an import specifier becomes a file. Every pattern below is matched
102
+ // against a resolved path, so this is what makes the rest of the file mean
103
+ // anything.
104
+ resolve: ResolveConfig,
105
+ baseline: Schema.optionalKey(Schema.String),
106
+ // Prohibitions that hold everywhere, declared once. `not-to-spec` and the
107
+ // "this driver lives in one package" rules are statements about the whole
108
+ // repo, not about a tier — putting them on every tree root would be six
109
+ // copies and a seventh forgotten.
110
+ deny: Schema.optionalKey(Schema.Array(Denial)),
111
+ exports: Schema.optionalKey(Schema.Array(ExportRestriction)),
112
+ // Shorthands expanded in every glob, so a pattern reads the way the repo's own
113
+ // imports do rather than repeating `packages/server/src` on every line.
114
+ aliases: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
115
+ tree: Schema.Record(Schema.String, ManifestNodeSchema),
116
+ });
117
+ export const globsOf = (globs) => typeof globs === "string" ? [globs] : globs;
118
+ const decode = Schema.decodeUnknownResult(Manifest);
119
+ export const decodeManifest = (configPath, input) => Result.mapError(decode(input), (issue) => new ConfigInvalid({ configPath, detail: String(issue) }));
120
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/manifest/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=file-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-system.js","sourceRoot":"","sources":["../../../src/ports/file-system.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=module-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-resolver.js","sourceRoot":"","sources":["../../../src/ports/module-resolver.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "oxlint-architecture-rules",
3
+ "version": "0.1.0-beta.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Architecture policy as configuration, enforced as one oxlint plugin: import boundaries over resolved module paths, restricted export sites, folder taxonomy with sibling parity, and declared-member allowlists.",
7
+ "author": "zacharyweidenbach",
8
+ "keywords": [
9
+ "oxlint",
10
+ "oxlint-plugin",
11
+ "architecture",
12
+ "lint",
13
+ "import-boundaries",
14
+ "hexagonal",
15
+ "monorepo",
16
+ "typescript"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/dataquail/oxlint-utils.git",
24
+ "directory": "packages/oxlint-architecture-rules"
25
+ },
26
+ "homepage": "https://dataquail.github.io/oxlint-utils",
27
+ "bugs": {
28
+ "url": "https://github.com/dataquail/oxlint-utils/issues"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./build/dts/index.d.ts",
33
+ "default": "./build/esm/index.js"
34
+ },
35
+ "./plugin": {
36
+ "types": "./build/dts/adapters/oxlint/plugin.d.ts",
37
+ "default": "./build/esm/adapters/oxlint/plugin.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "types": "./build/dts/index.d.ts",
42
+ "bin": {
43
+ "architecture": "./build/esm/adapters/cli/main.js"
44
+ },
45
+ "files": [
46
+ "build/dts",
47
+ "build/esm",
48
+ "src",
49
+ "!src/**/*.test.ts"
50
+ ],
51
+ "dependencies": {
52
+ "effect": "4.0.0-beta.94",
53
+ "typescript": "^5.9.3",
54
+ "unrs-resolver": "^1.12.2"
55
+ },
56
+ "peerDependencies": {
57
+ "oxlint": ">=1.77.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^25.9.4",
61
+ "oxlint": "1.77.0"
62
+ },
63
+ "nx": {
64
+ "includedScripts": []
65
+ },
66
+ "scripts": {
67
+ "build": "tsc -b tsconfig.build.json",
68
+ "check": "tsc -b tsconfig.json",
69
+ "clean": "rm -rf build .tsbuildinfo",
70
+ "test": "vitest run",
71
+ "coverage": "vitest run --coverage"
72
+ }
73
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import * as Cause from "effect/Cause";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Exit from "effect/Exit";
5
+
6
+ import { run } from "./run.js";
7
+
8
+ const exit = await Effect.runPromiseExit(run(process.cwd(), process.argv.slice(2)));
9
+
10
+ if (Exit.isFailure(exit)) {
11
+ // The expected failures carry their own sentence; anything else is a defect
12
+ // and deserves the full cause.
13
+ const squashed = Cause.squash(exit.cause);
14
+ const message =
15
+ typeof squashed === "object" && squashed !== null && "message" in squashed
16
+ ? String(squashed.message)
17
+ : Cause.pretty(exit.cause);
18
+ process.stderr.write(`\n${message}\n`);
19
+ process.exitCode = 1;
20
+ }
@@ -0,0 +1,270 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import * as Effect from "effect/Effect";
5
+ import * as Result from "effect/Result";
6
+
7
+ import {
8
+ type Baseline,
9
+ baselineOf,
10
+ decodeBaseline,
11
+ EMPTY_BASELINE,
12
+ serializeBaseline,
13
+ staleEntriesOf,
14
+ unbaselined,
15
+ } from "../../core/baseline.js";
16
+ import { evaluateSelectedBindings, exportRulesSelecting } from "../../core/exports.js";
17
+ import { evaluateSelectedEdge, rulesSelecting } from "../../core/imports.js";
18
+ import { evaluateMemberSite, memberRulesSelecting } from "../../core/members.js";
19
+ import { evaluateStructure, requiredSiblingsOf } from "../../core/structure.js";
20
+ import { fingerprintOf, formatMessage, type Violation } from "../../domain/violation.js";
21
+ import { type LoadedPolicy, loadPolicy } from "../oxlint/config-loader.js";
22
+ import { sourceFactsOf } from "./source-facts.js";
23
+ import { listSourceFiles } from "./source-files.js";
24
+
25
+ // The policy, run with no linter in the loop.
26
+ //
27
+ // oxlint's JS plugin API is alpha, and a policy that can only be evaluated by
28
+ // one alpha host is a policy with a single point of failure. This adapter is the
29
+ // second way to ask the same question — and the only way to write a baseline,
30
+ // since that needs every finding at once rather than one file at a time.
31
+ //
32
+ // It covers all four families. The two that need a syntax tree read TypeScript's
33
+ // rather than oxlint's; both adapters meet at the same vocabulary — a specifier,
34
+ // a binding, a member site — so they answer to the same core rather than to each
35
+ // other.
36
+
37
+ export type CliFailure = { readonly _tag: "CliFailure"; readonly message: string };
38
+
39
+ const fail = (message: string): CliFailure => ({ _tag: "CliFailure", message });
40
+
41
+ export type Findings = {
42
+ readonly violations: ReadonlyArray<Violation>;
43
+ readonly unresolved: ReadonlyArray<string>;
44
+ readonly files: number;
45
+ };
46
+
47
+ export const collectFindings = (policy: LoadedPolicy, roots: ReadonlyArray<string>): Findings => {
48
+ const files = listSourceFiles(policy.repoRoot, roots);
49
+ const violations: Array<Violation> = [];
50
+ const unresolved: Array<string> = [];
51
+
52
+ for (const file of files) {
53
+ for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
54
+ violations.push(violation);
55
+ }
56
+
57
+ const selectedImports = rulesSelecting(policy.importRules, file);
58
+ const selectedExports = exportRulesSelecting(policy.exportRules, file);
59
+ const selectedMembers = memberRulesSelecting(policy.memberRules, file);
60
+ if (selectedImports.length + selectedExports.length + selectedMembers.length === 0) continue;
61
+
62
+ const facts = sourceFactsOf(policy.repoRoot, file);
63
+
64
+ for (const site of facts.memberSites) {
65
+ for (const violation of evaluateMemberSite(selectedMembers, site)) violations.push(violation);
66
+ }
67
+
68
+ for (const specifier of facts.specifiers) {
69
+ const edge = { importer: file, specifier };
70
+
71
+ const imported = evaluateSelectedEdge(selectedImports, policy.resolver, edge);
72
+ if (Result.isFailure(imported)) {
73
+ if (policy.config.resolve.unresolved === "off") continue;
74
+ if (policy.ignoreUnresolved.some((pattern) => pattern.test(specifier))) continue;
75
+ unresolved.push(`${file} → ${specifier} (${imported.failure.detail})`);
76
+ continue;
77
+ }
78
+ for (const violation of imported.success) violations.push(violation);
79
+
80
+ const bound = facts.bindings.get(specifier) ?? [];
81
+ const exported = evaluateSelectedBindings(selectedExports, policy.resolver, {
82
+ ...edge,
83
+ bindings: bound,
84
+ });
85
+ if (!Result.isFailure(exported)) {
86
+ for (const { violation } of exported.success) violations.push(violation);
87
+ }
88
+ }
89
+ }
90
+
91
+ return { violations, unresolved, files: files.length };
92
+ };
93
+
94
+ const baselinePathOf = (policy: LoadedPolicy): string | null =>
95
+ policy.config.baseline === undefined
96
+ ? null
97
+ : path.resolve(policy.repoRoot, policy.config.baseline);
98
+
99
+ const readBaseline = (policy: LoadedPolicy): Baseline => {
100
+ const at = baselinePathOf(policy);
101
+ if (at === null) return EMPTY_BASELINE;
102
+ try {
103
+ return decodeBaseline(JSON.parse(readFileSync(at, "utf8")) as unknown);
104
+ } catch {
105
+ return EMPTY_BASELINE;
106
+ }
107
+ };
108
+
109
+ const report = (lines: ReadonlyArray<string>): Effect.Effect<void> =>
110
+ Effect.sync(() => {
111
+ for (const line of lines) process.stdout.write(`${line}\n`);
112
+ });
113
+
114
+ const describe = (violation: Violation): string =>
115
+ ` ${violation.file}\n ${formatMessage(violation)}`;
116
+
117
+ export const check = (
118
+ policy: LoadedPolicy,
119
+ roots: ReadonlyArray<string>,
120
+ ): Effect.Effect<void, CliFailure> =>
121
+ Effect.gen(function* () {
122
+ const findings = collectFindings(policy, roots);
123
+ const baseline = readBaseline(policy);
124
+ const reportable = unbaselined(baseline, findings.violations);
125
+ const stale = staleEntriesOf(baseline, findings.violations);
126
+
127
+ yield* report(reportable.map(describe));
128
+ yield* report(findings.unresolved.map((one) => ` unresolved: ${one}`));
129
+
130
+ const carried = findings.violations.length - reportable.length;
131
+ yield* report([
132
+ "",
133
+ `${String(findings.files)} files, ${String(reportable.length)} violations` +
134
+ (carried > 0 ? `, ${String(carried)} carried by the baseline` : ""),
135
+ ]);
136
+
137
+ if (stale.length > 0) {
138
+ // The ratchet: a fixed violation must leave the baseline, or the floor
139
+ // never rises and the file stops describing anything real.
140
+ yield* report([
141
+ "",
142
+ `${String(stale.length)} baseline entries no longer fire. The code was fixed; prune them:`,
143
+ ...stale.map((entry) => ` ${entry}`),
144
+ "",
145
+ " architecture baseline # rewrites the file from what still fires",
146
+ ]);
147
+ return yield* Effect.fail(fail("stale baseline entries"));
148
+ }
149
+
150
+ if (reportable.length > 0 || findings.unresolved.length > 0) {
151
+ return yield* Effect.fail(fail("architecture violations"));
152
+ }
153
+ });
154
+
155
+ export const writeBaseline = (
156
+ policy: LoadedPolicy,
157
+ roots: ReadonlyArray<string>,
158
+ ): Effect.Effect<void, CliFailure> =>
159
+ Effect.gen(function* () {
160
+ const at = baselinePathOf(policy);
161
+ if (at === null) {
162
+ return yield* Effect.fail(
163
+ fail("this policy declares no `baseline` path, so there is nowhere to write one"),
164
+ );
165
+ }
166
+
167
+ const findings = collectFindings(policy, roots);
168
+ const baseline = baselineOf(findings.violations);
169
+ yield* Effect.sync(() => {
170
+ writeFileSync(at, serializeBaseline(baseline));
171
+ });
172
+ yield* report([
173
+ `${String(baseline.entries.length)} violations recorded in ${path.relative(policy.repoRoot, at)}.`,
174
+ "Each one is debt the policy is carrying. Fixing one means deleting its line.",
175
+ ]);
176
+ });
177
+
178
+ // The question a tree config makes harder to answer than a flat one: given a
179
+ // file, what governs it? A flat config you grep; a tree you have to walk.
180
+ export const explain = (policy: LoadedPolicy, file: string): Effect.Effect<void, CliFailure> =>
181
+ Effect.gen(function* () {
182
+ const relative = path.relative(policy.repoRoot, path.resolve(policy.repoRoot, file));
183
+ const selected = rulesSelecting(policy.importRules, relative);
184
+
185
+ // An allowlist rule names no `to` — it fires when the target matches none of
186
+ // its patterns. A prohibition names one.
187
+ const allowlists = selected.filter(([rule]) => rule.to.length === 0 && rule.toNot.length > 0);
188
+ const prohibitions = selected.filter(([rule]) => rule.to.length > 0);
189
+
190
+ const owed = policy.structure.parity
191
+ .filter(
192
+ (rule) =>
193
+ rule.file.some((pattern) => pattern.test(relative)) &&
194
+ !rule.fileNot.some((pattern) => pattern.test(relative)),
195
+ )
196
+ .flatMap((rule) => requiredSiblingsOf(rule, relative));
197
+
198
+ const governing = policy.structure.folders.filter((rule) =>
199
+ rule.folder.some((pattern) => pattern.test(path.dirname(relative))),
200
+ );
201
+
202
+ const naming = policy.structure.naming.filter(
203
+ (rule) =>
204
+ rule.file.some((pattern) => pattern.test(relative)) &&
205
+ !rule.fileNot.some((pattern) => pattern.test(relative)),
206
+ );
207
+
208
+ const firstSentence = (message: string) => `${message.split(". ")[0] ?? message}.`;
209
+
210
+ yield* report([
211
+ relative,
212
+ "",
213
+ allowlists.length === 0
214
+ ? " may import: anything (no tier above this file states an allowlist)"
215
+ : " may import:",
216
+ ...allowlists.flatMap(([rule]) => [
217
+ ` — ${rule.name}`,
218
+ ...rule.toNot.map((pattern) => ` ${pattern}`),
219
+ ]),
220
+ "",
221
+ " may not import:",
222
+ ...(prohibitions.length === 0
223
+ ? [" (nothing beyond the allowlist above)"]
224
+ : prohibitions.map(([rule]) => ` ${rule.name} — ${firstSentence(rule.message)}`)),
225
+ "",
226
+ ` lives in: ${governing.length === 0 ? "a folder no rule governs" : governing.map((rule) => rule.name).join(", ")}`,
227
+ ...(naming.length === 0
228
+ ? []
229
+ : [
230
+ " is named by:",
231
+ ...naming.map(
232
+ (rule) =>
233
+ ` ${rule.name} — ${rule.sameAs !== null ? "its folder's own name" : (rule.convention?.source ?? "")}`,
234
+ ),
235
+ ]),
236
+ ...(owed.length === 0 ? [] : [" owes:", ...owed.map((one) => ` ${one}`)]),
237
+ ]);
238
+ });
239
+
240
+ export const run = (
241
+ repoRoot: string,
242
+ argv: ReadonlyArray<string>,
243
+ ): Effect.Effect<void, CliFailure> =>
244
+ Effect.gen(function* () {
245
+ const [command = "check", ...rest] = argv;
246
+ const policy = yield* Effect.tryPromise({
247
+ try: () => loadPolicy(repoRoot),
248
+ catch: (cause) => fail(String(cause)),
249
+ });
250
+
251
+ const roots = rest.length > 0 ? rest : ["packages"];
252
+
253
+ switch (command) {
254
+ case "check":
255
+ return yield* check(policy, roots);
256
+ case "baseline":
257
+ return yield* writeBaseline(policy, roots);
258
+ case "explain": {
259
+ const [file] = rest;
260
+ if (file === undefined) return yield* Effect.fail(fail("explain needs a file path"));
261
+ return yield* explain(policy, file);
262
+ }
263
+ default:
264
+ return yield* Effect.fail(
265
+ fail(`unknown command "${command}". Try: check | baseline | explain <file>`),
266
+ );
267
+ }
268
+ });
269
+
270
+ export const fingerprint = fingerprintOf;
@@ -0,0 +1,137 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import ts from "typescript";
5
+
6
+ import type { Binding } from "../../core/exports.js";
7
+ import type { MemberSite } from "../../core/members.js";
8
+
9
+ // Everything the policy needs to know about one file, read once.
10
+ //
11
+ // The oxlint adapter gets these facts from oxlint's AST; this one reads them
12
+ // from TypeScript's. They meet at the same vocabulary — a specifier, a binding,
13
+ // a member site — which is what keeps the two adapters answerable to the same
14
+ // core rather than to each other.
15
+ export type SourceFacts = {
16
+ // One entry per import edge, in source order. An edge appears once even if it
17
+ // carries several bindings.
18
+ readonly specifiers: ReadonlyArray<string>;
19
+ // The names pulled across each edge, keyed by specifier.
20
+ readonly bindings: ReadonlyMap<string, ReadonlyArray<Binding>>;
21
+ readonly memberSites: ReadonlyArray<MemberSite>;
22
+ };
23
+
24
+ const scriptKindOf = (file: string): ts.ScriptKind =>
25
+ file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
26
+
27
+ const nameOf = (node: ts.PropertyName | ts.ModuleExportName | undefined): string | null => {
28
+ if (node === undefined) return null;
29
+ if (ts.isIdentifier(node)) return node.text;
30
+ if (ts.isStringLiteral(node)) return node.text;
31
+ return null;
32
+ };
33
+
34
+ const bindingsOfImportClause = (clause: ts.ImportClause | undefined): ReadonlyArray<Binding> => {
35
+ if (clause === undefined) return [];
36
+ const found: Array<Binding> = [];
37
+ if (clause.name !== undefined) found.push({ symbol: "default", kind: "default" });
38
+ const bindings = clause.namedBindings;
39
+ if (bindings !== undefined) {
40
+ if (ts.isNamespaceImport(bindings)) found.push({ symbol: "*", kind: "namespace" });
41
+ else {
42
+ for (const element of bindings.elements) {
43
+ const symbol = nameOf(element.propertyName ?? element.name);
44
+ if (symbol !== null) found.push({ symbol, kind: "named" });
45
+ }
46
+ }
47
+ }
48
+ return found;
49
+ };
50
+
51
+ // `export { a } from "m"` — `propertyName` is the name in the source module when
52
+ // the export is renamed, so it is the one the policy is about.
53
+ const bindingsOfExportClause = (
54
+ clause: ts.NamedExportBindings | undefined,
55
+ ): ReadonlyArray<Binding> => {
56
+ if (clause === undefined || ts.isNamespaceExport(clause)) return [];
57
+ const found: Array<Binding> = [];
58
+ for (const element of clause.elements) {
59
+ const symbol = nameOf(element.propertyName ?? element.name);
60
+ if (symbol !== null) found.push({ symbol, kind: "named" });
61
+ }
62
+ return found;
63
+ };
64
+
65
+ const calleeNameOf = (expression: ts.LeftHandSideExpression): string | null => {
66
+ if (ts.isIdentifier(expression)) return expression.text;
67
+ if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name)) {
68
+ return expression.name.text;
69
+ }
70
+ return null;
71
+ };
72
+
73
+ export const sourceFactsOf = (repoRoot: string, file: string): SourceFacts => {
74
+ const text = readFileSync(path.join(repoRoot, file), "utf8");
75
+ const parsed = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKindOf(file));
76
+
77
+ const specifiers: Array<string> = [];
78
+ const bindings = new Map<string, Array<Binding>>();
79
+ const memberSites: Array<MemberSite> = [];
80
+
81
+ const record = (specifier: string, found: ReadonlyArray<Binding>): void => {
82
+ if (!bindings.has(specifier)) {
83
+ specifiers.push(specifier);
84
+ bindings.set(specifier, []);
85
+ }
86
+ const existing = bindings.get(specifier);
87
+ if (existing !== undefined) for (const binding of found) existing.push(binding);
88
+ };
89
+
90
+ const visit = (node: ts.Node): void => {
91
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
92
+ // A side-effect import carries no bindings but is still an edge — the
93
+ // `import "server-only"` form a regex cannot see.
94
+ record(node.moduleSpecifier.text, bindingsOfImportClause(node.importClause));
95
+ } else if (
96
+ ts.isExportDeclaration(node) &&
97
+ node.moduleSpecifier !== undefined &&
98
+ ts.isStringLiteral(node.moduleSpecifier)
99
+ ) {
100
+ record(node.moduleSpecifier.text, bindingsOfExportClause(node.exportClause));
101
+ } else if (
102
+ ts.isImportEqualsDeclaration(node) &&
103
+ ts.isExternalModuleReference(node.moduleReference) &&
104
+ ts.isStringLiteral(node.moduleReference.expression)
105
+ ) {
106
+ record(node.moduleReference.expression.text, []);
107
+ } else if (ts.isCallExpression(node)) {
108
+ const [first] = node.arguments;
109
+ const isModuleCall =
110
+ node.expression.kind === ts.SyntaxKind.ImportKeyword ||
111
+ (ts.isIdentifier(node.expression) && node.expression.text === "require");
112
+ if (isModuleCall && first !== undefined && ts.isStringLiteral(first)) {
113
+ record(first.text, []);
114
+ }
115
+
116
+ const callee = calleeNameOf(node.expression);
117
+ if (callee !== null) memberSites.push({ file, subject: "calls", name: callee });
118
+ } else if (ts.isTypeAliasDeclaration(node) && ts.isTypeLiteralNode(node.type)) {
119
+ const declaration = node.name.text;
120
+ for (const member of node.type.members) {
121
+ // A computed key is not a name a vocabulary rule can speak about.
122
+ if (!ts.isPropertySignature(member) && !ts.isMethodSignature(member)) continue;
123
+ if (ts.isComputedPropertyName(member.name)) continue;
124
+ const name = nameOf(member.name);
125
+ if (name !== null) {
126
+ memberSites.push({ file, subject: "type-members", name, in: declaration });
127
+ }
128
+ }
129
+ }
130
+
131
+ ts.forEachChild(node, visit);
132
+ };
133
+
134
+ visit(parsed);
135
+
136
+ return { specifiers, bindings, memberSites };
137
+ };