@goodbones/core 0.1.0-beta.1

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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/build/dts/core/baseline.d.ts +16 -0
  3. package/build/dts/core/baseline.d.ts.map +1 -0
  4. package/build/dts/core/coverage.d.ts +47 -0
  5. package/build/dts/core/coverage.d.ts.map +1 -0
  6. package/build/dts/core/exports.d.ts +38 -0
  7. package/build/dts/core/exports.d.ts.map +1 -0
  8. package/build/dts/core/graph.d.ts +40 -0
  9. package/build/dts/core/graph.d.ts.map +1 -0
  10. package/build/dts/core/imports.d.ts +29 -0
  11. package/build/dts/core/imports.d.ts.map +1 -0
  12. package/build/dts/core/members.d.ts +26 -0
  13. package/build/dts/core/members.d.ts.map +1 -0
  14. package/build/dts/core/patterns.d.ts +17 -0
  15. package/build/dts/core/patterns.d.ts.map +1 -0
  16. package/build/dts/core/structure.d.ts +48 -0
  17. package/build/dts/core/structure.d.ts.map +1 -0
  18. package/build/dts/core/surface.d.ts +39 -0
  19. package/build/dts/core/surface.d.ts.map +1 -0
  20. package/build/dts/domain/architecture-config.d.ts +322 -0
  21. package/build/dts/domain/architecture-config.d.ts.map +1 -0
  22. package/build/dts/domain/architecture-error.d.ts +35 -0
  23. package/build/dts/domain/architecture-error.d.ts.map +1 -0
  24. package/build/dts/domain/facts.d.ts +26 -0
  25. package/build/dts/domain/facts.d.ts.map +1 -0
  26. package/build/dts/domain/violation.d.ts +11 -0
  27. package/build/dts/domain/violation.d.ts.map +1 -0
  28. package/build/dts/index.d.ts +23 -0
  29. package/build/dts/index.d.ts.map +1 -0
  30. package/build/dts/infrastructure/fact-extractor-fake.d.ts +4 -0
  31. package/build/dts/infrastructure/fact-extractor-fake.d.ts.map +1 -0
  32. package/build/dts/infrastructure/file-system-fake.d.ts +3 -0
  33. package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -0
  34. package/build/dts/infrastructure/file-system-live.d.ts +3 -0
  35. package/build/dts/infrastructure/file-system-live.d.ts.map +1 -0
  36. package/build/dts/infrastructure/manifest-file.d.ts +3 -0
  37. package/build/dts/infrastructure/manifest-file.d.ts.map +1 -0
  38. package/build/dts/infrastructure/module-resolver-fake.d.ts +3 -0
  39. package/build/dts/infrastructure/module-resolver-fake.d.ts.map +1 -0
  40. package/build/dts/infrastructure/walk.d.ts +4 -0
  41. package/build/dts/infrastructure/walk.d.ts.map +1 -0
  42. package/build/dts/load/policy.d.ts +42 -0
  43. package/build/dts/load/policy.d.ts.map +1 -0
  44. package/build/dts/manifest/compile.d.ts +26 -0
  45. package/build/dts/manifest/compile.d.ts.map +1 -0
  46. package/build/dts/manifest/glob.d.ts +14 -0
  47. package/build/dts/manifest/glob.d.ts.map +1 -0
  48. package/build/dts/manifest/manifest.d.ts +208 -0
  49. package/build/dts/manifest/manifest.d.ts.map +1 -0
  50. package/build/dts/ports/fact-extractor.d.ts +5 -0
  51. package/build/dts/ports/fact-extractor.d.ts.map +1 -0
  52. package/build/dts/ports/file-system.d.ts +5 -0
  53. package/build/dts/ports/file-system.d.ts.map +1 -0
  54. package/build/dts/ports/language.d.ts +14 -0
  55. package/build/dts/ports/language.d.ts.map +1 -0
  56. package/build/dts/ports/module-resolver.d.ts +12 -0
  57. package/build/dts/ports/module-resolver.d.ts.map +1 -0
  58. package/build/dts/testing.d.ts +4 -0
  59. package/build/dts/testing.d.ts.map +1 -0
  60. package/build/esm/core/baseline.js +30 -0
  61. package/build/esm/core/baseline.js.map +1 -0
  62. package/build/esm/core/coverage.js +75 -0
  63. package/build/esm/core/coverage.js.map +1 -0
  64. package/build/esm/core/exports.js +102 -0
  65. package/build/esm/core/exports.js.map +1 -0
  66. package/build/esm/core/graph.js +259 -0
  67. package/build/esm/core/graph.js.map +1 -0
  68. package/build/esm/core/imports.js +103 -0
  69. package/build/esm/core/imports.js.map +1 -0
  70. package/build/esm/core/members.js +106 -0
  71. package/build/esm/core/members.js.map +1 -0
  72. package/build/esm/core/patterns.js +75 -0
  73. package/build/esm/core/patterns.js.map +1 -0
  74. package/build/esm/core/structure.js +257 -0
  75. package/build/esm/core/structure.js.map +1 -0
  76. package/build/esm/core/surface.js +137 -0
  77. package/build/esm/core/surface.js.map +1 -0
  78. package/build/esm/domain/architecture-config.js +312 -0
  79. package/build/esm/domain/architecture-config.js.map +1 -0
  80. package/build/esm/domain/architecture-error.js +41 -0
  81. package/build/esm/domain/architecture-error.js.map +1 -0
  82. package/build/esm/domain/facts.js +2 -0
  83. package/build/esm/domain/facts.js.map +1 -0
  84. package/build/esm/domain/violation.js +8 -0
  85. package/build/esm/domain/violation.js.map +1 -0
  86. package/build/esm/index.js +22 -0
  87. package/build/esm/index.js.map +1 -0
  88. package/build/esm/infrastructure/fact-extractor-fake.js +24 -0
  89. package/build/esm/infrastructure/fact-extractor-fake.js.map +1 -0
  90. package/build/esm/infrastructure/file-system-fake.js +10 -0
  91. package/build/esm/infrastructure/file-system-fake.js.map +1 -0
  92. package/build/esm/infrastructure/file-system-live.js +27 -0
  93. package/build/esm/infrastructure/file-system-live.js.map +1 -0
  94. package/build/esm/infrastructure/manifest-file.js +15 -0
  95. package/build/esm/infrastructure/manifest-file.js.map +1 -0
  96. package/build/esm/infrastructure/module-resolver-fake.js +33 -0
  97. package/build/esm/infrastructure/module-resolver-fake.js.map +1 -0
  98. package/build/esm/infrastructure/walk.js +45 -0
  99. package/build/esm/infrastructure/walk.js.map +1 -0
  100. package/build/esm/load/policy.js +207 -0
  101. package/build/esm/load/policy.js.map +1 -0
  102. package/build/esm/manifest/compile.js +706 -0
  103. package/build/esm/manifest/compile.js.map +1 -0
  104. package/build/esm/manifest/glob.js +77 -0
  105. package/build/esm/manifest/glob.js.map +1 -0
  106. package/build/esm/manifest/manifest.js +303 -0
  107. package/build/esm/manifest/manifest.js.map +1 -0
  108. package/build/esm/ports/fact-extractor.js +2 -0
  109. package/build/esm/ports/fact-extractor.js.map +1 -0
  110. package/build/esm/ports/file-system.js +2 -0
  111. package/build/esm/ports/file-system.js.map +1 -0
  112. package/build/esm/ports/language.js +2 -0
  113. package/build/esm/ports/language.js.map +1 -0
  114. package/build/esm/ports/module-resolver.js +2 -0
  115. package/build/esm/ports/module-resolver.js.map +1 -0
  116. package/build/esm/testing.js +7 -0
  117. package/build/esm/testing.js.map +1 -0
  118. package/package.json +61 -0
  119. package/src/core/baseline.ts +62 -0
  120. package/src/core/coverage.ts +154 -0
  121. package/src/core/exports.ts +171 -0
  122. package/src/core/graph.ts +331 -0
  123. package/src/core/imports.ts +160 -0
  124. package/src/core/members.ts +161 -0
  125. package/src/core/patterns.ts +116 -0
  126. package/src/core/structure.ts +349 -0
  127. package/src/core/surface.ts +181 -0
  128. package/src/domain/architecture-config.ts +370 -0
  129. package/src/domain/architecture-error.ts +59 -0
  130. package/src/domain/facts.ts +47 -0
  131. package/src/domain/violation.ts +24 -0
  132. package/src/index.ts +136 -0
  133. package/src/infrastructure/fact-extractor-fake.ts +28 -0
  134. package/src/infrastructure/file-system-fake.ts +14 -0
  135. package/src/infrastructure/file-system-live.ts +27 -0
  136. package/src/infrastructure/manifest-file.ts +17 -0
  137. package/src/infrastructure/module-resolver-fake.ts +39 -0
  138. package/src/infrastructure/walk.ts +54 -0
  139. package/src/load/policy.ts +344 -0
  140. package/src/manifest/compile.ts +1011 -0
  141. package/src/manifest/glob.ts +108 -0
  142. package/src/manifest/manifest.ts +408 -0
  143. package/src/ports/fact-extractor.ts +12 -0
  144. package/src/ports/file-system.ts +8 -0
  145. package/src/ports/language.ts +39 -0
  146. package/src/ports/module-resolver.ts +28 -0
  147. package/src/testing.ts +6 -0
@@ -0,0 +1,27 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import type { FileSystem } from "../ports/file-system.js";
5
+
6
+ // One lint run asks about the same sibling many times — every handler in a
7
+ // folder resolves the same `../../infrastructure/...` shape — so the answers are
8
+ // memoised for the life of the run.
9
+ export const makeFileSystemLive = (repoRoot: string): FileSystem => {
10
+ const cache = new Map<string, boolean>();
11
+ return {
12
+ exists: (repoRelativePath) => {
13
+ const cached = cache.get(repoRelativePath);
14
+ if (cached !== undefined) return cached;
15
+ const answer = existsSync(path.resolve(repoRoot, repoRelativePath));
16
+ cache.set(repoRelativePath, answer);
17
+ return answer;
18
+ },
19
+ readText: (repoRelativePath) => {
20
+ try {
21
+ return readFileSync(path.resolve(repoRoot, repoRelativePath), "utf8");
22
+ } catch {
23
+ return null;
24
+ }
25
+ },
26
+ };
27
+ };
@@ -0,0 +1,17 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ import { ConfigInvalid } from "../domain/architecture-error.js";
4
+
5
+ export const DEFAULT_CONFIG_FILENAME = "architecture.config.mjs";
6
+
7
+ // The manifest, as the file on disk states it, before any decoding. Today that
8
+ // is a JavaScript module and its default export; a manifest in another format
9
+ // changes this function and nothing behind it.
10
+ export const readManifestFile = async (configPath: string): Promise<unknown> => {
11
+ const module: unknown = await import(pathToFileURL(configPath).href).catch((cause: unknown) => {
12
+ throw new ConfigInvalid({ configPath, detail: String(cause) });
13
+ });
14
+ return typeof module === "object" && module !== null && "default" in module
15
+ ? module.default
16
+ : module;
17
+ };
@@ -0,0 +1,39 @@
1
+ import * as Result from "effect/Result";
2
+
3
+ import { ImportUnresolved } from "../domain/architecture-error.js";
4
+ import type { ModuleResolver, ResolvedTarget } from "../ports/module-resolver.js";
5
+
6
+ // Keyed by specifier alone: a core-evaluator test states the edges it is about
7
+ // and never the resolution algorithm, which has its own tests against the real
8
+ // resolver.
9
+ //
10
+ // A target may be staged as the full `ResolvedTarget`, or as a path alone — in
11
+ // which case its kind and package are read the way the TypeScript resolver
12
+ // would report them, so a test written in that language's vocabulary stays
13
+ // short. A test about another language stages the whole target.
14
+ export const makeModuleResolverFake = (
15
+ targets: Readonly<Record<string, string | ResolvedTarget>>,
16
+ ): ModuleResolver => ({
17
+ resolve: (fromFile, specifier) => {
18
+ const staged = targets[specifier];
19
+ if (staged === undefined) {
20
+ return Result.fail(
21
+ new ImportUnresolved({ fromFile, specifier, detail: "not staged in the fake" }),
22
+ );
23
+ }
24
+ return Result.succeed(typeof staged === "string" ? targetOfPath(staged) : staged);
25
+ },
26
+ });
27
+
28
+ // The npm layout, as the TypeScript pack reads it: the package is the segment
29
+ // after the LAST `node_modules/` (pnpm's store puts a second one in front).
30
+ // Restated here rather than imported, because the core does not depend on any
31
+ // language pack — and a test about another language stages the whole target.
32
+ const LAST_NODE_MODULES = /(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)/g;
33
+
34
+ const targetOfPath = (path: string): ResolvedTarget => {
35
+ if (path.startsWith("node:")) return { path, kind: "builtin" };
36
+ let pkg: string | undefined;
37
+ for (const match of path.matchAll(LAST_NODE_MODULES)) pkg = match[1];
38
+ return pkg === undefined ? { path, kind: "local" } : { path, kind: "external", package: pkg };
39
+ };
@@ -0,0 +1,54 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import type { Language } from "../ports/language.js";
5
+
6
+ // Folders no policy is written about and no linter visits. These are the right
7
+ // defaults for any language on this host: a package store, build output, the
8
+ // VCS, and a coverage report are never the architecture.
9
+ const SKIPPED = new Set([
10
+ "node_modules",
11
+ "build",
12
+ "dist",
13
+ ".next",
14
+ ".git",
15
+ "storybook-static",
16
+ "coverage",
17
+ ]);
18
+
19
+ // What the walker needs to know about a language: what a source file of it is
20
+ // called, and which of those are not source after all.
21
+ export type WalkedLanguage = Pick<Language, "extensions" | "ignoredFiles">;
22
+
23
+ // The source files under `roots`, repo-relative with forward slashes, sorted.
24
+ // Which files count as source is the languages' to say: the extension set is
25
+ // the union of theirs, and so is the set of files to step over. A root that
26
+ // names a file is that file, whatever its extension — `architecture check
27
+ // <file>` is a reasonable thing to type.
28
+ export const listSourceFiles = (
29
+ repoRoot: string,
30
+ roots: ReadonlyArray<string>,
31
+ languages: ReadonlyArray<WalkedLanguage>,
32
+ ): ReadonlyArray<string> => {
33
+ const extensions = new Set(languages.flatMap((language) => language.extensions));
34
+ const ignored = languages.flatMap((language) => language.ignoredFiles);
35
+ const isSource = (entry: string): boolean =>
36
+ extensions.has(path.extname(entry)) && !ignored.some((pattern) => pattern.test(entry));
37
+
38
+ const found: Array<string> = [];
39
+ const walk = (absolute: string): void => {
40
+ for (const entry of readdirSync(absolute)) {
41
+ if (SKIPPED.has(entry)) continue;
42
+ const child = path.join(absolute, entry);
43
+ if (statSync(child).isDirectory()) walk(child);
44
+ else if (isSource(entry))
45
+ found.push(path.relative(repoRoot, child).replaceAll(path.sep, "/"));
46
+ }
47
+ };
48
+ for (const root of roots) {
49
+ const absolute = path.resolve(repoRoot, root);
50
+ if (statSync(absolute).isDirectory()) walk(absolute);
51
+ else found.push(path.relative(repoRoot, absolute).replaceAll(path.sep, "/"));
52
+ }
53
+ return found.sort();
54
+ };
@@ -0,0 +1,344 @@
1
+ import * as Result from "effect/Result";
2
+
3
+ import {
4
+ type Baseline,
5
+ type BaselineFilter,
6
+ decodeBaseline,
7
+ EMPTY_BASELINE,
8
+ makeBaselineFilter,
9
+ } from "../core/baseline.js";
10
+ import {
11
+ type CompiledExportRule,
12
+ compileExportRules,
13
+ exportRulesFailingTheirProbe,
14
+ } from "../core/exports.js";
15
+ import {
16
+ type CompiledGraph,
17
+ compileGraphRules,
18
+ graphRulesFailingTheirProbe,
19
+ } from "../core/graph.js";
20
+ import {
21
+ type CompiledImportRule,
22
+ compileImportRules,
23
+ rulesFailingTheirProbe,
24
+ } from "../core/imports.js";
25
+ import {
26
+ type CompiledMemberRule,
27
+ compileMemberRules,
28
+ memberRulesFailingTheirProbe,
29
+ } from "../core/members.js";
30
+ import {
31
+ type CompiledStructure,
32
+ compileStructure,
33
+ structureRulesFailingTheirProbe,
34
+ } from "../core/structure.js";
35
+ import {
36
+ type CompiledSurfaceRule,
37
+ compileSurfaceRules,
38
+ surfaceRulesFailingTheirProbe,
39
+ } from "../core/surface.js";
40
+ import type { ResolveScope } from "../domain/architecture-config.js";
41
+ import {
42
+ ConfigInvalid,
43
+ ImportUnresolved,
44
+ type PatternInvalid,
45
+ } from "../domain/architecture-error.js";
46
+ import type { SourceFacts } from "../domain/facts.js";
47
+ import { type LoweredRules, lowerManifest } from "../manifest/compile.js";
48
+ import { decodeManifest, type Manifest } from "../manifest/manifest.js";
49
+ import type { FactExtractor } from "../ports/fact-extractor.js";
50
+ import type { FileSystem } from "../ports/file-system.js";
51
+ import type { Language } from "../ports/language.js";
52
+ import type { ModuleResolver } from "../ports/module-resolver.js";
53
+
54
+ // A manifest, read by a host, turned into the policy both adapters evaluate.
55
+ // Decoding, lowering, compiling and probing happen here, once, with whatever
56
+ // languages the host hands in — this tier never names one. The host reads the
57
+ // manifest file, constructs its language packs and the live file system, and
58
+ // hands them over; that is the whole of what a host has to know.
59
+
60
+ export type LoadedPolicy = {
61
+ readonly repoRoot: string;
62
+ readonly config: Manifest;
63
+ readonly importRules: ReadonlyArray<CompiledImportRule>;
64
+ readonly exportRules: ReadonlyArray<CompiledExportRule>;
65
+ readonly memberRules: ReadonlyArray<CompiledMemberRule>;
66
+ readonly surfaceRules: ReadonlyArray<CompiledSurfaceRule>;
67
+ // Evaluated by the CLI only; compiled and probed here so a vacuous one fails
68
+ // the plugin's load as well.
69
+ readonly graph: CompiledGraph;
70
+ readonly adoption: LoweredRules["adoption"];
71
+ readonly structure: CompiledStructure;
72
+ readonly fileSystem: FileSystem;
73
+ // The language packs this policy is evaluated with. The walker takes its
74
+ // extensions from them, and lowering the shape of its probes.
75
+ readonly languages: ReadonlyArray<Language>;
76
+ // Violations this repository is carrying while it adopts the policy. Applied
77
+ // at report time so a baselined finding costs nothing but a line in a file.
78
+ readonly baseline: BaselineFilter;
79
+ // Routes each file to the resolver of the scope that covers it.
80
+ readonly resolver: ModuleResolver;
81
+ // Routes each file to the extractor of the language whose scope covers it.
82
+ // The CLI reads every file through this; the plugin reads oxlint's tree.
83
+ readonly extractor: FactExtractor;
84
+ readonly ignoreUnresolved: ReadonlyArray<RegExp>;
85
+ // Deprecation notices from reading the manifest. The host prints them once.
86
+ readonly notices: ReadonlyArray<string>;
87
+ };
88
+
89
+ export type LoadPolicyInput = {
90
+ readonly repoRoot: string;
91
+ // Where the manifest came from, for the error that names it.
92
+ readonly configPath: string;
93
+ // The manifest as the host read it — a module's default export, a parsed
94
+ // document — before decoding.
95
+ readonly manifest: unknown;
96
+ readonly languages: ReadonlyArray<Language>;
97
+ readonly fileSystem: FileSystem;
98
+ };
99
+
100
+ const NOTHING: SourceFacts = {
101
+ specifiers: [],
102
+ bindings: new Map(),
103
+ memberSites: [],
104
+ exportSites: [],
105
+ };
106
+
107
+ type Route = {
108
+ readonly scope: ResolveScope;
109
+ readonly matches: RegExp;
110
+ readonly language: Language;
111
+ };
112
+
113
+ // Each scope, paired with the language it names. A scope naming a language no
114
+ // pack answers to is refused here: every rule about that scope would otherwise
115
+ // be evaluated against nothing.
116
+ const routesOf = (
117
+ configPath: string,
118
+ scopes: ReadonlyArray<ResolveScope>,
119
+ languages: ReadonlyArray<Language>,
120
+ ): Result.Result<ReadonlyArray<Route>, ConfigInvalid> => {
121
+ const routes: Array<Route> = [];
122
+ for (const scope of scopes) {
123
+ const language = languages.find((one) => one.id === scope.language);
124
+ if (language === undefined) {
125
+ return Result.fail(
126
+ new ConfigInvalid({
127
+ configPath,
128
+ detail:
129
+ `resolve scope ${JSON.stringify(scope.files)} names the language ` +
130
+ `"${scope.language}", and no language pack by that name is loaded ` +
131
+ `(loaded: ${languages.length === 0 ? "none" : languages.map((one) => one.id).join(", ")}).`,
132
+ }),
133
+ );
134
+ }
135
+ routes.push({ scope, matches: new RegExp(scope.files), language });
136
+ }
137
+ return Result.succeed(routes);
138
+ };
139
+
140
+ const routeFor = (routes: ReadonlyArray<Route>, file: string): Route | undefined =>
141
+ routes.find((route) => route.matches.test(file));
142
+
143
+ // One resolver per scope, built by the scope's language, behind one port that
144
+ // picks the scope by the importing file.
145
+ const makeRouter = (
146
+ configPath: string,
147
+ repoRoot: string,
148
+ routes: ReadonlyArray<Route>,
149
+ ): Result.Result<ModuleResolver, ConfigInvalid> => {
150
+ const resolvers: Array<readonly [Route, ModuleResolver]> = [];
151
+ for (const route of routes) {
152
+ const resolver = route.language.makeResolver(repoRoot, route.scope);
153
+ if (Result.isFailure(resolver)) {
154
+ return Result.fail(new ConfigInvalid({ configPath, detail: resolver.failure.message }));
155
+ }
156
+ resolvers.push([route, resolver.success]);
157
+ }
158
+ return Result.succeed({
159
+ resolve: (fromFile, specifier) => {
160
+ const found = resolvers.find(([route]) => route.matches.test(fromFile));
161
+ if (found === undefined) {
162
+ return Result.fail(
163
+ new ImportUnresolved({
164
+ fromFile,
165
+ specifier,
166
+ detail: "no resolve scope in the architecture config matches this file",
167
+ }),
168
+ );
169
+ }
170
+ return found[1].resolve(fromFile, specifier);
171
+ },
172
+ });
173
+ };
174
+
175
+ // A file is parsed by the language whose scope covers it. A file no scope
176
+ // covers reads as nothing — which, for a probe, is a failed probe with a
177
+ // message that says so.
178
+ const makeRoutingExtractor = (routes: ReadonlyArray<Route>): FactExtractor => ({
179
+ factsOf: (file, text) =>
180
+ routeFor(routes, file)?.language.extractor.factsOf(file, text) ?? NOTHING,
181
+ });
182
+
183
+ // The baseline is read through the port, so this tier touches no file itself.
184
+ // An absent or unreadable one carries nothing, which is the safe direction:
185
+ // every violation reports.
186
+ const readBaseline = (fileSystem: FileSystem, at: string | undefined): Baseline => {
187
+ if (at === undefined) return EMPTY_BASELINE;
188
+ const text = fileSystem.readText(at);
189
+ if (text === null) return EMPTY_BASELINE;
190
+ try {
191
+ return decodeBaseline(JSON.parse(text) as unknown);
192
+ } catch {
193
+ return EMPTY_BASELINE;
194
+ }
195
+ };
196
+
197
+ // Anything wrong with the policy — a bad shape, an uncompilable pattern, a rule
198
+ // that cannot report its own probe — is refused. A policy that loaded with a
199
+ // gap reports nothing there and looks exactly like a clean codebase.
200
+ export const loadPolicy = (
201
+ input: LoadPolicyInput,
202
+ ): Result.Result<LoadedPolicy, ConfigInvalid | PatternInvalid> => {
203
+ const { configPath, fileSystem, languages, repoRoot } = input;
204
+
205
+ const decoded = decodeManifest(configPath, input.manifest);
206
+ if (Result.isFailure(decoded)) return Result.fail(decoded.failure);
207
+ const config = decoded.success.manifest;
208
+
209
+ const routes = routesOf(configPath, config.resolve.scopes, languages);
210
+ if (Result.isFailure(routes)) return Result.fail(routes.failure);
211
+
212
+ // The manifest is the authoring surface; these flat rules are the machine's.
213
+ // The languages tell lowering what a source file in each scope is called, so
214
+ // a synthetic probe is a file of the scope's language.
215
+ const rules = lowerManifest(config, languages);
216
+
217
+ // The ceilings. A tier that says "not tightened yet" is a sentence someone
218
+ // wrote; a ceiling on how many may say so is what keeps the backlog from
219
+ // becoming the architecture. Exceeding it is a policy that broke its own
220
+ // promise, and is refused like any other invalid policy.
221
+ const ceilings = [
222
+ ["unrestricted", rules.adoption.unrestricted, config.limits?.unrestricted],
223
+ ["partial", rules.adoption.partial, config.limits?.partial],
224
+ ] as const;
225
+ const exceeded = ceilings.flatMap(([label, nodes, ceiling]) =>
226
+ ceiling !== undefined && nodes.length > ceiling
227
+ ? [
228
+ `${label}: ${String(nodes.length)} nodes against a ceiling of ${String(ceiling)} (${nodes.join(", ")})`,
229
+ ]
230
+ : [],
231
+ );
232
+ if (exceeded.length > 0) {
233
+ return Result.fail(
234
+ new ConfigInvalid({
235
+ configPath,
236
+ detail:
237
+ `the policy exceeds its own adoption ceiling — ${exceeded.join("; ")}. ` +
238
+ `Tighten a tier, or raise the ceiling in \`limits\` on purpose.`,
239
+ }),
240
+ );
241
+ }
242
+
243
+ const importRules = compileImportRules(rules.imports);
244
+ if (Result.isFailure(importRules)) return Result.fail(importRules.failure);
245
+
246
+ const exportRules = compileExportRules(rules.exports);
247
+ if (Result.isFailure(exportRules)) return Result.fail(exportRules.failure);
248
+
249
+ // A fix is a rewrite in one language's module syntax. A rule naming one that
250
+ // no loaded language implements would report as fixable and never fix.
251
+ const unfixable = exportRules.success.filter((rule) => {
252
+ const fix = rule.fix;
253
+ return fix !== null && !languages.some((one) => one.fixes.includes(fix));
254
+ });
255
+ if (unfixable.length > 0) {
256
+ return Result.fail(
257
+ new ConfigInvalid({
258
+ configPath,
259
+ detail:
260
+ `these exports rules name a fix no loaded language implements: ` +
261
+ unfixable.map((rule) => `${rule.name} (${rule.fix ?? ""})`).join(", ") +
262
+ `. Drop the fix, or load a language pack that carries it.`,
263
+ }),
264
+ );
265
+ }
266
+
267
+ const memberRules = compileMemberRules(rules.members);
268
+ if (Result.isFailure(memberRules)) return Result.fail(memberRules.failure);
269
+
270
+ const surfaceRules = compileSurfaceRules(rules.surface);
271
+ if (Result.isFailure(surfaceRules)) return Result.fail(surfaceRules.failure);
272
+
273
+ const graph = compileGraphRules(rules.graph);
274
+ if (Result.isFailure(graph)) return Result.fail(graph.failure);
275
+
276
+ const structure = compileStructure(rules.structure);
277
+ if (Result.isFailure(structure)) return Result.fail(structure.failure);
278
+
279
+ // A probe carrying a source snippet is parsed by the extractor of the
280
+ // language whose scope covers the probe's file — the same extractor the CLI
281
+ // reads that file through. The plugin reads through oxlint's tree instead,
282
+ // and the parity suite is what holds that tree to this one.
283
+ const extractor = makeRoutingExtractor(routes.success);
284
+ const parsedBy = (rule: { readonly probe: { readonly from: string } }): string => {
285
+ const route = routeFor(routes.success, rule.probe.from);
286
+ return route === undefined
287
+ ? ` — no resolve scope covers its probe ${JSON.stringify(rule.probe.from)}, so no language parsed it`
288
+ : ` — probe parsed by ${route.language.id}, selected by the scope ${JSON.stringify(route.scope.files)}`;
289
+ };
290
+ const vacuous = [
291
+ ...rulesFailingTheirProbe(importRules.success).map((rule) => rule.name),
292
+ ...exportRulesFailingTheirProbe(exportRules.success, extractor).map(
293
+ (rule) => rule.name + (rule.probe.source === undefined ? "" : parsedBy(rule)),
294
+ ),
295
+ ...memberRulesFailingTheirProbe(memberRules.success, extractor).map(
296
+ (rule) => rule.name + (rule.probe.source === undefined ? "" : parsedBy(rule)),
297
+ ),
298
+ ...surfaceRulesFailingTheirProbe(surfaceRules.success, extractor).map(
299
+ (rule) => rule.name + (rule.probe.source === undefined ? "" : parsedBy(rule)),
300
+ ),
301
+ ...structureRulesFailingTheirProbe(structure.success),
302
+ ...graphRulesFailingTheirProbe(graph.success),
303
+ ];
304
+ if (vacuous.length > 0) {
305
+ return Result.fail(
306
+ new ConfigInvalid({
307
+ configPath,
308
+ detail:
309
+ `these rules do not report their own probe, so they enforce nothing: ` +
310
+ `${vacuous.join(", ")}. Fix the rule or its probe — ` +
311
+ `a rule that cannot flag a violation it was written for is worse than no rule. ` +
312
+ `A probe with a source snippet fails when the parser reads no site of that name ` +
313
+ `out of it; \`architecture facts\` shows what the parser reads.`,
314
+ }),
315
+ );
316
+ }
317
+
318
+ // A scope the language cannot build a resolver from — options it does not
319
+ // understand — is a policy whose rules would be evaluated against the wrong
320
+ // files, and is refused.
321
+ const resolver = makeRouter(configPath, repoRoot, routes.success);
322
+ if (Result.isFailure(resolver)) return Result.fail(resolver.failure);
323
+
324
+ return Result.succeed({
325
+ repoRoot,
326
+ config,
327
+ importRules: importRules.success,
328
+ exportRules: exportRules.success,
329
+ memberRules: memberRules.success,
330
+ surfaceRules: surfaceRules.success,
331
+ graph: graph.success,
332
+ adoption: rules.adoption,
333
+ structure: structure.success,
334
+ fileSystem,
335
+ languages,
336
+ baseline: makeBaselineFilter(readBaseline(fileSystem, config.baseline)),
337
+ resolver: resolver.success,
338
+ extractor,
339
+ ignoreUnresolved: (config.resolve.ignoreUnresolved ?? []).map(
340
+ (pattern: string) => new RegExp(pattern),
341
+ ),
342
+ notices: decoded.success.notices,
343
+ });
344
+ };