@goodbones/cli 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Data Quail
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.
@@ -0,0 +1,4 @@
1
+ import { type LoadedPolicy } from "@goodbones/core";
2
+ export type { LoadedPolicy } from "@goodbones/core";
3
+ export declare const loadPolicyFromFile: (repoRoot: string, configFilename?: string) => Promise<LoadedPolicy>;
4
+ //# sourceMappingURL=config-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAMpD,eAAO,MAAM,kBAAkB,aACnB,MAAM,mBACA,MAAM,KACrB,OAAO,CAAC,YAAY,CAWtB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Graph, ModuleResolver, SourceFacts } from "@goodbones/core";
2
+ export declare const buildGraph: (files: ReadonlyArray<string>, resolver: ModuleResolver, factsOf: (file: string) => SourceFacts) => Graph;
3
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAO1E,eAAO,MAAM,UAAU,UACd,aAAa,CAAC,MAAM,CAAC,YAClB,cAAc,WACf,CAAC,IAAI,EAAE,MAAM,KAAK,WAAW,KACrC,KAeF,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import { type Violation } from "@goodbones/core";
2
+ import * as Effect from "effect/Effect";
3
+ import { type LoadedPolicy } from "./config-loader.js";
4
+ export type CliFailure = {
5
+ readonly _tag: "CliFailure";
6
+ readonly message: string;
7
+ };
8
+ export type Findings = {
9
+ readonly violations: ReadonlyArray<Violation>;
10
+ readonly unresolved: ReadonlyArray<string>;
11
+ readonly files: number;
12
+ };
13
+ export declare const collectFindings: (policy: LoadedPolicy, roots: ReadonlyArray<string>) => Findings;
14
+ export declare const check: (policy: LoadedPolicy, roots: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
15
+ export declare const coverage: (policy: LoadedPolicy, roots: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
16
+ export declare const writeBaseline: (policy: LoadedPolicy, roots: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
17
+ export declare const explain: (policy: LoadedPolicy, file: string) => Effect.Effect<void, CliFailure>;
18
+ export declare const facts: (policy: LoadedPolicy, file: string, format?: "text" | "json") => Effect.Effect<void, CliFailure>;
19
+ export declare const run: (repoRoot: string, argv: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
20
+ export declare const fingerprint: (violation: Violation) => string;
21
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAGA,OAAO,EA2BL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,oBAAoB,CAAC;AAgB3E,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAInF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,eAAe,WAAY,YAAY,SAAS,aAAa,CAAC,MAAM,CAAC,KAAG,QAgFpF,CAAC;AAyBF,eAAO,MAAM,KAAK,WACR,YAAY,SACb,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAwD7B,CAAC;AAOL,eAAO,MAAM,QAAQ,WACX,YAAY,SACb,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAiC7B,CAAC;AAEL,eAAO,MAAM,aAAa,WAChB,YAAY,SACb,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAkB7B,CAAC;AAIL,eAAO,MAAM,OAAO,WAAY,YAAY,QAAQ,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAuFvF,CAAC;AAQL,eAAO,MAAM,KAAK,WACR,YAAY,QACd,MAAM,WACJ,MAAM,GAAG,MAAM,KACtB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAmD7B,CAAC;AAEL,eAAO,MAAM,GAAG,aACJ,MAAM,QACV,aAAa,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAqC7B,CAAC;AAEL,eAAO,MAAM,WAAW,kCAAgB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { FactExtractor, SourceFacts } from "@goodbones/core";
2
+ export type { SourceFacts } from "@goodbones/core";
3
+ export declare const sourceFactsOf: (repoRoot: string, file: string, extractor: FactExtractor) => SourceFacts;
4
+ //# sourceMappingURL=source-facts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-facts.d.ts","sourceRoot":"","sources":["../../src/source-facts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAElE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,eAAO,MAAM,aAAa,aACd,MAAM,QACV,MAAM,aACD,aAAa,KACvB,WAAuF,CAAC"}
@@ -0,0 +1,22 @@
1
+ import * as path from "node:path";
2
+ import { DEFAULT_CONFIG_FILENAME, loadPolicy, makeFileSystemLive, readManifestFile, } from "@goodbones/core";
3
+ import { typescriptLanguage } from "@goodbones/typescript";
4
+ import * as Result from "effect/Result";
5
+ // The CLI's composition root: read the manifest file, construct the language
6
+ // packs and the live file system, and hand them to the loader. The plugin has
7
+ // one of its own with the same three lines, on purpose — the two hosts share
8
+ // the core and never each other.
9
+ export const loadPolicyFromFile = async (repoRoot, configFilename = DEFAULT_CONFIG_FILENAME) => {
10
+ const configPath = path.resolve(repoRoot, configFilename);
11
+ const loaded = loadPolicy({
12
+ repoRoot,
13
+ configPath,
14
+ manifest: await readManifestFile(configPath),
15
+ languages: [typescriptLanguage()],
16
+ fileSystem: makeFileSystemLive(repoRoot),
17
+ });
18
+ if (Result.isFailure(loaded))
19
+ throw loaded.failure;
20
+ return loaded.success;
21
+ };
22
+ //# sourceMappingURL=config-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EACL,uBAAuB,EAEvB,UAAU,EACV,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAIxC,6EAA6E;AAC7E,8EAA8E;AAC9E,6EAA6E;AAC7E,iCAAiC;AACjC,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,QAAgB,EAChB,cAAc,GAAW,uBAAuB,EACzB,EAAE;IACzB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC;QACxB,QAAQ;QACR,UAAU;QACV,QAAQ,EAAE,MAAM,gBAAgB,CAAC,UAAU,CAAC;QAC5C,SAAS,EAAE,CAAC,kBAAkB,EAAE,CAAC;QACjC,UAAU,EAAE,kBAAkB,CAAC,QAAQ,CAAC;KACzC,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAAE,MAAM,MAAM,CAAC,OAAO,CAAC;IACnD,OAAO,MAAM,CAAC,OAAO,CAAC;AACxB,CAAC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import * as Result from "effect/Result";
2
+ // The repository's import graph: every walked file, and the walked files each
3
+ // resolves to. An external, a builtin, a file outside the walked roots and an
4
+ // edge nobody can resolve are not in it — the first three are not the
5
+ // repository's shape, and the last is reported by `check` on its own.
6
+ export const buildGraph = (files, resolver, factsOf) => {
7
+ const known = new Set(files);
8
+ const edges = new Map();
9
+ for (const file of files) {
10
+ const targets = new Set();
11
+ for (const specifier of factsOf(file).specifiers) {
12
+ const resolved = resolver.resolve(file, specifier);
13
+ if (Result.isFailure(resolved))
14
+ continue;
15
+ if (resolved.success.kind === "local" && known.has(resolved.success.path)) {
16
+ targets.add(resolved.success.path);
17
+ }
18
+ }
19
+ edges.set(file, [...targets]);
20
+ }
21
+ return { files, edges };
22
+ };
23
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/graph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,8EAA8E;AAC9E,8EAA8E;AAC9E,sEAAsE;AACtE,sEAAsE;AACtE,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,KAA4B,EAC5B,QAAwB,EACxB,OAAsC,EAC/B,EAAE;IACT,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiC,CAAC;IACvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAClC,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;YACjD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YACnD,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACzC,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1E,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC,CAAC"}
@@ -0,0 +1,17 @@
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
+ import { run } from "./run.js";
6
+ const exit = await Effect.runPromiseExit(run(process.cwd(), process.argv.slice(2)));
7
+ if (Exit.isFailure(exit)) {
8
+ // The expected failures carry their own sentence; anything else is a defect
9
+ // and deserves the full cause.
10
+ const squashed = Cause.squash(exit.cause);
11
+ const message = typeof squashed === "object" && squashed !== null && "message" in squashed
12
+ ? String(squashed.message)
13
+ : Cause.pretty(exit.cause);
14
+ process.stderr.write(`\n${message}\n`);
15
+ process.exitCode = 1;
16
+ }
17
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":";AACA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEpF,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;IACzB,4EAA4E;IAC5E,+BAA+B;IAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,OAAO,GACX,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,IAAI,SAAS,IAAI,QAAQ;QACxE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAC1B,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC;IACvC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,337 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { baselineOf, coverageOf, coverageShortfalls, decodeBaseline, EMPTY_BASELINE, evaluateGraph, evaluateMemberSite, evaluateSelectedBindings, evaluateSelectedEdge, evaluateStructure, evaluateSurface, exportRulesSelecting, fingerprintOf, formatMessage, fractionsOf, hasGraphRules, listSourceFiles, memberRulesSelecting, requiredSiblingsOf, rulesSelecting, serializeBaseline, staleEntriesOf, surfaceRulesSelecting, unbaselined, } from "@goodbones/core";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Result from "effect/Result";
6
+ import { loadPolicyFromFile } from "./config-loader.js";
7
+ import { buildGraph } from "./graph.js";
8
+ import { sourceFactsOf } from "./source-facts.js";
9
+ const fail = (message) => ({ _tag: "CliFailure", message });
10
+ export const collectFindings = (policy, roots) => {
11
+ const files = listSourceFiles(policy.repoRoot, roots, policy.languages);
12
+ const violations = [];
13
+ const unresolved = [];
14
+ // Each file is parsed at most once, whether the per-file families or the
15
+ // graph pass asks first.
16
+ const parsed = new Map();
17
+ const factsOf = (file) => {
18
+ const cached = parsed.get(file);
19
+ if (cached !== undefined)
20
+ return cached;
21
+ const facts = sourceFactsOf(policy.repoRoot, file, policy.extractor);
22
+ parsed.set(file, facts);
23
+ return facts;
24
+ };
25
+ // The graph is the whole repository resolved at once — the one question no
26
+ // per-file adapter can ask — and is built only when a rule needs it.
27
+ if (hasGraphRules(policy.graph)) {
28
+ for (const violation of evaluateGraph(policy.graph, buildGraph(files, policy.resolver, factsOf))) {
29
+ violations.push(violation);
30
+ }
31
+ }
32
+ for (const file of files) {
33
+ for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
34
+ violations.push(violation);
35
+ }
36
+ const selectedImports = rulesSelecting(policy.importRules, file);
37
+ const selectedExports = exportRulesSelecting(policy.exportRules, file);
38
+ const selectedMembers = memberRulesSelecting(policy.memberRules, file);
39
+ const selectedSurface = surfaceRulesSelecting(policy.surfaceRules, file);
40
+ if (selectedImports.length +
41
+ selectedExports.length +
42
+ selectedMembers.length +
43
+ selectedSurface.length ===
44
+ 0) {
45
+ continue;
46
+ }
47
+ const facts = factsOf(file);
48
+ for (const violation of evaluateSurface(selectedSurface, file, facts.exportSites)) {
49
+ violations.push(violation);
50
+ }
51
+ for (const site of facts.memberSites) {
52
+ for (const violation of evaluateMemberSite(selectedMembers, site))
53
+ violations.push(violation);
54
+ }
55
+ for (const specifier of facts.specifiers) {
56
+ const edge = { importer: file, specifier };
57
+ const imported = evaluateSelectedEdge(selectedImports, policy.resolver, edge);
58
+ if (Result.isFailure(imported)) {
59
+ if (policy.config.resolve.unresolved === "off")
60
+ continue;
61
+ if (policy.ignoreUnresolved.some((pattern) => pattern.test(specifier)))
62
+ continue;
63
+ unresolved.push(`${file} → ${specifier} (${imported.failure.detail})`);
64
+ continue;
65
+ }
66
+ for (const violation of imported.success)
67
+ violations.push(violation);
68
+ const bound = facts.bindings.get(specifier) ?? [];
69
+ const exported = evaluateSelectedBindings(selectedExports, policy.resolver, {
70
+ ...edge,
71
+ bindings: bound,
72
+ });
73
+ if (!Result.isFailure(exported)) {
74
+ for (const { violation } of exported.success)
75
+ violations.push(violation);
76
+ }
77
+ }
78
+ }
79
+ return { violations, unresolved, files: files.length };
80
+ };
81
+ const baselinePathOf = (policy) => policy.config.baseline === undefined
82
+ ? null
83
+ : path.resolve(policy.repoRoot, policy.config.baseline);
84
+ const readBaseline = (policy) => {
85
+ const at = baselinePathOf(policy);
86
+ if (at === null)
87
+ return EMPTY_BASELINE;
88
+ try {
89
+ return decodeBaseline(JSON.parse(readFileSync(at, "utf8")));
90
+ }
91
+ catch {
92
+ return EMPTY_BASELINE;
93
+ }
94
+ };
95
+ const report = (lines) => Effect.sync(() => {
96
+ for (const line of lines)
97
+ process.stdout.write(`${line}\n`);
98
+ });
99
+ const describe = (violation) => ` ${violation.file}\n ${formatMessage(violation)}`;
100
+ export const check = (policy, roots) => Effect.gen(function* () {
101
+ const findings = collectFindings(policy, roots);
102
+ const baseline = readBaseline(policy);
103
+ const reportable = unbaselined(baseline, findings.violations);
104
+ const stale = staleEntriesOf(baseline, findings.violations);
105
+ yield* report(reportable.map(describe));
106
+ yield* report(findings.unresolved.map((one) => ` unresolved: ${one}`));
107
+ const carried = findings.violations.length - reportable.length;
108
+ yield* report([
109
+ "",
110
+ `${String(findings.files)} files, ${String(reportable.length)} violations` +
111
+ (carried > 0 ? `, ${String(carried)} carried by the baseline` : ""),
112
+ ]);
113
+ if (stale.length > 0) {
114
+ // The ratchet: a fixed violation must leave the baseline, or the floor
115
+ // never rises and the file stops describing anything real.
116
+ yield* report([
117
+ "",
118
+ `${String(stale.length)} baseline entries no longer fire. The code was fixed; prune them:`,
119
+ ...stale.map((entry) => ` ${entry}`),
120
+ "",
121
+ " architecture baseline # rewrites the file from what still fires",
122
+ ]);
123
+ return yield* Effect.fail(fail("stale baseline entries"));
124
+ }
125
+ // The floors. A policy states how much of the tree it reaches, per
126
+ // family; falling under is a policy that quietly stopped covering files.
127
+ const floors = policy.config.limits?.coverage;
128
+ const shortfalls = floors === undefined
129
+ ? []
130
+ : coverageShortfalls(coverageOf(policy, listSourceFiles(policy.repoRoot, roots, policy.languages)), floors);
131
+ if (shortfalls.length > 0) {
132
+ yield* report([
133
+ "",
134
+ "coverage is below the floor the policy states for itself:",
135
+ ...shortfalls.map((one) => ` ${one.family}: ${percent(one.actual)} covered, floor ${percent(one.floor)}`),
136
+ "",
137
+ " architecture coverage # which files no rule reaches",
138
+ ]);
139
+ return yield* Effect.fail(fail("coverage below floor"));
140
+ }
141
+ if (reportable.length > 0 || findings.unresolved.length > 0) {
142
+ return yield* Effect.fail(fail("architecture violations"));
143
+ }
144
+ });
145
+ const percent = (fraction) => `${String(Math.floor(fraction * 100))}%`;
146
+ // How much of the tree the policy reaches. A probe proves a rule can fire;
147
+ // this is whether the files are there to fire on. Reported per family, with the
148
+ // adoption backlog — the tiers that said "not tightened yet" — beneath it.
149
+ export const coverage = (policy, roots) => Effect.gen(function* () {
150
+ const files = listSourceFiles(policy.repoRoot, roots, policy.languages);
151
+ const found = coverageOf(policy, files);
152
+ const fractions = fractionsOf(found);
153
+ const floors = policy.config.limits?.coverage ?? {};
154
+ const row = (family, covered, note) => {
155
+ const floor = floors[family];
156
+ const mark = floor === undefined
157
+ ? ""
158
+ : fractions[family] >= floor
159
+ ? ` ≥ ${percent(floor)} ✓`
160
+ : ` < ${percent(floor)} ✗`;
161
+ return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(found.files)} ${percent(fractions[family]).padStart(4)} ${note}${mark}`;
162
+ };
163
+ yield* report([
164
+ `${String(found.files)} files under ${roots.join(", ")}`,
165
+ "",
166
+ row("imports", found.imports.covered, "under an import allowlist"),
167
+ row("structure", found.structure.enumerated, `in an enumerated folder (${String(found.structure.open)} in an open one, ${String(found.structure.total - found.structure.enumerated - found.structure.open)} in none)`),
168
+ row("members", found.members.covered, "selected by a members rule"),
169
+ row("surface", found.surface.covered, "selected by a surface rule"),
170
+ row("graph", found.graph.covered, "in a cycles or orphans scope"),
171
+ "",
172
+ ` unrestricted tiers: ${policy.adoption.unrestricted.length === 0 ? "(none)" : policy.adoption.unrestricted.join(", ")}`,
173
+ ` partial tiers: ${policy.adoption.partial.length === 0 ? "(none)" : policy.adoption.partial.join(", ")}`,
174
+ ]);
175
+ });
176
+ export const writeBaseline = (policy, roots) => Effect.gen(function* () {
177
+ const at = baselinePathOf(policy);
178
+ if (at === null) {
179
+ return yield* Effect.fail(fail("this policy declares no `baseline` path, so there is nowhere to write one"));
180
+ }
181
+ const findings = collectFindings(policy, roots);
182
+ const baseline = baselineOf(findings.violations);
183
+ yield* Effect.sync(() => {
184
+ writeFileSync(at, serializeBaseline(baseline));
185
+ });
186
+ yield* report([
187
+ `${String(baseline.entries.length)} violations recorded in ${path.relative(policy.repoRoot, at)}.`,
188
+ "Each one is debt the policy is carrying. Fixing one means deleting its line.",
189
+ ]);
190
+ });
191
+ // The question a tree config makes harder to answer than a flat one: given a
192
+ // file, what governs it? A flat config you grep; a tree you have to walk.
193
+ export const explain = (policy, file) => Effect.gen(function* () {
194
+ const relative = path.relative(policy.repoRoot, path.resolve(policy.repoRoot, file));
195
+ const selected = rulesSelecting(policy.importRules, relative);
196
+ // An allowlist rule names no `to` — it fires when the target matches none of
197
+ // its patterns. A prohibition names one.
198
+ const allowlists = selected.filter(([rule]) => rule.to.length === 0 && rule.toNot.length > 0);
199
+ const prohibitions = selected.filter(([rule]) => rule.to.length > 0);
200
+ const owed = policy.structure.parity
201
+ .filter((rule) => rule.file.some((pattern) => pattern.test(relative)) &&
202
+ !rule.fileNot.some((pattern) => pattern.test(relative)))
203
+ .flatMap((rule) => requiredSiblingsOf(rule, relative));
204
+ const governing = policy.structure.folders.filter((rule) => rule.folder.some((pattern) => pattern.test(path.dirname(relative))));
205
+ const naming = policy.structure.naming.filter((rule) => rule.file.some((pattern) => pattern.test(relative)) &&
206
+ !rule.fileNot.some((pattern) => pattern.test(relative)));
207
+ const firstSentence = (message) => `${message.split(". ")[0] ?? message}.`;
208
+ const named = (rule) => ` ${rule.name} — ${firstSentence(rule.message)}`;
209
+ // The families beyond imports and structure: which rules of each speak to
210
+ // this file at all. What they are evaluated against is `facts`' answer.
211
+ const restricted = exportRulesSelecting(policy.exportRules, relative).map(([rule]) => rule);
212
+ const vocabulary = memberRulesSelecting(policy.memberRules, relative);
213
+ const surface = surfaceRulesSelecting(policy.surfaceRules, relative);
214
+ const scoped = (rule) => rule.within.some((pattern) => pattern.test(relative)) &&
215
+ !rule.withinNot.some((pattern) => pattern.test(relative));
216
+ const graph = [
217
+ ...policy.graph.cycles.filter(scoped).map((rule) => `${named(rule)} (cycles)`),
218
+ ...policy.graph.orphans.filter(scoped).map((rule) => `${named(rule)} (orphans)`),
219
+ ...policy.graph.reach
220
+ .filter((rule) => rule.from.some((pattern) => pattern.test(relative)) &&
221
+ !rule.fromNot.some((pattern) => pattern.test(relative)))
222
+ .map((rule) => `${named(rule)} (reach)`),
223
+ ];
224
+ const section = (title, lines) => lines.length === 0 ? [] : ["", title, ...lines];
225
+ yield* report([
226
+ relative,
227
+ "",
228
+ allowlists.length === 0
229
+ ? " may import: anything (no tier above this file states an allowlist)"
230
+ : " may import:",
231
+ ...allowlists.flatMap(([rule]) => [
232
+ ` — ${rule.name}`,
233
+ ...rule.toNot.map((pattern) => ` ${pattern}`),
234
+ ...[...rule.externals].map((name) => ` external: ${name}`),
235
+ ]),
236
+ "",
237
+ " may not import:",
238
+ ...(prohibitions.length === 0
239
+ ? [" (nothing beyond the allowlist above)"]
240
+ : prohibitions.map(([rule]) => ` ${rule.name} — ${firstSentence(rule.message)}`)),
241
+ "",
242
+ ` lives in: ${governing.length === 0 ? "a folder no rule governs" : governing.map((rule) => rule.name).join(", ")}`,
243
+ ...(naming.length === 0
244
+ ? []
245
+ : [
246
+ " is named by:",
247
+ ...naming.map((rule) => ` ${rule.name} — ${rule.sameAs !== null ? "its folder's own name" : (rule.convention?.source ?? "")}`),
248
+ ]),
249
+ ...(owed.length === 0 ? [] : [" owes:", ...owed.map((one) => ` ${one}`)]),
250
+ ...section(" may not name (exports):", restricted.map(named)),
251
+ ...section(" vocabulary (members):", vocabulary.map(named)),
252
+ ...section(" may export (surface):", surface.map(named)),
253
+ ...section(" graph:", graph),
254
+ ]);
255
+ });
256
+ // The other half of `explain`. `explain` says which rules select a file; this
257
+ // says what those rules are evaluated against — every edge the parser found,
258
+ // the names carried across each, every declared member and called name. A rule
259
+ // that "should fire" and does not is one of two mistakes, and this is how to
260
+ // tell them apart: the pattern does not select the site, or the site is not a
261
+ // fact the adapter extracts.
262
+ export const facts = (policy, file, format = "text") => Effect.gen(function* () {
263
+ const relative = path
264
+ .relative(policy.repoRoot, path.resolve(policy.repoRoot, file))
265
+ .replaceAll(path.sep, "/");
266
+ const read = yield* Effect.try({
267
+ try: () => sourceFactsOf(policy.repoRoot, relative, policy.extractor),
268
+ catch: (cause) => fail(`could not read ${relative}: ${String(cause)}`),
269
+ });
270
+ const edges = read.specifiers.map((specifier) => ({
271
+ specifier,
272
+ bindings: read.bindings.get(specifier) ?? [],
273
+ }));
274
+ const declared = read.memberSites.filter((site) => site.subject === "members");
275
+ const called = read.memberSites.filter((site) => site.subject === "calls");
276
+ if (format === "json") {
277
+ return yield* report([
278
+ JSON.stringify({ file: relative, edges, memberSites: read.memberSites, exportSites: read.exportSites }, null, 2),
279
+ ]);
280
+ }
281
+ yield* report([
282
+ relative,
283
+ "",
284
+ edges.length === 0 ? " edges: (none)" : " edges:",
285
+ ...edges.flatMap(({ bindings, specifier }) => [
286
+ ` ${specifier}`,
287
+ ...(bindings.length === 0
288
+ ? [" (no bindings)"]
289
+ : bindings.map((binding) => ` ${binding.kind.padEnd(9)} ${binding.symbol}`)),
290
+ ]),
291
+ "",
292
+ declared.length === 0 ? " members: (none)" : " members:",
293
+ ...declared.map((site) => ` ${site.in ?? ""}.${site.name} (${site.declares ?? "other"})`),
294
+ "",
295
+ called.length === 0 ? " calls: (none)" : " calls:",
296
+ ...called.map((site) => ` ${site.name}`),
297
+ "",
298
+ read.exportSites.length === 0 ? " exports: (none)" : " exports:",
299
+ ...read.exportSites.map((site) => ` ${site.kind.padEnd(9)} ${site.name} (${site.reexport ? "re-export" : site.declares})`),
300
+ ]);
301
+ });
302
+ export const run = (repoRoot, argv) => Effect.gen(function* () {
303
+ const [command = "check", ...rest] = argv;
304
+ const policy = yield* Effect.tryPromise({
305
+ try: () => loadPolicyFromFile(repoRoot),
306
+ catch: (cause) => fail(String(cause)),
307
+ });
308
+ yield* Effect.sync(() => {
309
+ for (const notice of policy.notices)
310
+ process.stderr.write(`deprecated: ${notice}\n`);
311
+ });
312
+ const roots = rest.length > 0 ? rest : ["packages"];
313
+ switch (command) {
314
+ case "check":
315
+ return yield* check(policy, roots);
316
+ case "baseline":
317
+ return yield* writeBaseline(policy, roots);
318
+ case "explain": {
319
+ const [file] = rest;
320
+ if (file === undefined)
321
+ return yield* Effect.fail(fail("explain needs a file path"));
322
+ return yield* explain(policy, file);
323
+ }
324
+ case "coverage":
325
+ return yield* coverage(policy, roots);
326
+ case "facts": {
327
+ const [file] = rest.filter((argument) => argument !== "--json");
328
+ if (file === undefined)
329
+ return yield* Effect.fail(fail("facts needs a file path"));
330
+ return yield* facts(policy, file, rest.includes("--json") ? "json" : "text");
331
+ }
332
+ default:
333
+ return yield* Effect.fail(fail(`unknown command "${command}". Try: check | baseline | coverage | explain <file> | facts <file> [--json]`));
334
+ }
335
+ });
336
+ export const fingerprint = fingerprintOf;
337
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ // Everything the policy needs to know about one file on disk, read once,
4
+ // through the extractor of whichever language the policy routes the file to.
5
+ export const sourceFactsOf = (repoRoot, file, extractor) => extractor.factsOf(file, readFileSync(path.join(repoRoot, file), "utf8"));
6
+ //# sourceMappingURL=source-facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-facts.js","sourceRoot":"","sources":["../../src/source-facts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAMlC,yEAAyE;AACzE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,QAAgB,EAChB,IAAY,EACZ,SAAwB,EACX,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@goodbones/cli",
3
+ "version": "0.1.0-beta.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "The architecture CLI for @goodbones/core: check, baseline, coverage, explain and facts over a whole repository with no linter in the loop, including the graph family (cycles, orphans, transitive reach) that only an adapter seeing every file at once can evaluate.",
7
+ "author": "zacharyweidenbach",
8
+ "keywords": [
9
+ "architecture",
10
+ "lint",
11
+ "import-boundaries",
12
+ "hexagonal",
13
+ "monorepo"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/dataquail/goodbones.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "homepage": "https://dataquail.github.io/goodbones",
24
+ "bugs": {
25
+ "url": "https://github.com/dataquail/goodbones/issues"
26
+ },
27
+ "exports": {
28
+ "./package.json": "./package.json"
29
+ },
30
+ "bin": {
31
+ "architecture": "./build/esm/main.js"
32
+ },
33
+ "files": [
34
+ "build/dts",
35
+ "build/esm",
36
+ "src",
37
+ "!src/**/*.test.ts"
38
+ ],
39
+ "dependencies": {
40
+ "effect": "4.0.0-beta.94",
41
+ "@goodbones/typescript": "0.1.0-beta.1",
42
+ "@goodbones/core": "0.1.0-beta.1"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25.9.4"
46
+ },
47
+ "nx": {
48
+ "includedScripts": []
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -b tsconfig.build.json",
52
+ "check": "tsc -b tsconfig.json",
53
+ "clean": "rm -rf build .tsbuildinfo",
54
+ "test": "vitest run",
55
+ "coverage": "vitest run --coverage"
56
+ }
57
+ }
@@ -0,0 +1,33 @@
1
+ import * as path from "node:path";
2
+
3
+ import {
4
+ DEFAULT_CONFIG_FILENAME,
5
+ type LoadedPolicy,
6
+ loadPolicy,
7
+ makeFileSystemLive,
8
+ readManifestFile,
9
+ } from "@goodbones/core";
10
+ import { typescriptLanguage } from "@goodbones/typescript";
11
+ import * as Result from "effect/Result";
12
+
13
+ export type { LoadedPolicy } from "@goodbones/core";
14
+
15
+ // The CLI's composition root: read the manifest file, construct the language
16
+ // packs and the live file system, and hand them to the loader. The plugin has
17
+ // one of its own with the same three lines, on purpose — the two hosts share
18
+ // the core and never each other.
19
+ export const loadPolicyFromFile = async (
20
+ repoRoot: string,
21
+ configFilename: string = DEFAULT_CONFIG_FILENAME,
22
+ ): Promise<LoadedPolicy> => {
23
+ const configPath = path.resolve(repoRoot, configFilename);
24
+ const loaded = loadPolicy({
25
+ repoRoot,
26
+ configPath,
27
+ manifest: await readManifestFile(configPath),
28
+ languages: [typescriptLanguage()],
29
+ fileSystem: makeFileSystemLive(repoRoot),
30
+ });
31
+ if (Result.isFailure(loaded)) throw loaded.failure;
32
+ return loaded.success;
33
+ };
package/src/graph.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Graph, ModuleResolver, SourceFacts } from "@goodbones/core";
2
+ import * as Result from "effect/Result";
3
+
4
+ // The repository's import graph: every walked file, and the walked files each
5
+ // resolves to. An external, a builtin, a file outside the walked roots and an
6
+ // edge nobody can resolve are not in it — the first three are not the
7
+ // repository's shape, and the last is reported by `check` on its own.
8
+ export const buildGraph = (
9
+ files: ReadonlyArray<string>,
10
+ resolver: ModuleResolver,
11
+ factsOf: (file: string) => SourceFacts,
12
+ ): Graph => {
13
+ const known = new Set(files);
14
+ const edges = new Map<string, ReadonlyArray<string>>();
15
+ for (const file of files) {
16
+ const targets = new Set<string>();
17
+ for (const specifier of factsOf(file).specifiers) {
18
+ const resolved = resolver.resolve(file, specifier);
19
+ if (Result.isFailure(resolved)) continue;
20
+ if (resolved.success.kind === "local" && known.has(resolved.success.path)) {
21
+ targets.add(resolved.success.path);
22
+ }
23
+ }
24
+ edges.set(file, [...targets]);
25
+ }
26
+ return { files, edges };
27
+ };
package/src/main.ts ADDED
@@ -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
+ }
package/src/run.ts ADDED
@@ -0,0 +1,490 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import {
5
+ type Baseline,
6
+ baselineOf,
7
+ coverageOf,
8
+ coverageShortfalls,
9
+ decodeBaseline,
10
+ EMPTY_BASELINE,
11
+ evaluateGraph,
12
+ evaluateMemberSite,
13
+ evaluateSelectedBindings,
14
+ evaluateSelectedEdge,
15
+ evaluateStructure,
16
+ evaluateSurface,
17
+ exportRulesSelecting,
18
+ fingerprintOf,
19
+ formatMessage,
20
+ fractionsOf,
21
+ hasGraphRules,
22
+ listSourceFiles,
23
+ memberRulesSelecting,
24
+ requiredSiblingsOf,
25
+ rulesSelecting,
26
+ serializeBaseline,
27
+ type SourceFacts,
28
+ staleEntriesOf,
29
+ surfaceRulesSelecting,
30
+ unbaselined,
31
+ type Violation,
32
+ } from "@goodbones/core";
33
+ import * as Effect from "effect/Effect";
34
+ import * as Result from "effect/Result";
35
+
36
+ import { type LoadedPolicy, loadPolicyFromFile } from "./config-loader.js";
37
+ import { buildGraph } from "./graph.js";
38
+ import { sourceFactsOf } from "./source-facts.js";
39
+
40
+ // The policy, run with no linter in the loop.
41
+ //
42
+ // oxlint's JS plugin API is alpha, and a policy that can only be evaluated by
43
+ // one alpha host is a policy with a single point of failure. This adapter is the
44
+ // second way to ask the same question — and the only way to write a baseline,
45
+ // since that needs every finding at once rather than one file at a time.
46
+ //
47
+ // It covers all four families. The two that need a syntax tree read TypeScript's
48
+ // rather than oxlint's; both adapters meet at the same vocabulary — a specifier,
49
+ // a binding, a member site — so they answer to the same core rather than to each
50
+ // other.
51
+
52
+ export type CliFailure = { readonly _tag: "CliFailure"; readonly message: string };
53
+
54
+ const fail = (message: string): CliFailure => ({ _tag: "CliFailure", message });
55
+
56
+ export type Findings = {
57
+ readonly violations: ReadonlyArray<Violation>;
58
+ readonly unresolved: ReadonlyArray<string>;
59
+ readonly files: number;
60
+ };
61
+
62
+ export const collectFindings = (policy: LoadedPolicy, roots: ReadonlyArray<string>): Findings => {
63
+ const files = listSourceFiles(policy.repoRoot, roots, policy.languages);
64
+ const violations: Array<Violation> = [];
65
+ const unresolved: Array<string> = [];
66
+
67
+ // Each file is parsed at most once, whether the per-file families or the
68
+ // graph pass asks first.
69
+ const parsed = new Map<string, SourceFacts>();
70
+ const factsOf = (file: string): SourceFacts => {
71
+ const cached = parsed.get(file);
72
+ if (cached !== undefined) return cached;
73
+ const facts = sourceFactsOf(policy.repoRoot, file, policy.extractor);
74
+ parsed.set(file, facts);
75
+ return facts;
76
+ };
77
+
78
+ // The graph is the whole repository resolved at once — the one question no
79
+ // per-file adapter can ask — and is built only when a rule needs it.
80
+ if (hasGraphRules(policy.graph)) {
81
+ for (const violation of evaluateGraph(
82
+ policy.graph,
83
+ buildGraph(files, policy.resolver, factsOf),
84
+ )) {
85
+ violations.push(violation);
86
+ }
87
+ }
88
+
89
+ for (const file of files) {
90
+ for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
91
+ violations.push(violation);
92
+ }
93
+
94
+ const selectedImports = rulesSelecting(policy.importRules, file);
95
+ const selectedExports = exportRulesSelecting(policy.exportRules, file);
96
+ const selectedMembers = memberRulesSelecting(policy.memberRules, file);
97
+ const selectedSurface = surfaceRulesSelecting(policy.surfaceRules, file);
98
+ if (
99
+ selectedImports.length +
100
+ selectedExports.length +
101
+ selectedMembers.length +
102
+ selectedSurface.length ===
103
+ 0
104
+ ) {
105
+ continue;
106
+ }
107
+
108
+ const facts = factsOf(file);
109
+
110
+ for (const violation of evaluateSurface(selectedSurface, file, facts.exportSites)) {
111
+ violations.push(violation);
112
+ }
113
+
114
+ for (const site of facts.memberSites) {
115
+ for (const violation of evaluateMemberSite(selectedMembers, site)) violations.push(violation);
116
+ }
117
+
118
+ for (const specifier of facts.specifiers) {
119
+ const edge = { importer: file, specifier };
120
+
121
+ const imported = evaluateSelectedEdge(selectedImports, policy.resolver, edge);
122
+ if (Result.isFailure(imported)) {
123
+ if (policy.config.resolve.unresolved === "off") continue;
124
+ if (policy.ignoreUnresolved.some((pattern) => pattern.test(specifier))) continue;
125
+ unresolved.push(`${file} → ${specifier} (${imported.failure.detail})`);
126
+ continue;
127
+ }
128
+ for (const violation of imported.success) violations.push(violation);
129
+
130
+ const bound = facts.bindings.get(specifier) ?? [];
131
+ const exported = evaluateSelectedBindings(selectedExports, policy.resolver, {
132
+ ...edge,
133
+ bindings: bound,
134
+ });
135
+ if (!Result.isFailure(exported)) {
136
+ for (const { violation } of exported.success) violations.push(violation);
137
+ }
138
+ }
139
+ }
140
+
141
+ return { violations, unresolved, files: files.length };
142
+ };
143
+
144
+ const baselinePathOf = (policy: LoadedPolicy): string | null =>
145
+ policy.config.baseline === undefined
146
+ ? null
147
+ : path.resolve(policy.repoRoot, policy.config.baseline);
148
+
149
+ const readBaseline = (policy: LoadedPolicy): Baseline => {
150
+ const at = baselinePathOf(policy);
151
+ if (at === null) return EMPTY_BASELINE;
152
+ try {
153
+ return decodeBaseline(JSON.parse(readFileSync(at, "utf8")) as unknown);
154
+ } catch {
155
+ return EMPTY_BASELINE;
156
+ }
157
+ };
158
+
159
+ const report = (lines: ReadonlyArray<string>): Effect.Effect<void> =>
160
+ Effect.sync(() => {
161
+ for (const line of lines) process.stdout.write(`${line}\n`);
162
+ });
163
+
164
+ const describe = (violation: Violation): string =>
165
+ ` ${violation.file}\n ${formatMessage(violation)}`;
166
+
167
+ export const check = (
168
+ policy: LoadedPolicy,
169
+ roots: ReadonlyArray<string>,
170
+ ): Effect.Effect<void, CliFailure> =>
171
+ Effect.gen(function* () {
172
+ const findings = collectFindings(policy, roots);
173
+ const baseline = readBaseline(policy);
174
+ const reportable = unbaselined(baseline, findings.violations);
175
+ const stale = staleEntriesOf(baseline, findings.violations);
176
+
177
+ yield* report(reportable.map(describe));
178
+ yield* report(findings.unresolved.map((one) => ` unresolved: ${one}`));
179
+
180
+ const carried = findings.violations.length - reportable.length;
181
+ yield* report([
182
+ "",
183
+ `${String(findings.files)} files, ${String(reportable.length)} violations` +
184
+ (carried > 0 ? `, ${String(carried)} carried by the baseline` : ""),
185
+ ]);
186
+
187
+ if (stale.length > 0) {
188
+ // The ratchet: a fixed violation must leave the baseline, or the floor
189
+ // never rises and the file stops describing anything real.
190
+ yield* report([
191
+ "",
192
+ `${String(stale.length)} baseline entries no longer fire. The code was fixed; prune them:`,
193
+ ...stale.map((entry) => ` ${entry}`),
194
+ "",
195
+ " architecture baseline # rewrites the file from what still fires",
196
+ ]);
197
+ return yield* Effect.fail(fail("stale baseline entries"));
198
+ }
199
+
200
+ // The floors. A policy states how much of the tree it reaches, per
201
+ // family; falling under is a policy that quietly stopped covering files.
202
+ const floors = policy.config.limits?.coverage;
203
+ const shortfalls =
204
+ floors === undefined
205
+ ? []
206
+ : coverageShortfalls(
207
+ coverageOf(policy, listSourceFiles(policy.repoRoot, roots, policy.languages)),
208
+ floors,
209
+ );
210
+ if (shortfalls.length > 0) {
211
+ yield* report([
212
+ "",
213
+ "coverage is below the floor the policy states for itself:",
214
+ ...shortfalls.map(
215
+ (one) => ` ${one.family}: ${percent(one.actual)} covered, floor ${percent(one.floor)}`,
216
+ ),
217
+ "",
218
+ " architecture coverage # which files no rule reaches",
219
+ ]);
220
+ return yield* Effect.fail(fail("coverage below floor"));
221
+ }
222
+
223
+ if (reportable.length > 0 || findings.unresolved.length > 0) {
224
+ return yield* Effect.fail(fail("architecture violations"));
225
+ }
226
+ });
227
+
228
+ const percent = (fraction: number): string => `${String(Math.floor(fraction * 100))}%`;
229
+
230
+ // How much of the tree the policy reaches. A probe proves a rule can fire;
231
+ // this is whether the files are there to fire on. Reported per family, with the
232
+ // adoption backlog — the tiers that said "not tightened yet" — beneath it.
233
+ export const coverage = (
234
+ policy: LoadedPolicy,
235
+ roots: ReadonlyArray<string>,
236
+ ): Effect.Effect<void, CliFailure> =>
237
+ Effect.gen(function* () {
238
+ const files = listSourceFiles(policy.repoRoot, roots, policy.languages);
239
+ const found = coverageOf(policy, files);
240
+ const fractions = fractionsOf(found);
241
+ const floors = policy.config.limits?.coverage ?? {};
242
+ const row = (family: keyof typeof fractions, covered: number, note: string): string => {
243
+ const floor = floors[family];
244
+ const mark =
245
+ floor === undefined
246
+ ? ""
247
+ : fractions[family] >= floor
248
+ ? ` ≥ ${percent(floor)} ✓`
249
+ : ` < ${percent(floor)} ✗`;
250
+ return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(found.files)} ${percent(fractions[family]).padStart(4)} ${note}${mark}`;
251
+ };
252
+
253
+ yield* report([
254
+ `${String(found.files)} files under ${roots.join(", ")}`,
255
+ "",
256
+ row("imports", found.imports.covered, "under an import allowlist"),
257
+ row(
258
+ "structure",
259
+ found.structure.enumerated,
260
+ `in an enumerated folder (${String(found.structure.open)} in an open one, ${String(found.structure.total - found.structure.enumerated - found.structure.open)} in none)`,
261
+ ),
262
+ row("members", found.members.covered, "selected by a members rule"),
263
+ row("surface", found.surface.covered, "selected by a surface rule"),
264
+ row("graph", found.graph.covered, "in a cycles or orphans scope"),
265
+ "",
266
+ ` unrestricted tiers: ${policy.adoption.unrestricted.length === 0 ? "(none)" : policy.adoption.unrestricted.join(", ")}`,
267
+ ` partial tiers: ${policy.adoption.partial.length === 0 ? "(none)" : policy.adoption.partial.join(", ")}`,
268
+ ]);
269
+ });
270
+
271
+ export const writeBaseline = (
272
+ policy: LoadedPolicy,
273
+ roots: ReadonlyArray<string>,
274
+ ): Effect.Effect<void, CliFailure> =>
275
+ Effect.gen(function* () {
276
+ const at = baselinePathOf(policy);
277
+ if (at === null) {
278
+ return yield* Effect.fail(
279
+ fail("this policy declares no `baseline` path, so there is nowhere to write one"),
280
+ );
281
+ }
282
+
283
+ const findings = collectFindings(policy, roots);
284
+ const baseline = baselineOf(findings.violations);
285
+ yield* Effect.sync(() => {
286
+ writeFileSync(at, serializeBaseline(baseline));
287
+ });
288
+ yield* report([
289
+ `${String(baseline.entries.length)} violations recorded in ${path.relative(policy.repoRoot, at)}.`,
290
+ "Each one is debt the policy is carrying. Fixing one means deleting its line.",
291
+ ]);
292
+ });
293
+
294
+ // The question a tree config makes harder to answer than a flat one: given a
295
+ // file, what governs it? A flat config you grep; a tree you have to walk.
296
+ export const explain = (policy: LoadedPolicy, file: string): Effect.Effect<void, CliFailure> =>
297
+ Effect.gen(function* () {
298
+ const relative = path.relative(policy.repoRoot, path.resolve(policy.repoRoot, file));
299
+ const selected = rulesSelecting(policy.importRules, relative);
300
+
301
+ // An allowlist rule names no `to` — it fires when the target matches none of
302
+ // its patterns. A prohibition names one.
303
+ const allowlists = selected.filter(([rule]) => rule.to.length === 0 && rule.toNot.length > 0);
304
+ const prohibitions = selected.filter(([rule]) => rule.to.length > 0);
305
+
306
+ const owed = policy.structure.parity
307
+ .filter(
308
+ (rule) =>
309
+ rule.file.some((pattern) => pattern.test(relative)) &&
310
+ !rule.fileNot.some((pattern) => pattern.test(relative)),
311
+ )
312
+ .flatMap((rule) => requiredSiblingsOf(rule, relative));
313
+
314
+ const governing = policy.structure.folders.filter((rule) =>
315
+ rule.folder.some((pattern) => pattern.test(path.dirname(relative))),
316
+ );
317
+
318
+ const naming = policy.structure.naming.filter(
319
+ (rule) =>
320
+ rule.file.some((pattern) => pattern.test(relative)) &&
321
+ !rule.fileNot.some((pattern) => pattern.test(relative)),
322
+ );
323
+
324
+ const firstSentence = (message: string) => `${message.split(". ")[0] ?? message}.`;
325
+ const named = (rule: { readonly name: string; readonly message: string }): string =>
326
+ ` ${rule.name} — ${firstSentence(rule.message)}`;
327
+
328
+ // The families beyond imports and structure: which rules of each speak to
329
+ // this file at all. What they are evaluated against is `facts`' answer.
330
+ const restricted = exportRulesSelecting(policy.exportRules, relative).map(([rule]) => rule);
331
+ const vocabulary = memberRulesSelecting(policy.memberRules, relative);
332
+ const surface = surfaceRulesSelecting(policy.surfaceRules, relative);
333
+ const scoped = (rule: { within: ReadonlyArray<RegExp>; withinNot: ReadonlyArray<RegExp> }) =>
334
+ rule.within.some((pattern) => pattern.test(relative)) &&
335
+ !rule.withinNot.some((pattern) => pattern.test(relative));
336
+ const graph = [
337
+ ...policy.graph.cycles.filter(scoped).map((rule) => `${named(rule)} (cycles)`),
338
+ ...policy.graph.orphans.filter(scoped).map((rule) => `${named(rule)} (orphans)`),
339
+ ...policy.graph.reach
340
+ .filter(
341
+ (rule) =>
342
+ rule.from.some((pattern) => pattern.test(relative)) &&
343
+ !rule.fromNot.some((pattern) => pattern.test(relative)),
344
+ )
345
+ .map((rule) => `${named(rule)} (reach)`),
346
+ ];
347
+ const section = (title: string, lines: ReadonlyArray<string>): ReadonlyArray<string> =>
348
+ lines.length === 0 ? [] : ["", title, ...lines];
349
+
350
+ yield* report([
351
+ relative,
352
+ "",
353
+ allowlists.length === 0
354
+ ? " may import: anything (no tier above this file states an allowlist)"
355
+ : " may import:",
356
+ ...allowlists.flatMap(([rule]) => [
357
+ ` — ${rule.name}`,
358
+ ...rule.toNot.map((pattern) => ` ${pattern}`),
359
+ ...[...rule.externals].map((name) => ` external: ${name}`),
360
+ ]),
361
+ "",
362
+ " may not import:",
363
+ ...(prohibitions.length === 0
364
+ ? [" (nothing beyond the allowlist above)"]
365
+ : prohibitions.map(([rule]) => ` ${rule.name} — ${firstSentence(rule.message)}`)),
366
+ "",
367
+ ` lives in: ${governing.length === 0 ? "a folder no rule governs" : governing.map((rule) => rule.name).join(", ")}`,
368
+ ...(naming.length === 0
369
+ ? []
370
+ : [
371
+ " is named by:",
372
+ ...naming.map(
373
+ (rule) =>
374
+ ` ${rule.name} — ${rule.sameAs !== null ? "its folder's own name" : (rule.convention?.source ?? "")}`,
375
+ ),
376
+ ]),
377
+ ...(owed.length === 0 ? [] : [" owes:", ...owed.map((one) => ` ${one}`)]),
378
+ ...section(" may not name (exports):", restricted.map(named)),
379
+ ...section(" vocabulary (members):", vocabulary.map(named)),
380
+ ...section(" may export (surface):", surface.map(named)),
381
+ ...section(" graph:", graph),
382
+ ]);
383
+ });
384
+
385
+ // The other half of `explain`. `explain` says which rules select a file; this
386
+ // says what those rules are evaluated against — every edge the parser found,
387
+ // the names carried across each, every declared member and called name. A rule
388
+ // that "should fire" and does not is one of two mistakes, and this is how to
389
+ // tell them apart: the pattern does not select the site, or the site is not a
390
+ // fact the adapter extracts.
391
+ export const facts = (
392
+ policy: LoadedPolicy,
393
+ file: string,
394
+ format: "text" | "json" = "text",
395
+ ): Effect.Effect<void, CliFailure> =>
396
+ Effect.gen(function* () {
397
+ const relative = path
398
+ .relative(policy.repoRoot, path.resolve(policy.repoRoot, file))
399
+ .replaceAll(path.sep, "/");
400
+
401
+ const read = yield* Effect.try({
402
+ try: () => sourceFactsOf(policy.repoRoot, relative, policy.extractor),
403
+ catch: (cause) => fail(`could not read ${relative}: ${String(cause)}`),
404
+ });
405
+
406
+ const edges = read.specifiers.map((specifier) => ({
407
+ specifier,
408
+ bindings: read.bindings.get(specifier) ?? [],
409
+ }));
410
+ const declared = read.memberSites.filter((site) => site.subject === "members");
411
+ const called = read.memberSites.filter((site) => site.subject === "calls");
412
+
413
+ if (format === "json") {
414
+ return yield* report([
415
+ JSON.stringify(
416
+ { file: relative, edges, memberSites: read.memberSites, exportSites: read.exportSites },
417
+ null,
418
+ 2,
419
+ ),
420
+ ]);
421
+ }
422
+
423
+ yield* report([
424
+ relative,
425
+ "",
426
+ edges.length === 0 ? " edges: (none)" : " edges:",
427
+ ...edges.flatMap(({ bindings, specifier }) => [
428
+ ` ${specifier}`,
429
+ ...(bindings.length === 0
430
+ ? [" (no bindings)"]
431
+ : bindings.map((binding) => ` ${binding.kind.padEnd(9)} ${binding.symbol}`)),
432
+ ]),
433
+ "",
434
+ declared.length === 0 ? " members: (none)" : " members:",
435
+ ...declared.map((site) => ` ${site.in ?? ""}.${site.name} (${site.declares ?? "other"})`),
436
+ "",
437
+ called.length === 0 ? " calls: (none)" : " calls:",
438
+ ...called.map((site) => ` ${site.name}`),
439
+ "",
440
+ read.exportSites.length === 0 ? " exports: (none)" : " exports:",
441
+ ...read.exportSites.map(
442
+ (site) =>
443
+ ` ${site.kind.padEnd(9)} ${site.name} (${site.reexport ? "re-export" : site.declares})`,
444
+ ),
445
+ ]);
446
+ });
447
+
448
+ export const run = (
449
+ repoRoot: string,
450
+ argv: ReadonlyArray<string>,
451
+ ): Effect.Effect<void, CliFailure> =>
452
+ Effect.gen(function* () {
453
+ const [command = "check", ...rest] = argv;
454
+ const policy = yield* Effect.tryPromise({
455
+ try: () => loadPolicyFromFile(repoRoot),
456
+ catch: (cause) => fail(String(cause)),
457
+ });
458
+ yield* Effect.sync(() => {
459
+ for (const notice of policy.notices) process.stderr.write(`deprecated: ${notice}\n`);
460
+ });
461
+
462
+ const roots = rest.length > 0 ? rest : ["packages"];
463
+
464
+ switch (command) {
465
+ case "check":
466
+ return yield* check(policy, roots);
467
+ case "baseline":
468
+ return yield* writeBaseline(policy, roots);
469
+ case "explain": {
470
+ const [file] = rest;
471
+ if (file === undefined) return yield* Effect.fail(fail("explain needs a file path"));
472
+ return yield* explain(policy, file);
473
+ }
474
+ case "coverage":
475
+ return yield* coverage(policy, roots);
476
+ case "facts": {
477
+ const [file] = rest.filter((argument) => argument !== "--json");
478
+ if (file === undefined) return yield* Effect.fail(fail("facts needs a file path"));
479
+ return yield* facts(policy, file, rest.includes("--json") ? "json" : "text");
480
+ }
481
+ default:
482
+ return yield* Effect.fail(
483
+ fail(
484
+ `unknown command "${command}". Try: check | baseline | coverage | explain <file> | facts <file> [--json]`,
485
+ ),
486
+ );
487
+ }
488
+ });
489
+
490
+ export const fingerprint = fingerprintOf;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import type { FactExtractor, SourceFacts } from "@goodbones/core";
5
+
6
+ export type { SourceFacts } from "@goodbones/core";
7
+
8
+ // Everything the policy needs to know about one file on disk, read once,
9
+ // through the extractor of whichever language the policy routes the file to.
10
+ export const sourceFactsOf = (
11
+ repoRoot: string,
12
+ file: string,
13
+ extractor: FactExtractor,
14
+ ): SourceFacts => extractor.factsOf(file, readFileSync(path.join(repoRoot, file), "utf8"));