@goodbones/cli 0.1.0-beta.1 → 0.1.0-beta.11
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/build/dts/config-loader.d.ts +6 -1
- package/build/dts/config-loader.d.ts.map +1 -1
- package/build/dts/infer.d.ts +34 -0
- package/build/dts/infer.d.ts.map +1 -0
- package/build/dts/run.d.ts +80 -6
- package/build/dts/run.d.ts.map +1 -1
- package/build/esm/config-loader.js +52 -5
- package/build/esm/config-loader.js.map +1 -1
- package/build/esm/infer.js +378 -0
- package/build/esm/infer.js.map +1 -0
- package/build/esm/main.js +1 -1
- package/build/esm/main.js.map +1 -1
- package/build/esm/run.js +983 -70
- package/build/esm/run.js.map +1 -1
- package/package.json +5 -3
- package/src/config-loader.ts +67 -5
- package/src/infer.ts +511 -0
- package/src/main.ts +3 -1
- package/src/run.ts +1401 -92
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { type LoadedPolicy } from "@goodbones/core";
|
|
1
|
+
import { type ConfigInvalid, type Language, type LoadedPolicy, type PatternInvalid } from "@goodbones/core";
|
|
2
|
+
import * as Result from "effect/Result";
|
|
2
3
|
export type { LoadedPolicy } from "@goodbones/core";
|
|
4
|
+
export declare const hostLanguages: () => ReadonlyArray<Language>;
|
|
5
|
+
export declare const hostNow: () => number;
|
|
6
|
+
export declare const manifestPathOf: (repoRoot: string, configFilename?: string) => string;
|
|
3
7
|
export declare const loadPolicyFromFile: (repoRoot: string, configFilename?: string) => Promise<LoadedPolicy>;
|
|
8
|
+
export declare const loadPolicyFromManifest: (repoRoot: string, manifest: unknown) => Result.Result<LoadedPolicy, ConfigInvalid | PatternInvalid>;
|
|
4
9
|
//# sourceMappingURL=config-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,QAAQ,EACb,KAAK,YAAY,EAGjB,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAMpD,eAAO,MAAM,aAAa,QAAO,aAAa,CAAC,QAAQ,CAEtD,CAAC;AAKF,eAAO,MAAM,OAAO,QAAO,MAQ1B,CAAC;AAQF,eAAO,MAAM,cAAc,aAAc,MAAM,mBAAmB,MAAM,KAAG,MAG/B,CAAC;AAE7C,eAAO,MAAM,kBAAkB,aACnB,MAAM,mBACC,MAAM,KACtB,OAAO,CAAC,YAAY,CAkBtB,CAAC;AAMF,eAAO,MAAM,sBAAsB,aACvB,MAAM,YACN,OAAO,KAChB,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,GAAG,cAAc,CASzD,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Generalization, type Manifest } from "@goodbones/core";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Result from "effect/Result";
|
|
4
|
+
export type CliFailure = {
|
|
5
|
+
readonly _tag: "CliFailure";
|
|
6
|
+
readonly message: string;
|
|
7
|
+
};
|
|
8
|
+
export type InferFlags = {
|
|
9
|
+
readonly depth: number;
|
|
10
|
+
readonly roots: ReadonlyArray<string>;
|
|
11
|
+
readonly tsconfig: string | null;
|
|
12
|
+
readonly write: boolean;
|
|
13
|
+
readonly answers: "ask" | "yes" | "exhaustive";
|
|
14
|
+
readonly collapse: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare const INFER_USAGE = "infer [--depth N] [--root DIR]... [--tsconfig PATH] [--write] [--yes | --exhaustive] [--collapse]";
|
|
17
|
+
export declare const parseInferFlags: (argv: ReadonlyArray<string>) => Result.Result<InferFlags, string>;
|
|
18
|
+
export type InferIo = {
|
|
19
|
+
readonly out: (text: string) => void;
|
|
20
|
+
readonly err: (line: string) => void;
|
|
21
|
+
readonly ask: (question: string) => Promise<boolean>;
|
|
22
|
+
readonly interactive: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare const terminalIo: () => InferIo;
|
|
25
|
+
export type InferOutcome = {
|
|
26
|
+
readonly manifest: Manifest;
|
|
27
|
+
readonly yaml: string;
|
|
28
|
+
readonly files: number;
|
|
29
|
+
readonly nodes: number;
|
|
30
|
+
readonly unresolved: ReadonlyArray<string>;
|
|
31
|
+
readonly generalized: ReadonlyArray<Generalization>;
|
|
32
|
+
};
|
|
33
|
+
export declare const infer: (repoRoot: string, argv: ReadonlyArray<string>, configFilename: string | undefined, io?: InferIo) => Effect.Effect<InferOutcome, CliFailure>;
|
|
34
|
+
//# sourceMappingURL=infer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infer.d.ts","sourceRoot":"","sources":["../../src/infer.ts"],"names":[],"mappings":"AAIA,OAAO,EAOL,KAAK,cAAc,EAMnB,KAAK,QAAQ,EAId,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAaxC,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAGnF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAExB,QAAQ,CAAC,OAAO,EAAE,KAAK,GAAG,KAAK,GAAG,YAAY,CAAC;IAC/C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,WAAW,sGAC6E,CAAC;AAEtG,eAAO,MAAM,eAAe,SAAU,aAAa,CAAC,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CA2D7F,CAAC;AAIF,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,OAc5B,CAAC;AAqMH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CACrD,CAAC;AAEF,eAAO,MAAM,KAAK,aACN,MAAM,QACV,aAAa,CAAC,MAAM,CAAC,kBACX,MAAM,GAAG,SAAS,OAC9B,OAAO,KACV,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,UAAU,CAmKrC,CAAC"}
|
package/build/dts/run.d.ts
CHANGED
|
@@ -1,21 +1,95 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CampaignEvaluation, type CampaignReport } from "@goodbones/campaigns";
|
|
2
|
+
import { type ConformanceMeasure, type CoverageFamily, type Graph, type ObservedEdge, type Snapshot, type Violation } from "@goodbones/core";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
import { type LoadedPolicy } from "./config-loader.js";
|
|
4
5
|
export type CliFailure = {
|
|
5
6
|
readonly _tag: "CliFailure";
|
|
6
7
|
readonly message: string;
|
|
7
8
|
};
|
|
9
|
+
export type UnresolvedEdge = {
|
|
10
|
+
readonly file: string;
|
|
11
|
+
readonly specifier: string;
|
|
12
|
+
readonly detail: string;
|
|
13
|
+
};
|
|
8
14
|
export type Findings = {
|
|
9
15
|
readonly violations: ReadonlyArray<Violation>;
|
|
10
|
-
readonly
|
|
16
|
+
readonly campaigns: ReadonlyArray<CampaignEvaluation>;
|
|
17
|
+
readonly unresolved: ReadonlyArray<UnresolvedEdge>;
|
|
18
|
+
readonly files: number;
|
|
19
|
+
readonly edges: ReadonlyArray<ObservedEdge>;
|
|
20
|
+
readonly graph: Graph | null;
|
|
21
|
+
};
|
|
22
|
+
export type CollectOptions = {
|
|
23
|
+
readonly graph?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare const collectFindings: (policy: LoadedPolicy, roots: ReadonlyArray<string>, options?: CollectOptions) => Findings;
|
|
26
|
+
export type ReportedViolation = Violation & {
|
|
27
|
+
readonly fingerprint: string;
|
|
28
|
+
readonly baselined: boolean;
|
|
29
|
+
readonly ledgered: boolean;
|
|
30
|
+
readonly objective?: string;
|
|
31
|
+
readonly sector?: string;
|
|
32
|
+
readonly entry?: string;
|
|
33
|
+
};
|
|
34
|
+
export type CoverageReport = Readonly<Record<CoverageFamily, {
|
|
35
|
+
readonly covered: number;
|
|
36
|
+
readonly total: number;
|
|
37
|
+
readonly floor?: number;
|
|
38
|
+
}>>;
|
|
39
|
+
export type ConformanceReport = Readonly<Record<ConformanceMeasure, {
|
|
40
|
+
readonly count: number;
|
|
41
|
+
readonly ceiling?: number;
|
|
42
|
+
}>>;
|
|
43
|
+
export type CheckReport = {
|
|
44
|
+
readonly version: 1;
|
|
11
45
|
readonly files: number;
|
|
46
|
+
readonly roots: ReadonlyArray<string>;
|
|
47
|
+
readonly ok: boolean;
|
|
48
|
+
readonly manifest: {
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly sha256: string;
|
|
51
|
+
};
|
|
52
|
+
readonly violations: ReadonlyArray<ReportedViolation>;
|
|
53
|
+
readonly unresolved: ReadonlyArray<UnresolvedEdge>;
|
|
54
|
+
readonly stale: ReadonlyArray<string>;
|
|
55
|
+
readonly coverage: CoverageReport;
|
|
56
|
+
readonly conformance: ConformanceReport;
|
|
57
|
+
readonly adoption: {
|
|
58
|
+
readonly unrestricted: ReadonlyArray<string>;
|
|
59
|
+
readonly partial: ReadonlyArray<string>;
|
|
60
|
+
};
|
|
61
|
+
readonly campaigns: ReadonlyArray<CampaignReport>;
|
|
62
|
+
};
|
|
63
|
+
export declare const checkReport: (policy: LoadedPolicy, roots: ReadonlyArray<string>, manifestPath: string) => CheckReport;
|
|
64
|
+
export type CheckOptions = {
|
|
65
|
+
readonly format: "text" | "json";
|
|
66
|
+
readonly manifestPath: string;
|
|
12
67
|
};
|
|
13
|
-
export declare const
|
|
14
|
-
export declare const
|
|
68
|
+
export declare const check: (policy: LoadedPolicy, roots: ReadonlyArray<string>, options: CheckOptions) => Effect.Effect<void, CliFailure>;
|
|
69
|
+
export declare const snapshotOf: (policy: LoadedPolicy, roots: ReadonlyArray<string>, manifestPath: string) => Snapshot;
|
|
70
|
+
export type ConformanceOptions = CheckOptions;
|
|
71
|
+
export declare const conformance: (policy: LoadedPolicy, roots: ReadonlyArray<string>, options: ConformanceOptions) => Effect.Effect<void, CliFailure>;
|
|
15
72
|
export declare const coverage: (policy: LoadedPolicy, roots: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
|
|
16
73
|
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>;
|
|
74
|
+
export declare const explain: (policy: LoadedPolicy, file: string, roots?: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
|
|
18
75
|
export declare const facts: (policy: LoadedPolicy, file: string, format?: "text" | "json") => Effect.Effect<void, CliFailure>;
|
|
19
|
-
export declare const
|
|
76
|
+
export declare const init: (repoRoot: string) => Effect.Effect<void, CliFailure>;
|
|
77
|
+
export declare const migrate: (repoRoot: string, configFilename?: string) => Effect.Effect<void, CliFailure>;
|
|
78
|
+
export declare const campaignArgsOf: (argv: ReadonlyArray<string>, ids: ReadonlyArray<string>) => {
|
|
79
|
+
readonly subcommand: string | undefined;
|
|
80
|
+
readonly args: ReadonlyArray<string>;
|
|
81
|
+
readonly roots: ReadonlyArray<string>;
|
|
82
|
+
};
|
|
83
|
+
export declare const objectiveArgsOf: (argv: ReadonlyArray<string>, ids: ReadonlyArray<string>) => {
|
|
84
|
+
readonly subcommand: string | undefined;
|
|
85
|
+
readonly target: {
|
|
86
|
+
campaign: string;
|
|
87
|
+
objective: string | null;
|
|
88
|
+
} | null;
|
|
89
|
+
readonly roots: ReadonlyArray<string>;
|
|
90
|
+
};
|
|
91
|
+
export declare const objectives: (policy: LoadedPolicy, defaultRoots: ReadonlyArray<string>, argv: ReadonlyArray<string>) => Effect.Effect<void, CliFailure>;
|
|
92
|
+
export declare const campaigns: (policy: LoadedPolicy, defaultRoots: ReadonlyArray<string>, argv: ReadonlyArray<string>, configFilename?: string) => Effect.Effect<void, CliFailure>;
|
|
93
|
+
export declare const run: (repoRoot: string, argv: ReadonlyArray<string>, configFilename?: string) => Effect.Effect<void, CliFailure>;
|
|
20
94
|
export declare const fingerprint: (violation: Violation) => string;
|
|
21
95
|
//# sourceMappingURL=run.d.ts.map
|
package/build/dts/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,22 +1,69 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { astGrepMatcher } from "@goodbones/ast-grep";
|
|
3
|
+
import { campaignsExtension, loadCampaignFunctions, makeReportSourceLive, } from "@goodbones/campaigns";
|
|
4
|
+
import { findManifestFile, loadPolicy, makeFileSystemLive, readManifestFile, } from "@goodbones/core";
|
|
3
5
|
import { typescriptLanguage } from "@goodbones/typescript";
|
|
4
6
|
import * as Result from "effect/Result";
|
|
7
|
+
// The language packs this host is composed with. Constructed here and handed
|
|
8
|
+
// down as the `Language` port, so nothing below this file names TypeScript —
|
|
9
|
+
// or ast-grep, which the pack is composed with here for the campaigns
|
|
10
|
+
// family's `syntax` term.
|
|
11
|
+
export const hostLanguages = () => [
|
|
12
|
+
typescriptLanguage({ syntax: astGrepMatcher() }),
|
|
13
|
+
];
|
|
14
|
+
// The clock the campaigns are judged by. `ARCHITECTURE_NOW` (an ISO date or
|
|
15
|
+
// epoch milliseconds) pins it, for a CI that replays a day or a test that
|
|
16
|
+
// needs a stall to have happened.
|
|
17
|
+
export const hostNow = () => {
|
|
18
|
+
const pinned = process.env.ARCHITECTURE_NOW;
|
|
19
|
+
if (pinned === undefined || pinned === "")
|
|
20
|
+
return Date.now();
|
|
21
|
+
const parsed = /^\d+$/.test(pinned) ? Number(pinned) : Date.parse(pinned);
|
|
22
|
+
if (Number.isNaN(parsed)) {
|
|
23
|
+
throw new Error(`ARCHITECTURE_NOW is ${JSON.stringify(pinned)}, which is not a date.`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
};
|
|
5
27
|
// The CLI's composition root: read the manifest file, construct the language
|
|
6
28
|
// packs and the live file system, and hand them to the loader. The plugin has
|
|
7
29
|
// one of its own with the same three lines, on purpose — the two hosts share
|
|
8
30
|
// the core and never each other.
|
|
9
|
-
|
|
10
|
-
|
|
31
|
+
// Named, or discovered: architecture.yaml, .yml, .json, or .config.mjs,
|
|
32
|
+
// exactly one of which may be present. Absolute.
|
|
33
|
+
export const manifestPathOf = (repoRoot, configFilename) => configFilename === undefined
|
|
34
|
+
? findManifestFile(repoRoot)
|
|
35
|
+
: path.resolve(repoRoot, configFilename);
|
|
36
|
+
export const loadPolicyFromFile = async (repoRoot, configFilename) => {
|
|
37
|
+
const configPath = manifestPathOf(repoRoot, configFilename);
|
|
38
|
+
const read = await readManifestFile(configPath);
|
|
39
|
+
// The `fn` terms are imported here, before the policy loads: the core
|
|
40
|
+
// receives the functions as a map and touches no module loader.
|
|
41
|
+
const { functions, manifest } = await loadCampaignFunctions(configPath, read.manifest);
|
|
11
42
|
const loaded = loadPolicy({
|
|
12
43
|
repoRoot,
|
|
13
44
|
configPath,
|
|
14
|
-
manifest
|
|
15
|
-
|
|
45
|
+
manifest,
|
|
46
|
+
locate: read.locate,
|
|
47
|
+
languages: hostLanguages(),
|
|
16
48
|
fileSystem: makeFileSystemLive(repoRoot),
|
|
49
|
+
extensions: [campaignsExtension({ functions, reports: makeReportSourceLive(repoRoot) })],
|
|
50
|
+
now: hostNow(),
|
|
17
51
|
});
|
|
18
52
|
if (Result.isFailure(loaded))
|
|
19
53
|
throw loaded.failure;
|
|
20
54
|
return loaded.success;
|
|
21
55
|
};
|
|
56
|
+
// The same composition, for a manifest the host already holds as a value
|
|
57
|
+
// rather than a file: the bootstrap `infer` resolves through, and the
|
|
58
|
+
// manifest it then proves loads. Failure is returned, not thrown — the caller
|
|
59
|
+
// has a sentence to put around it.
|
|
60
|
+
export const loadPolicyFromManifest = (repoRoot, manifest) => loadPolicy({
|
|
61
|
+
repoRoot,
|
|
62
|
+
configPath: path.resolve(repoRoot, "architecture.yaml"),
|
|
63
|
+
manifest,
|
|
64
|
+
languages: hostLanguages(),
|
|
65
|
+
fileSystem: makeFileSystemLive(repoRoot),
|
|
66
|
+
extensions: [campaignsExtension({ reports: makeReportSourceLive(repoRoot) })],
|
|
67
|
+
now: hostNow(),
|
|
68
|
+
});
|
|
22
69
|
//# sourceMappingURL=config-loader.js.map
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as readline from "node:readline/promises";
|
|
4
|
+
import { candidatesOf, coverageOf, cyclesIn, formatManifestYaml, fractionsOf, inferManifest, listPackageRoots, listSourceFiles, MANIFEST_FILENAMES, MANIFEST_SCHEMA_ID, } from "@goodbones/core";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Result from "effect/Result";
|
|
7
|
+
import { hostLanguages, loadPolicyFromFile, loadPolicyFromManifest } from "./config-loader.js";
|
|
8
|
+
import { buildGraph } from "./graph.js";
|
|
9
|
+
import { sourceFactsOf } from "./source-facts.js";
|
|
10
|
+
const fail = (message) => ({ _tag: "CliFailure", message });
|
|
11
|
+
export const INFER_USAGE = "infer [--depth N] [--root DIR]... [--tsconfig PATH] [--write] [--yes | --exhaustive] [--collapse]";
|
|
12
|
+
export const parseInferFlags = (argv) => {
|
|
13
|
+
let depth = 2;
|
|
14
|
+
const roots = [];
|
|
15
|
+
let tsconfig = null;
|
|
16
|
+
let write = false;
|
|
17
|
+
let answers = "ask";
|
|
18
|
+
let collapse = false;
|
|
19
|
+
const args = [...argv];
|
|
20
|
+
while (args.length > 0) {
|
|
21
|
+
const arg = args.shift();
|
|
22
|
+
if (arg === undefined)
|
|
23
|
+
break;
|
|
24
|
+
const value = () => {
|
|
25
|
+
const next = args.shift();
|
|
26
|
+
return next === undefined || next.startsWith("--")
|
|
27
|
+
? Result.fail(`${arg} needs a value`)
|
|
28
|
+
: Result.succeed(next);
|
|
29
|
+
};
|
|
30
|
+
switch (arg) {
|
|
31
|
+
case "--depth": {
|
|
32
|
+
const given = value();
|
|
33
|
+
if (Result.isFailure(given))
|
|
34
|
+
return Result.fail(given.failure);
|
|
35
|
+
const parsed = Number(given.success);
|
|
36
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
37
|
+
return Result.fail(`--depth takes a whole number, not "${given.success}"`);
|
|
38
|
+
}
|
|
39
|
+
depth = parsed;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case "--root": {
|
|
43
|
+
const given = value();
|
|
44
|
+
if (Result.isFailure(given))
|
|
45
|
+
return Result.fail(given.failure);
|
|
46
|
+
roots.push(given.success);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "--tsconfig": {
|
|
50
|
+
const given = value();
|
|
51
|
+
if (Result.isFailure(given))
|
|
52
|
+
return Result.fail(given.failure);
|
|
53
|
+
tsconfig = given.success;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "--write":
|
|
57
|
+
write = true;
|
|
58
|
+
break;
|
|
59
|
+
case "--yes":
|
|
60
|
+
answers = "yes";
|
|
61
|
+
break;
|
|
62
|
+
case "--exhaustive":
|
|
63
|
+
answers = "exhaustive";
|
|
64
|
+
break;
|
|
65
|
+
case "--collapse":
|
|
66
|
+
collapse = true;
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
if (arg.startsWith("--"))
|
|
70
|
+
return Result.fail(`unknown flag ${arg}. Usage: ${INFER_USAGE}`);
|
|
71
|
+
roots.push(arg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Result.succeed({ depth, roots, tsconfig, write, answers, collapse });
|
|
75
|
+
};
|
|
76
|
+
export const terminalIo = () => ({
|
|
77
|
+
out: (text) => process.stdout.write(text),
|
|
78
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
79
|
+
ask: async (question) => {
|
|
80
|
+
// The questions go to stderr so stdout stays the manifest.
|
|
81
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
82
|
+
try {
|
|
83
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
84
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
rl.close();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
interactive: process.stdin.isTTY === true,
|
|
91
|
+
});
|
|
92
|
+
const normalizeRoot = (root) => root.replaceAll(path.sep, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
93
|
+
const isGlob = (key) => /[*{}|[\]]/.test(key);
|
|
94
|
+
// The top-level keys of an existing manifest, as folders: aliases expanded,
|
|
95
|
+
// the trailing slash dropped, and any key that is a pattern left out.
|
|
96
|
+
const rootsOfManifest = (manifest) => {
|
|
97
|
+
const aliases = Object.entries(manifest.aliases ?? {}).sort(([a], [b]) => b.length - a.length);
|
|
98
|
+
const expand = (key) => {
|
|
99
|
+
for (const [alias, target] of aliases) {
|
|
100
|
+
if (key === alias)
|
|
101
|
+
return target;
|
|
102
|
+
if (key.startsWith(`${alias}/`))
|
|
103
|
+
return `${target}${key.slice(alias.length)}`;
|
|
104
|
+
}
|
|
105
|
+
return key;
|
|
106
|
+
};
|
|
107
|
+
return Object.keys(manifest.tree)
|
|
108
|
+
.filter((key) => !isGlob(key))
|
|
109
|
+
.map((key) => normalizeRoot(expand(key)))
|
|
110
|
+
.filter((key) => key !== "");
|
|
111
|
+
};
|
|
112
|
+
// With no manifest to read them from: `src/` when there is one, otherwise
|
|
113
|
+
// every top-level folder that holds a source file.
|
|
114
|
+
const rootsOfRepository = (repoRoot, languages) => {
|
|
115
|
+
if (existsSync(path.join(repoRoot, "src")))
|
|
116
|
+
return ["src"];
|
|
117
|
+
const folders = readdirSync(repoRoot).filter((entry) => {
|
|
118
|
+
if (entry.startsWith("."))
|
|
119
|
+
return false;
|
|
120
|
+
try {
|
|
121
|
+
return statSync(path.join(repoRoot, entry)).isDirectory();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return folders.filter((folder) => listSourceFiles(repoRoot, [folder], languages).length > 0);
|
|
128
|
+
};
|
|
129
|
+
// A policy that can resolve the tree and nothing else: one open root per
|
|
130
|
+
// walk root, no allowlist, resolution through the tsconfig the flag names or
|
|
131
|
+
// the one at the repository root.
|
|
132
|
+
const bootstrapManifest = (roots, tsconfig) => ({
|
|
133
|
+
resolve: {
|
|
134
|
+
scopes: [{ files: "", language: "typescript", options: { tsconfig } }],
|
|
135
|
+
unresolved: "off",
|
|
136
|
+
},
|
|
137
|
+
tree: Object.fromEntries(roots.map((root) => [
|
|
138
|
+
`${root}/`,
|
|
139
|
+
{ layout: "open", imports: { unrestricted: true }, children: {} },
|
|
140
|
+
])),
|
|
141
|
+
});
|
|
142
|
+
const settingOf = async (repoRoot, configFilename, flags) => {
|
|
143
|
+
const hasManifest = configFilename !== undefined ||
|
|
144
|
+
MANIFEST_FILENAMES.some((name) => existsSync(path.resolve(repoRoot, name)));
|
|
145
|
+
if (hasManifest && flags.tsconfig === null) {
|
|
146
|
+
const policy = await loadPolicyFromFile(repoRoot, configFilename);
|
|
147
|
+
const fromManifest = rootsOfManifest(policy.config);
|
|
148
|
+
const roots = flags.roots.length > 0
|
|
149
|
+
? flags.roots.map(normalizeRoot)
|
|
150
|
+
: fromManifest.length > 0
|
|
151
|
+
? fromManifest
|
|
152
|
+
: rootsOfRepository(repoRoot, policy.languages);
|
|
153
|
+
return Result.succeed({
|
|
154
|
+
policy,
|
|
155
|
+
roots,
|
|
156
|
+
resolve: policy.config.resolve,
|
|
157
|
+
aliases: policy.config.aliases,
|
|
158
|
+
baseline: policy.config.baseline ?? ".architecture-baseline.json",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const tsconfig = flags.tsconfig ?? "tsconfig.json";
|
|
162
|
+
if (!existsSync(path.resolve(repoRoot, tsconfig))) {
|
|
163
|
+
return Result.fail(`${tsconfig} does not exist, and the TypeScript resolver needs one to turn a specifier ` +
|
|
164
|
+
`into a file. Name the right one with --tsconfig <path>.`);
|
|
165
|
+
}
|
|
166
|
+
const roots = flags.roots.length > 0
|
|
167
|
+
? flags.roots.map(normalizeRoot)
|
|
168
|
+
: rootsOfRepository(repoRoot, hostLanguages());
|
|
169
|
+
if (roots.length === 0) {
|
|
170
|
+
return Result.fail("no source files found under any top-level folder. Name the folder to describe with --root <dir>.");
|
|
171
|
+
}
|
|
172
|
+
const loaded = loadPolicyFromManifest(repoRoot, bootstrapManifest(roots, tsconfig));
|
|
173
|
+
if (Result.isFailure(loaded))
|
|
174
|
+
return Result.fail(String(loaded.failure));
|
|
175
|
+
return Result.succeed({
|
|
176
|
+
policy: loaded.success,
|
|
177
|
+
roots,
|
|
178
|
+
resolve: { ...loaded.success.config.resolve, unresolved: "error" },
|
|
179
|
+
aliases: undefined,
|
|
180
|
+
baseline: ".architecture-baseline.json",
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
const describeCandidate = (candidate) => {
|
|
184
|
+
const parent = candidate.parent === "" ? "the root" : `${candidate.parent}/`;
|
|
185
|
+
const shared = [
|
|
186
|
+
...candidate.sharedFolders.map((one) => `${one}/`),
|
|
187
|
+
...candidate.sharedStereotypes,
|
|
188
|
+
];
|
|
189
|
+
const key = candidate.parent === ""
|
|
190
|
+
? `{${candidate.capture}}/`
|
|
191
|
+
: `${candidate.parent}/{${candidate.capture}}/`;
|
|
192
|
+
return (`\n${parent}: ${candidate.members.join(", ")} each have ${shared.join(", ")}.\n` +
|
|
193
|
+
` Describe them as one node, ${key}?`);
|
|
194
|
+
};
|
|
195
|
+
const describeReach = (candidate) => {
|
|
196
|
+
const glob = candidate.parent === ""
|
|
197
|
+
? `*/${candidate.crossReach ?? ""}`
|
|
198
|
+
: `${candidate.parent}/*/${candidate.crossReach ?? ""}`;
|
|
199
|
+
return (` All ${String(candidate.crossEdges)} imports between them land on ${candidate.crossReach ?? ""}.\n` +
|
|
200
|
+
` Restrict what one may reach in another to ${glob}?`);
|
|
201
|
+
};
|
|
202
|
+
const keyOf = (candidate) => `${candidate.parent}:${candidate.members.join(",")}`;
|
|
203
|
+
// The questionnaire. Each round recomputes the candidates over the tree as
|
|
204
|
+
// it now stands, so a group accepted at one level can reveal one above it;
|
|
205
|
+
// a declined group is not asked twice.
|
|
206
|
+
const decide = async (input, flags, io) => {
|
|
207
|
+
if (flags.answers === "exhaustive")
|
|
208
|
+
return [];
|
|
209
|
+
const accepted = [];
|
|
210
|
+
const declined = new Set();
|
|
211
|
+
for (;;) {
|
|
212
|
+
const next = candidatesOf(input, accepted).find((one) => !declined.has(keyOf(one)));
|
|
213
|
+
if (next === undefined)
|
|
214
|
+
break;
|
|
215
|
+
const yes = flags.answers === "yes" || (await io.ask(describeCandidate(next)));
|
|
216
|
+
if (!yes) {
|
|
217
|
+
declined.add(keyOf(next));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const tighten = next.crossReach !== null && (flags.answers === "yes" || (await io.ask(describeReach(next))));
|
|
221
|
+
accepted.push({
|
|
222
|
+
parent: next.parent,
|
|
223
|
+
members: next.members,
|
|
224
|
+
capture: next.capture,
|
|
225
|
+
crossReach: tighten ? next.crossReach : null,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return accepted;
|
|
229
|
+
};
|
|
230
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
231
|
+
const header = (date) => `# yaml-language-server: $schema=${MANIFEST_SCHEMA_ID}
|
|
232
|
+
#
|
|
233
|
+
# The as-built architecture of this repository, inferred on ${date} by
|
|
234
|
+
# \`architecture infer\`. Every node says what its folder does today — what it
|
|
235
|
+
# imports, and nothing about what it may not. Each \`allow\` entry is a fact;
|
|
236
|
+
# each one you delete is a decision. \`unrestricted: true\` marks a node nobody
|
|
237
|
+
# has reviewed yet, and \`limits.unrestricted\` counts how many remain: lower it
|
|
238
|
+
# as you go, and the policy refuses to drift back.
|
|
239
|
+
#
|
|
240
|
+
# https://dataquail.github.io/goodbones/architecture-rules/getting-started/infer/
|
|
241
|
+
|
|
242
|
+
`;
|
|
243
|
+
export const infer = (repoRoot, argv, configFilename, io = terminalIo()) => Effect.gen(function* () {
|
|
244
|
+
const parsed = parseInferFlags(argv);
|
|
245
|
+
if (Result.isFailure(parsed))
|
|
246
|
+
return yield* Effect.fail(fail(parsed.failure));
|
|
247
|
+
const flags = parsed.success;
|
|
248
|
+
if (flags.write) {
|
|
249
|
+
const present = MANIFEST_FILENAMES.filter((name) => existsSync(path.resolve(repoRoot, name)));
|
|
250
|
+
if (present.length > 0) {
|
|
251
|
+
return yield* Effect.fail(fail(`${present.join(", ")} already exists. \`infer --write\` writes a manifest for a ` +
|
|
252
|
+
`repository that has none, and does not overwrite one; leave --write off to print it.`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const setting = yield* Effect.tryPromise({
|
|
256
|
+
try: () => settingOf(repoRoot, configFilename, flags),
|
|
257
|
+
catch: (cause) => fail(String(cause)),
|
|
258
|
+
});
|
|
259
|
+
if (Result.isFailure(setting))
|
|
260
|
+
return yield* Effect.fail(fail(setting.failure));
|
|
261
|
+
const { aliases, baseline, policy, resolve, roots } = setting.success;
|
|
262
|
+
for (const root of roots) {
|
|
263
|
+
const absolute = path.resolve(repoRoot, root);
|
|
264
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
|
|
265
|
+
return yield* Effect.fail(fail(`--root ${root} is not a folder under ${repoRoot}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const files = listSourceFiles(repoRoot, roots, policy.languages);
|
|
269
|
+
const packages = listPackageRoots(repoRoot, roots, policy.languages);
|
|
270
|
+
const parsedFacts = new Map();
|
|
271
|
+
const factsOf = (file) => {
|
|
272
|
+
const cached = parsedFacts.get(file);
|
|
273
|
+
if (cached !== undefined)
|
|
274
|
+
return cached;
|
|
275
|
+
const facts = sourceFactsOf(repoRoot, file, policy.extractor);
|
|
276
|
+
parsedFacts.set(file, facts);
|
|
277
|
+
return facts;
|
|
278
|
+
};
|
|
279
|
+
const targets = new Map();
|
|
280
|
+
const unresolved = [];
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const found = [];
|
|
283
|
+
for (const specifier of factsOf(file).specifiers) {
|
|
284
|
+
const resolved = policy.resolver.resolve(file, specifier);
|
|
285
|
+
if (Result.isFailure(resolved)) {
|
|
286
|
+
if (!policy.ignoreUnresolved.some((pattern) => pattern.test(specifier))) {
|
|
287
|
+
unresolved.push(`${file} → ${specifier} (${resolved.failure.detail})`);
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const target = resolved.success;
|
|
292
|
+
found.push(target.kind === "local"
|
|
293
|
+
? { kind: "local", path: target.path }
|
|
294
|
+
: target.kind === "builtin"
|
|
295
|
+
? { kind: "builtin", path: target.path }
|
|
296
|
+
: { kind: "external", package: target.package ?? target.path });
|
|
297
|
+
}
|
|
298
|
+
targets.set(file, found);
|
|
299
|
+
}
|
|
300
|
+
const input = {
|
|
301
|
+
files,
|
|
302
|
+
targetsOf: (file) => targets.get(file) ?? [],
|
|
303
|
+
roots,
|
|
304
|
+
packages,
|
|
305
|
+
depth: flags.depth,
|
|
306
|
+
};
|
|
307
|
+
if (flags.answers === "ask" && !io.interactive) {
|
|
308
|
+
io.err("not a terminal, so no questions: every folder gets a node of its own. " +
|
|
309
|
+
"Pass --yes to accept every generalization, or run this at a terminal.");
|
|
310
|
+
}
|
|
311
|
+
const generalized = yield* Effect.promise(() => decide(input, flags.answers === "ask" && !io.interactive ? { ...flags, answers: "exhaustive" } : flags, io));
|
|
312
|
+
const date = today();
|
|
313
|
+
const hasCycles = cyclesIn(buildGraph(files, policy.resolver, factsOf)).length > 0;
|
|
314
|
+
const inferred = inferManifest(input, {
|
|
315
|
+
generalize: generalized,
|
|
316
|
+
collapse: flags.collapse,
|
|
317
|
+
date,
|
|
318
|
+
resolve,
|
|
319
|
+
aliases,
|
|
320
|
+
baseline,
|
|
321
|
+
hasCycles,
|
|
322
|
+
});
|
|
323
|
+
// The proof: what was written loads, every probe passes, and the floors
|
|
324
|
+
// are the numbers it reaches today.
|
|
325
|
+
const loaded = loadPolicyFromManifest(repoRoot, inferred.manifest);
|
|
326
|
+
if (Result.isFailure(loaded)) {
|
|
327
|
+
return yield* Effect.fail(fail(`infer wrote a manifest that does not load — this is a bug: ${String(loaded.failure)}`));
|
|
328
|
+
}
|
|
329
|
+
const fractions = fractionsOf(coverageOf(loaded.success, files));
|
|
330
|
+
const floor = (fraction) => Math.floor(fraction * 100) / 100;
|
|
331
|
+
const coverage = {};
|
|
332
|
+
for (const family of ["imports", "structure", "members", "surface", "graph"]) {
|
|
333
|
+
if (fractions[family] > 0)
|
|
334
|
+
coverage[family] = floor(fractions[family]);
|
|
335
|
+
}
|
|
336
|
+
const manifest = {
|
|
337
|
+
...inferred.manifest,
|
|
338
|
+
limits: { ...inferred.manifest.limits, coverage },
|
|
339
|
+
};
|
|
340
|
+
const yaml = `${header(date)}${formatManifestYaml(manifest)}`;
|
|
341
|
+
if (flags.write) {
|
|
342
|
+
yield* Effect.sync(() => {
|
|
343
|
+
writeFileSync(path.resolve(repoRoot, "architecture.yaml"), yaml);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
yield* Effect.sync(() => {
|
|
348
|
+
io.out(yaml);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
yield* Effect.sync(() => {
|
|
352
|
+
const summary = `${String(files.length)} files under ${roots.join(", ")}: ${String(inferred.nodes)} nodes` +
|
|
353
|
+
(generalized.length > 0 ? `, ${String(generalized.length)} generalized` : "") +
|
|
354
|
+
(hasCycles ? "; the graph has a cycle, so no no-cycles rule was written" : "");
|
|
355
|
+
io.err(flags.write ? `wrote architecture.yaml. ${summary}.` : summary);
|
|
356
|
+
if (unresolved.length > 0) {
|
|
357
|
+
io.err("");
|
|
358
|
+
io.err(`${String(unresolved.length)} imports could not be resolved, and are in no allowlist. ` +
|
|
359
|
+
"`check` will report them until the resolver can see them or `ignoreUnresolved` names them:");
|
|
360
|
+
for (const one of unresolved)
|
|
361
|
+
io.err(` ${one}`);
|
|
362
|
+
}
|
|
363
|
+
if (flags.write) {
|
|
364
|
+
io.err("");
|
|
365
|
+
io.err(" architecture check # should be clean: the manifest describes today");
|
|
366
|
+
io.err(" architecture coverage # every node is unrestricted; review them one by one");
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
manifest,
|
|
371
|
+
yaml,
|
|
372
|
+
files: files.length,
|
|
373
|
+
nodes: inferred.nodes,
|
|
374
|
+
unresolved,
|
|
375
|
+
generalized,
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
//# sourceMappingURL=infer.js.map
|