@ecoma-io/archkeep 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/cli.mjs +571 -61
- package/commands.mjs +57 -0
- package/lsp.mjs +15 -2
- package/package.json +8 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +485 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +289 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +11 -3
- package/src/commands/README.md +52 -1
- package/src/commands/change-intent.mjs +461 -0
- package/src/commands/change.mjs +612 -0
- package/src/commands/check.mjs +2 -1
- package/src/commands/context.mjs +124 -16
- package/src/commands/custom-rules.mjs +286 -2
- package/src/commands/delta-classify.mjs +195 -33
- package/src/commands/delta-snapshot.mjs +156 -1
- package/src/commands/delta.mjs +142 -17
- package/src/commands/diff.mjs +41 -13
- package/src/commands/evolution.mjs +473 -0
- package/src/commands/history.mjs +130 -103
- package/src/commands/policy.mjs +57 -0
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/commands/trajectory.mjs +437 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/path-util.mjs +40 -0
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/report/change-text.mjs +148 -0
- package/src/report/delta-text.mjs +82 -1
- package/src/report/evolution-text.mjs +83 -0
- package/src/report/history-text.mjs +4 -114
- package/src/report/sarif.mjs +255 -0
- package/src/report/snapshot-text.mjs +123 -0
- package/src/report/trajectory-text.mjs +143 -0
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/tsconfig-paths.mjs +3 -2
- package/src/workspace.mjs +115 -23
package/src/workspace.mjs
CHANGED
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
* is the same tracked-file set every resolver in this project already reasons
|
|
29
29
|
* about ("Resolvers read tracked files only", project `AGENTS.md`).
|
|
30
30
|
*/
|
|
31
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
32
32
|
import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
|
|
33
33
|
|
|
34
34
|
import { containmentViolation } from "./containment.mjs";
|
|
35
35
|
import { analyzeFile, languageOf } from "./analysis/analyze.mjs";
|
|
36
|
+
import { basenameMatches } from "./analysis/manifest-util.mjs";
|
|
36
37
|
import { fileFailure, projectOwning } from "./analysis/source-util.mjs";
|
|
37
38
|
import { UsageError } from "./errors.mjs";
|
|
38
39
|
import { parseNxJson } from "./nx-json.mjs";
|
|
@@ -57,29 +58,109 @@ export { environmentForTree, runProcess };
|
|
|
57
58
|
* with a hoisted install, the right one by luck (same reason
|
|
58
59
|
* `loadBoundaryConfig` takes a root — see `config.mjs`).
|
|
59
60
|
*
|
|
61
|
+
* The walk is bounded by the enclosing git repository: a marker above the top
|
|
62
|
+
* level `git rev-parse --show-toplevel` names for `from` is tooling state,
|
|
63
|
+
* not this workspace's root. The case that made the bound necessary is
|
|
64
|
+
* `~/.moon` — moonrepo's user-level state directory, present on every machine
|
|
65
|
+
* moonrepo has ever run on (its own documentation puts the shared cache at
|
|
66
|
+
* `~/.moon/cache/shared`), which a walk reading only directory existence
|
|
67
|
+
* climbed to from any unmarked directory under the home directory, selecting
|
|
68
|
+
* `$HOME` as a "Moon workspace" and failing to load
|
|
69
|
+
* `$HOME/module-boundaries.config.mjs` instead of refusing (#339). The bound
|
|
70
|
+
* is inclusive: a marker ON the top level is the ordinary case, a repository
|
|
71
|
+
* that is itself the workspace. With no enclosing repository — or no `git` to
|
|
72
|
+
* ask — there is no bound, and the walk climbs as far as it did before one
|
|
73
|
+
* existed; the shape of the Moon markers is what holds the line there.
|
|
74
|
+
*
|
|
60
75
|
* `markers` defaults to `nx.json` alone, so every existing caller keeps
|
|
61
76
|
* finding exactly the root it found before. A native-provider caller passes
|
|
62
77
|
* `[NX_CONFIG_FILE, ARCHKEEP_MODEL_FILE]` to recognise either root marker in
|
|
63
78
|
* one walk — see `../cli.mjs`, which is the only caller that needs to tell
|
|
64
79
|
* the two apart, and does so by checking which marker(s) the returned
|
|
65
|
-
* directory actually carries.
|
|
80
|
+
* directory actually carries. A Moon marker names the directory's
|
|
81
|
+
* `workspace.yml` — the file moonrepo itself requires of a workspace — never
|
|
82
|
+
* the directory alone (`../providers/moon.mjs`'s `MOON_WORKSPACE_MARKER`):
|
|
83
|
+
* a bare `.moon` is the user-level state directory again, and directory
|
|
84
|
+
* presence alone is exactly what selected `$HOME`.
|
|
66
85
|
*
|
|
67
86
|
* @param {string} from Absolute directory to start at.
|
|
68
|
-
* @param {string[]} [markers] Filenames or
|
|
69
|
-
* marks a workspace root. `existsSync` works for
|
|
70
|
-
* like `.moon` is detected the same way a filename like
|
|
87
|
+
* @param {string[]} [markers] Filenames or relative paths whose presence
|
|
88
|
+
* marks a workspace root. `existsSync` works for all of them — a relative
|
|
89
|
+
* path like `.moon/workspace.yml` is detected the same way a filename like
|
|
90
|
+
* `nx.json` is.
|
|
91
|
+
* @param {{gitTopLevel?: (from: string) => string|null}} [io] The git seam:
|
|
92
|
+
* how the walk asks for the enclosing repository's top level, injectable
|
|
93
|
+
* for the same reason every spawn here is. The default runs
|
|
94
|
+
* `git rev-parse --show-toplevel` through `runProcess` — so ambient
|
|
95
|
+
* `GIT_DIR`-style redirects are stripped, the boundary describing the tree
|
|
96
|
+
* at `from` rather than whatever repository a hook exported — and answers
|
|
97
|
+
* `null` when git cannot (no repository encloses `from`, or git is absent).
|
|
98
|
+
* `null` is a missing boundary, never a refusal: the walk then climbs
|
|
99
|
+
* unbounded, exactly as it did before the boundary existed.
|
|
71
100
|
* @returns {string|null} Absolute path, or `null` when no ancestor has one.
|
|
72
101
|
*/
|
|
73
|
-
export function findWorkspaceRoot(
|
|
102
|
+
export function findWorkspaceRoot(
|
|
103
|
+
from,
|
|
104
|
+
markers = [NX_CONFIG_FILE],
|
|
105
|
+
{ gitTopLevel = gitTopLevelOf } = {},
|
|
106
|
+
) {
|
|
107
|
+
const ceiling = gitTopLevel(resolve(from));
|
|
74
108
|
let current = resolve(from);
|
|
75
109
|
for (;;) {
|
|
76
110
|
if (markers.some((marker) => existsSync(join(current, marker)))) return current;
|
|
111
|
+
// Inclusive on purpose: the marker check above already ran for the top
|
|
112
|
+
// level itself, so reaching the ceiling with no marker means no ancestor
|
|
113
|
+
// within the repository is a workspace root — and every ancestor beyond
|
|
114
|
+
// it is outside the tree `git ls-files` would answer for.
|
|
115
|
+
if (ceiling !== null && sameDirectory(current, ceiling)) return null;
|
|
77
116
|
const parent = dirname(current);
|
|
78
117
|
if (parent === current) return null;
|
|
79
118
|
current = parent;
|
|
80
119
|
}
|
|
81
120
|
}
|
|
82
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The top level of the git repository enclosing `from` — the ceiling
|
|
124
|
+
* `findWorkspaceRoot` above stops its walk at.
|
|
125
|
+
*
|
|
126
|
+
* `null` when none does or when git cannot answer (absent binary, bare
|
|
127
|
+
* repository, unreadable `.git`): a boundary that cannot be measured is a
|
|
128
|
+
* boundary absent, and the walk degrades to its previous unbounded climb
|
|
129
|
+
* rather than refusing a root it never looked at. A repository git genuinely
|
|
130
|
+
* cannot read is caught loudly one call later by `listTrackedFiles`, whose
|
|
131
|
+
* own spawn has no `null` answer.
|
|
132
|
+
*/
|
|
133
|
+
function gitTopLevelOf(from) {
|
|
134
|
+
try {
|
|
135
|
+
// `stderr: "ignore"` because a directory no repository encloses is the
|
|
136
|
+
// COMMON case this probe must answer quietly — git's `fatal: not a git
|
|
137
|
+
// repository` belongs nowhere near a user's terminal for it.
|
|
138
|
+
return runProcess("git", ["rev-parse", "--show-toplevel"], from, undefined, {
|
|
139
|
+
stderr: "ignore",
|
|
140
|
+
}).trim();
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Do `a` and `b` name the same directory? `git rev-parse --show-toplevel`
|
|
148
|
+
* answers a fully resolved path while the walk holds the spelling it was
|
|
149
|
+
* given — on macOS every `os.tmpdir()` path starts `/var/folders/…`, whose
|
|
150
|
+
* real spelling is `/private/var/folders/…`, and a plain `===` would let the
|
|
151
|
+
* ceiling silently never bind there. Doubt answers false: a boundary that
|
|
152
|
+
* fails to bind is the old walk, while one that binds wrongly refuses a real
|
|
153
|
+
* workspace.
|
|
154
|
+
*/
|
|
155
|
+
const sameDirectory = (a, b) => {
|
|
156
|
+
if (a === b) return true;
|
|
157
|
+
try {
|
|
158
|
+
return realpathSync(a) === realpathSync(b);
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
83
164
|
/**
|
|
84
165
|
* Every tracked file in the workspace, workspace-relative.
|
|
85
166
|
*
|
|
@@ -536,26 +617,34 @@ export function analyzeWorkspace(workspace, files, { analyze = analyzeFile } = {
|
|
|
536
617
|
return { imports, failures, analyzed: analyzedFiles.length, analyzedFiles };
|
|
537
618
|
}
|
|
538
619
|
|
|
539
|
-
/** The
|
|
540
|
-
const POLYGLOT_MANIFEST_NAMES = [
|
|
620
|
+
/** The polyglot manifests `polyglotManifests` looks for. */
|
|
621
|
+
const POLYGLOT_MANIFEST_NAMES = [
|
|
622
|
+
"go.mod",
|
|
623
|
+
"Cargo.toml",
|
|
624
|
+
"pyproject.toml",
|
|
625
|
+
"pom.xml",
|
|
626
|
+
"settings.gradle",
|
|
627
|
+
"settings.gradle.kts",
|
|
628
|
+
"*.csproj",
|
|
629
|
+
];
|
|
541
630
|
|
|
542
631
|
/**
|
|
543
|
-
* Tracked Go, Rust and
|
|
544
|
-
* the fact the unregistered-Nx-plugin gap turns on
|
|
632
|
+
* Tracked Go, Rust, Python, Maven and .NET manifests that sit under some project's
|
|
633
|
+
* root — the fact the unregistered-Nx-plugin gap turns on
|
|
545
634
|
* (`./commands/context.mjs`'s `pluginGap.manifests` is where a caller reads
|
|
546
635
|
* it, and `./options.mjs`'s `pluginIsRegistered` is the other half of that
|
|
547
|
-
* gap). A workspace running under Nx draws no edge for any of these
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
636
|
+
* gap). A workspace running under Nx draws no edge for any of these languages
|
|
637
|
+
* unless this plugin is registered in `nx.json` — Nx parses only TypeScript
|
|
638
|
+
* and JavaScript imports natively (`../../../AGENTS.md`, "for the other
|
|
639
|
+
* three both go quiet") — so a tracked manifest with no registered plugin is
|
|
640
|
+
* exactly the silent hole that invariant refuses. This function only names
|
|
641
|
+
* the manifests; it does not decide whether the plugin is registered. The
|
|
642
|
+
* pair the gap turns on is wired in twice today: `resolveCommandContext`
|
|
643
|
+
* reads both into its `pluginGap`, which every descriptive command refuses
|
|
644
|
+
* on, while `check` renders the same fact as a `coverageGaps`
|
|
645
|
+
* degraded-coverage note rather than a refusal (`../cli.mjs`) — the checker's
|
|
646
|
+
* own analysis covers what the graph does not, so a note is the right level
|
|
647
|
+
* there.
|
|
559
648
|
*
|
|
560
649
|
* Root matching mirrors `projectOwning`'s longest-prefix attribution of a
|
|
561
650
|
* source file: a project rooted at the workspace root (`root: ""` or `"."`)
|
|
@@ -572,7 +661,10 @@ export function polyglotManifests(tracked, projects) {
|
|
|
572
661
|
const roots = projects.map((project) => project.root);
|
|
573
662
|
return tracked.filter((file) => {
|
|
574
663
|
const base = file.slice(file.lastIndexOf("/") + 1);
|
|
575
|
-
|
|
664
|
+
// Literal-first: four of the five names answer by equality, and only
|
|
665
|
+
// `*.csproj` reaches the glob — this filter runs once per tracked file
|
|
666
|
+
// (`./analysis/manifest-util.mjs`'s `basenameMatches` owns why).
|
|
667
|
+
if (!basenameMatches(base, POLYGLOT_MANIFEST_NAMES, posix.matchesGlob)) return false;
|
|
576
668
|
return roots.some(
|
|
577
669
|
(root) => root === "" || root === "." || file === root || file.startsWith(`${root}/`),
|
|
578
670
|
);
|