@ecoma-io/archkeep 0.15.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.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/cli.mjs +126 -4
  3. package/commands.mjs +6 -0
  4. package/lsp.mjs +15 -2
  5. package/package.json +6 -2
  6. package/src/analysis/analyze.mjs +15 -0
  7. package/src/analysis/contract.md +36 -18
  8. package/src/analysis/csharp.mjs +485 -0
  9. package/src/analysis/dotnet/csproj.mjs +380 -0
  10. package/src/analysis/dotnet/mask.mjs +178 -0
  11. package/src/analysis/dotnet/namespaces.mjs +172 -0
  12. package/src/analysis/dotnet/resolve.mjs +89 -0
  13. package/src/analysis/go.mjs +289 -5
  14. package/src/analysis/java.mjs +329 -0
  15. package/src/analysis/jvm/gradle.mjs +545 -0
  16. package/src/analysis/jvm/mask.mjs +170 -0
  17. package/src/analysis/jvm/maven.mjs +612 -0
  18. package/src/analysis/jvm/packages.mjs +209 -0
  19. package/src/analysis/jvm/resolve.mjs +139 -0
  20. package/src/analysis/kotlin.mjs +210 -0
  21. package/src/analysis/manifest-util.mjs +30 -0
  22. package/src/analysis/python.mjs +3 -2
  23. package/src/analysis/registry.mjs +11 -0
  24. package/src/analysis/rust.mjs +171 -17
  25. package/src/analysis/source-util.mjs +155 -6
  26. package/src/analysis/typescript.mjs +9 -2
  27. package/src/commands/context.mjs +84 -14
  28. package/src/commands/provenance.mjs +7 -44
  29. package/src/commands/rules.mjs +775 -0
  30. package/src/governance/profile-registry.mjs +0 -1
  31. package/src/graph/create-dependencies.mjs +138 -15
  32. package/src/lsp/diagnose.mjs +1 -1
  33. package/src/lsp/server.mjs +97 -1
  34. package/src/lsp/workspace-index.mjs +106 -15
  35. package/src/options.mjs +30 -7
  36. package/src/process.mjs +10 -1
  37. package/src/providers/moon.mjs +287 -36
  38. package/src/providers/native/differential.fixtures.mjs +32 -6
  39. package/src/providers/native/discover.mjs +83 -4
  40. package/src/providers/native/graph.mjs +58 -0
  41. package/src/providers/native/model.mjs +59 -1
  42. package/src/rules/index.mjs +21 -6
  43. package/src/rules/reachability.mjs +2 -0
  44. package/src/rules/tags.mjs +7 -5
  45. package/src/rules/topology.mjs +5 -3
  46. package/src/workspace.mjs +115 -23
@@ -26,9 +26,16 @@ import { existsSync, readFileSync } from "node:fs";
26
26
  import { join } from "node:path";
27
27
 
28
28
  import { containmentViolation } from "../containment.mjs";
29
+ import { dotnetIndexFailures } from "../analysis/dotnet/namespaces.mjs";
30
+ import { dotnetManifestFailures } from "../analysis/dotnet/csproj.mjs";
31
+ import { goManifestFailures } from "../analysis/go.mjs";
32
+ import { mavenManifestFailures } from "../analysis/jvm/maven.mjs";
33
+ import { gradleManifestFailures } from "../analysis/jvm/gradle.mjs";
34
+ import { jvmIndexFailures } from "../analysis/jvm/packages.mjs";
29
35
  import { languageOf } from "../analysis/registry.mjs";
30
36
  import { pythonUnmodelledFailures } from "../analysis/python.mjs";
31
- import { fileFailure } from "../analysis/source-util.mjs";
37
+ import { rustManifestFailures } from "../analysis/rust.mjs";
38
+ import { dedupeWholeFileFailures, fileFailure } from "../analysis/source-util.mjs";
32
39
  import {
33
40
  DEFAULT_OPTIONS,
34
41
  NX_CONFIG_FILE,
@@ -39,13 +46,18 @@ import {
39
46
  import { readProjectGraph } from "../providers/nx.mjs";
40
47
  import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
41
48
  import {
42
- MOON_DIR,
43
- MOON_ALT_DIR,
49
+ MOON_ALT_WORKSPACE_MARKER,
50
+ MOON_WORKSPACE_MARKER,
44
51
  mergeImportEdges,
45
52
  moonMarkerAt,
46
53
  moonProvider,
47
54
  } from "../providers/moon.mjs";
48
55
  import { nativeProvider } from "../providers/native/index.mjs";
56
+ import { mergeDeclaredEdges } from "../providers/native/graph.mjs";
57
+ import {
58
+ resolveDeclaredManifestEdges,
59
+ resolveDeclaredManifestFailures,
60
+ } from "../graph/create-dependencies.mjs";
49
61
  import {
50
62
  analyzeWorkspace,
51
63
  annotateMFERemotes,
@@ -173,9 +185,22 @@ export function requireSingleProjectModel(root, { exists = existsSync } = {}) {
173
185
  * two callers still differ in posture — one throws where the other returns a
174
186
  * default — but they may not differ in what a workspace root IS.
175
187
  *
188
+ * The Moon entries are the SHAPED markers — `workspace.yml` inside the
189
+ * directory, not the directory alone (`../providers/moon.mjs`'s
190
+ * `MOON_WORKSPACE_MARKER` owns why): a walk reading directory existence
191
+ * selected the user's home directory as a workspace through `~/.moon`,
192
+ * moonrepo's user-level state directory (#339). A bare `.moon` beside an
193
+ * `nx.json` at a root already chosen is a different question, and stays with
194
+ * `requireSingleProjectModel`'s directory-presence gate below.
195
+ *
176
196
  * @type {string[]}
177
197
  */
178
- export const WORKSPACE_MARKERS = [NX_CONFIG_FILE, ARCHKEEP_MODEL_FILE, MOON_DIR, MOON_ALT_DIR];
198
+ export const WORKSPACE_MARKERS = [
199
+ NX_CONFIG_FILE,
200
+ ARCHKEEP_MODEL_FILE,
201
+ MOON_WORKSPACE_MARKER,
202
+ MOON_ALT_WORKSPACE_MARKER,
203
+ ];
179
204
 
180
205
  /**
181
206
  * @typedef {object} CommandContext
@@ -495,9 +520,11 @@ export function resolveCommandContext(
495
520
  if (root === null) {
496
521
  throw new Error(
497
522
  `archkeep: no workspace root above ${cwd} — looked for an nx.json, a archkeep.json, or a ` +
498
- `.moon (or .config/moon) directory in every parent. The tree to judge is found from the working directory, ` +
499
- `never from this tool's own location: installed from the registry, this tool lives under ` +
500
- `the consumer's node_modules and the two are always different trees.`,
523
+ `.moon/workspace.yml (or .config/moon/workspace.yml) in every parent, stopping at the top ` +
524
+ `level of the enclosing git repository: beyond it, a marker such as ~/.moon is user-level ` +
525
+ `tooling state, not this workspace's root. The tree to judge is found from the working ` +
526
+ `directory, never from this tool's own location: installed from the registry, this tool ` +
527
+ `lives under the consumer's node_modules and the two are always different trees.`,
501
528
  );
502
529
  }
503
530
  // Which provider may judge at all — the one gate
@@ -573,6 +600,22 @@ export function resolveCommandContext(
573
600
  });
574
601
  annotateMFERemotes(graph.nodes, workspace.readFile);
575
602
  annotatePackageFacts(graph.nodes, workspace.readFile);
603
+ // The manifest track's edges, folded into the graph the verdicts judge.
604
+ // ADR 0006 Decision 3 and ADR 0005 Decision 4 draw declared edges with no
605
+ // face qualifier, and this branch's graph IS the verdict input — without
606
+ // the fold, a cycle closed only by a `<ProjectReference>` (or a Maven
607
+ // `<dependency>`, or a Gradle `project(":x")`) reports empty, the one
608
+ // result this tool may never produce. The failure list doubles as the
609
+ // no-throw guard (`../graph/create-dependencies.mjs`'s
610
+ // `resolveDeclaredManifestFailures` — each resolver refuses on exactly
611
+ // the failures its `*ManifestFailures` twin reports, same memoized
612
+ // model), and the same list joins the funnel below, so a tree that WOULD
613
+ // refuse still refuses through the structured could-not-complete
614
+ // envelope instead of an unhandled error.
615
+ const manifestRefusalFailures = resolveDeclaredManifestFailures(workspace);
616
+ if (manifestRefusalFailures.length === 0) {
617
+ mergeDeclaredEdges(graph, resolveDeclaredManifestEdges(workspace));
618
+ }
576
619
 
577
620
  // `boundaryConfigDeclared` is carried straight off the model rather than
578
621
  // re-derived here: `../providers/native/model.mjs`'s
@@ -605,15 +648,21 @@ export function resolveCommandContext(
605
648
  // sources feeding the SAME whole-file failure shape a language analyzer
606
649
  // produces for an unreadable file, so nothing downstream needs to know
607
650
  // which provider found the gap.
608
- failures = [
651
+ failures = dedupeWholeFileFailures([
609
652
  ...wholeTreeAnalysis.failures.filter((failure) => selectedFiles.has(failure.sourceFile)),
610
653
  ...discovered.failures,
611
654
  // Workspace-scoped on purpose, the same posture the two unclaimed
612
655
  // equivalents above hold: a wildcard run must not be able to hide a
613
656
  // project whose manifest it cannot read by naming a path that excludes
614
- // it (`../analysis/python.mjs`'s `pythonUnmodelledFailures`).
657
+ // it (`../analysis/python.mjs`'s `pythonUnmodelledFailures`). The four
658
+ // manifest lists are computed once above, where the declared-edge fold
659
+ // needs them as its no-throw guard.
615
660
  ...pythonUnmodelledFailures(workspace),
616
- ];
661
+ ...manifestRefusalFailures,
662
+ ...goManifestFailures(workspace),
663
+ ...rustManifestFailures(workspace),
664
+ ...jvmIndexFailures(workspace),
665
+ ]);
617
666
  analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
618
667
  analyzed = analyzedFiles.length;
619
668
  // Unaffected by `paths`: an exempted file is by definition unowned by any
@@ -703,6 +752,16 @@ export function resolveCommandContext(
703
752
  importSites: wholeTreeAnalysis.imports,
704
753
  projectOf: (file) => projectOfFile.get(file),
705
754
  });
755
+ // The manifest track folds in after the import sites, for the same
756
+ // no-face-qualifier reason the native branch above states at its own
757
+ // fold — Moon has no plugin hook to draw declared edges either, and its
758
+ // `dependsOn` entries are a different track (`moon.yml`), not a
759
+ // substitute for Maven/Gradle/csproj manifests tracked inside its
760
+ // projects. Same provable guard, same funnel reuse.
761
+ const manifestRefusalFailures = resolveDeclaredManifestFailures(workspace);
762
+ if (manifestRefusalFailures.length === 0) {
763
+ mergeDeclaredEdges(graph, resolveDeclaredManifestEdges(workspace));
764
+ }
706
765
 
707
766
  const selected = selectFiles(
708
767
  owned.map(({ file }) => file),
@@ -717,11 +776,15 @@ export function resolveCommandContext(
717
776
  // analyzes the whole tree before `paths` narrows anything, for the same
718
777
  // reason).
719
778
  const unclaimedFiles = unclaimedAnalyzableFiles({ tracked, owned });
720
- failures = [
779
+ failures = dedupeWholeFileFailures([
721
780
  ...wholeTreeAnalysis.failures.filter((failure) => selectedFiles.has(failure.sourceFile)),
722
781
  ...unclaimedFileFailures({ files: unclaimedFiles, providerLabel: "the Moon project graph" }),
723
782
  ...pythonUnmodelledFailures(workspace),
724
- ];
783
+ ...manifestRefusalFailures,
784
+ ...goManifestFailures(workspace),
785
+ ...rustManifestFailures(workspace),
786
+ ...jvmIndexFailures(workspace),
787
+ ]);
725
788
  unclaimedGap = { files: unclaimedFiles };
726
789
  analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
727
790
  analyzed = analyzedFiles.length;
@@ -778,11 +841,18 @@ export function resolveCommandContext(
778
841
  // `discovered.failures` has, so a scoped `check <path>` cannot hide an
779
842
  // orphan file elsewhere in the tree by naming a path that excludes it.
780
843
  const unclaimedFiles = unclaimedAnalyzableFiles({ tracked, owned });
781
- failures = [
844
+ failures = dedupeWholeFileFailures([
782
845
  ...failures,
783
846
  ...unclaimedFileFailures({ files: unclaimedFiles, providerLabel: "the Nx project graph" }),
784
847
  ...pythonUnmodelledFailures(workspace),
785
- ];
848
+ ...goManifestFailures(workspace),
849
+ ...rustManifestFailures(workspace),
850
+ ...mavenManifestFailures(workspace),
851
+ ...gradleManifestFailures(workspace),
852
+ ...jvmIndexFailures(workspace),
853
+ ...dotnetManifestFailures(workspace),
854
+ ...dotnetIndexFailures(workspace),
855
+ ]);
786
856
  unclaimedGap = { files: unclaimedFiles };
787
857
  // Same reason as the Moon branch above: `coverage.exempt` is a native-only
788
858
  // concept, so an Nx workspace has nothing to report here.
@@ -24,9 +24,7 @@
24
24
  * timestamp is not.
25
25
  */
26
26
 
27
- import { execFileSync } from "node:child_process";
28
-
29
- import { environmentForTree } from "../process.mjs";
27
+ import { runProcess } from "../process.mjs";
30
28
 
31
29
  /**
32
30
  * Resolves repository provenance from the workspace root.
@@ -49,22 +47,12 @@ import { environmentForTree } from "../process.mjs";
49
47
  * @throws {Error} when `root` is a git repository with no commits.
50
48
  */
51
49
  export function resolveProvenance(root) {
52
- // G-09: every git spawn routes through the shared environment guard, so an
53
- // ambient GIT_DIR/GIT_WORK_TREE from a wrapping tool (the editor hooks, an
54
- // outer `git` call) can never make these spawns read a repository other
55
- // than the tree at `root`.
56
- const env = environmentForTree();
57
50
  // First, the "is this even a git repository at all" question, asked before
58
51
  // `rev-parse HEAD` so an unborn HEAD (a commitless repo) is distinguishable
59
52
  // from "not a repo" — the two must not share the `null` answer, because
60
53
  // only the first is a legitimate "no origin claim".
61
54
  try {
62
- execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
63
- cwd: root,
64
- env,
65
- encoding: "utf-8",
66
- stdio: ["pipe", "pipe", "pipe"],
67
- });
55
+ runProcess("git", ["rev-parse", "--is-inside-work-tree"], root);
68
56
  } catch {
69
57
  // git not available, or not a git repository. Return null — the envelope
70
58
  // carries no origin claim rather than a false one.
@@ -72,12 +60,7 @@ export function resolveProvenance(root) {
72
60
  }
73
61
  let commit;
74
62
  try {
75
- commit = execFileSync("git", ["rev-parse", "--verify", "HEAD"], {
76
- cwd: root,
77
- env,
78
- encoding: "utf-8",
79
- stdio: ["pipe", "pipe", "pipe"],
80
- }).trim();
63
+ commit = runProcess("git", ["rev-parse", "--verify", "HEAD"], root).trim();
81
64
  } catch {
82
65
  // `--is-inside-work-tree` passed above, so a repo EXISTS here, and
83
66
  // `--verify` (the two-argument form) failed to resolve HEAD. Two states
@@ -95,12 +78,7 @@ export function resolveProvenance(root) {
95
78
  // means the HEAD ref is broken.
96
79
  let reachable;
97
80
  try {
98
- reachable = execFileSync("git", ["rev-list", "--count", "--all"], {
99
- cwd: root,
100
- env,
101
- encoding: "utf-8",
102
- stdio: ["pipe", "pipe", "pipe"],
103
- }).trim();
81
+ reachable = runProcess("git", ["rev-list", "--count", "--all"], root).trim();
104
82
  } catch {
105
83
  // `rev-list --all` failing too is a degenerate repo; treat it as unborn
106
84
  // rather than inventing a third class.
@@ -124,21 +102,11 @@ export function resolveProvenance(root) {
124
102
  // the tree.
125
103
  let remote = null;
126
104
  try {
127
- const remotes = execFileSync("git", ["remote"], {
128
- cwd: root,
129
- env,
130
- encoding: "utf-8",
131
- stdio: ["pipe", "pipe", "pipe"],
132
- }).trim();
105
+ const remotes = runProcess("git", ["remote"], root).trim();
133
106
  if (remotes) {
134
107
  // Use the first remote's URL — typically "origin".
135
108
  const firstRemote = remotes.split("\n")[0].trim();
136
- remote = execFileSync("git", ["remote", "get-url", firstRemote], {
137
- cwd: root,
138
- env,
139
- encoding: "utf-8",
140
- stdio: ["pipe", "pipe", "pipe"],
141
- }).trim();
109
+ remote = runProcess("git", ["remote", "get-url", firstRemote], root).trim();
142
110
  }
143
111
  } catch {
144
112
  // No remotes configured — `remote` stays null.
@@ -147,12 +115,7 @@ export function resolveProvenance(root) {
147
115
  // Dirty: any uncommitted change to tracked files means the working tree
148
116
  // does not match the commit. A baseline from a dirty tree is not a
149
117
  // reproducible claim about that commit.
150
- const status = execFileSync("git", ["status", "--porcelain"], {
151
- cwd: root,
152
- env,
153
- encoding: "utf-8",
154
- stdio: ["pipe", "pipe", "pipe"],
155
- }).trim();
118
+ const status = runProcess("git", ["status", "--porcelain"], root).trim();
156
119
  const dirty = status.length > 0;
157
120
 
158
121
  return { commit, remote, dirty };