@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.
Files changed (68) hide show
  1. package/README.md +11 -5
  2. package/cli.mjs +571 -61
  3. package/commands.mjs +57 -0
  4. package/lsp.mjs +15 -2
  5. package/package.json +8 -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 +11 -3
  27. package/src/commands/README.md +52 -1
  28. package/src/commands/change-intent.mjs +461 -0
  29. package/src/commands/change.mjs +612 -0
  30. package/src/commands/check.mjs +2 -1
  31. package/src/commands/context.mjs +124 -16
  32. package/src/commands/custom-rules.mjs +286 -2
  33. package/src/commands/delta-classify.mjs +195 -33
  34. package/src/commands/delta-snapshot.mjs +156 -1
  35. package/src/commands/delta.mjs +142 -17
  36. package/src/commands/diff.mjs +41 -13
  37. package/src/commands/evolution.mjs +473 -0
  38. package/src/commands/history.mjs +130 -103
  39. package/src/commands/policy.mjs +57 -0
  40. package/src/commands/provenance.mjs +7 -44
  41. package/src/commands/rules.mjs +775 -0
  42. package/src/commands/trajectory.mjs +437 -0
  43. package/src/governance/profile-registry.mjs +0 -1
  44. package/src/graph/create-dependencies.mjs +138 -15
  45. package/src/lsp/diagnose.mjs +1 -1
  46. package/src/lsp/server.mjs +97 -1
  47. package/src/lsp/workspace-index.mjs +106 -15
  48. package/src/options.mjs +30 -7
  49. package/src/path-util.mjs +40 -0
  50. package/src/process.mjs +10 -1
  51. package/src/providers/moon.mjs +287 -36
  52. package/src/providers/native/differential.fixtures.mjs +32 -6
  53. package/src/providers/native/discover.mjs +83 -4
  54. package/src/providers/native/graph.mjs +58 -0
  55. package/src/providers/native/model.mjs +59 -1
  56. package/src/report/change-text.mjs +148 -0
  57. package/src/report/delta-text.mjs +82 -1
  58. package/src/report/evolution-text.mjs +83 -0
  59. package/src/report/history-text.mjs +4 -114
  60. package/src/report/sarif.mjs +255 -0
  61. package/src/report/snapshot-text.mjs +123 -0
  62. package/src/report/trajectory-text.mjs +143 -0
  63. package/src/rules/index.mjs +21 -6
  64. package/src/rules/reachability.mjs +2 -0
  65. package/src/rules/tags.mjs +7 -5
  66. package/src/rules/topology.mjs +5 -3
  67. package/src/tsconfig-paths.mjs +3 -2
  68. package/src/workspace.mjs +115 -23
package/commands.mjs ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The command layer's public face — what an integration imports from this
3
+ * package's `./commands` subpath to run the same commands `cli.mjs` runs,
4
+ * in-process, without spawning the CLI or parsing its text output.
5
+ *
6
+ * It holds no logic on purpose, the same bargain `./nx` (`nx.mjs`) makes: a
7
+ * named entry that is a re-export, so the command layer can grow under
8
+ * `src/commands/` without a second copy of any decision appearing beside it.
9
+ * `cli.mjs` and this file are two faces of one layer — the CLI owns argv,
10
+ * output destinations and exit codes; an importer of this face owns those for
11
+ * itself, and gets back exactly what the command functions return: statuses,
12
+ * payloads and reports, never a printed byte.
13
+ *
14
+ * Who this is for: integrations that compose the CLI's verbs programmatically
15
+ * — `packages/archkeep-mcp`, the agent capability interface, is the first. The
16
+ * engine primitives (discovery and judgment) remain the root entry
17
+ * (`index.mjs`); a caller that wants to JUDGE import records itself composes
18
+ * those, while a caller that wants the CLI's answers — a verdict, an impact
19
+ * set, an explanation — composes these. The two entries never overlap: a
20
+ * function exported here is never also exported there, so no integration can
21
+ * accidentally hold two spellings of one decision.
22
+ *
23
+ * The roster is exactly what the first consumer composes, no more — a
24
+ * function joins it when an integration calls it, never in anticipation of
25
+ * one. What is deliberately absent: the `run*` drivers and argv parsing
26
+ * (`cli.mjs` alone owns what a process's stdout and exit code mean), and the
27
+ * renderers (`src/report/` shapes bytes for humans; an importer reads the
28
+ * envelopes the commands already return).
29
+ *
30
+ * The seams the CLI threads are threaded the same way here: `readGraph` and
31
+ * `listFiles` are injectable on every entry point that reaches outside the
32
+ * process, so a caller drives the real analysis, rules and reports over a
33
+ * fixture tree with neither Nx nor git present.
34
+ */
35
+
36
+ export { resolveCommandContext, WORKSPACE_MARKERS } from "./src/commands/context.mjs";
37
+
38
+ export { resolveDescribedPolicy, resolvePolicy } from "./src/commands/policy.mjs";
39
+
40
+ export { check } from "./src/commands/check.mjs";
41
+ export { graphCommand } from "./src/commands/graph.mjs";
42
+ export { impactCommand } from "./src/commands/impact.mjs";
43
+ export { explainCommand } from "./src/commands/explain.mjs";
44
+ export { driftCommand } from "./src/commands/drift.mjs";
45
+ export { planContextCommand } from "./src/commands/plan-context-command.mjs";
46
+ export { historyCommand } from "./src/commands/history.mjs";
47
+ export { adrCommand } from "./src/commands/adr.mjs";
48
+ export { discoverCommand } from "./src/commands/discover.mjs";
49
+ export { reconcileCommand } from "./src/commands/reconcile.mjs";
50
+ export {
51
+ rulesAddCommand,
52
+ rulesInfoCommand,
53
+ rulesListCommand,
54
+ rulesVerifyCommand,
55
+ } from "./src/commands/rules.mjs";
56
+
57
+ export { UsageError } from "./src/errors.mjs";
package/lsp.mjs CHANGED
@@ -68,11 +68,24 @@ export function serve(input = process.stdin, output = process.stdout, onExit = n
68
68
  return;
69
69
  }
70
70
  pending = framed.rest;
71
- for (const message of framed.messages) server.handle(message);
71
+ for (const message of framed.messages) {
72
+ // `handle` is async and owns its own error envelope — a rejection here
73
+ // is a wiring defect, not a diagnostic. The catch reports it on stderr
74
+ // (the channel a client surfaces as its server log) instead of letting
75
+ // it become an unhandled rejection that kills the connection, or
76
+ // nothing at all, which would read as a clean frame.
77
+ server
78
+ .handle(message)
79
+ .catch((error) => process.stderr.write(`archkeep-lsp: ${error?.message ?? error}\n`));
80
+ }
72
81
  });
73
82
  // A client that closed the pipe without saying `exit` did not shut the server
74
83
  // down; reporting that as a clean stop would hide a crashed editor.
75
- input.on("end", () => server.handle({ jsonrpc: "2.0", method: "exit" }));
84
+ input.on("end", () => {
85
+ server
86
+ .handle({ jsonrpc: "2.0", method: "exit" })
87
+ .catch((error) => process.stderr.write(`archkeep-lsp: ${error?.message ?? error}\n`));
88
+ });
76
89
  return server;
77
90
  }
78
91
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ecoma-io/archkeep",
3
- "version": "0.14.0",
4
- "description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript and Vue",
3
+ "version": "0.16.0",
4
+ "description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript, Vue, Java and Kotlin",
5
5
  "keywords": [
6
6
  "architecture",
7
7
  "enforcement",
@@ -28,12 +28,14 @@
28
28
  "exports": {
29
29
  ".": "./index.mjs",
30
30
  "./nx": "./nx.mjs",
31
+ "./commands": "./commands.mjs",
31
32
  "./presets/*.json": "./presets/*.json",
32
33
  "./package.json": "./package.json"
33
34
  },
34
35
  "files": [
35
36
  "index.mjs",
36
37
  "nx.mjs",
38
+ "commands.mjs",
37
39
  "cli.mjs",
38
40
  "lsp.mjs",
39
41
  "src/",
@@ -51,6 +53,7 @@
51
53
  },
52
54
  "peerDependencies": {
53
55
  "@nx/eslint-plugin": ">=21",
56
+ "fast-xml-parser": "5.11.0",
54
57
  "nx": ">=21",
55
58
  "typescript": ">=5 <7",
56
59
  "vue": ">=3"
@@ -59,6 +62,9 @@
59
62
  "@nx/eslint-plugin": {
60
63
  "optional": true
61
64
  },
65
+ "fast-xml-parser": {
66
+ "optional": true
67
+ },
62
68
  "nx": {
63
69
  "optional": true
64
70
  },
@@ -22,7 +22,10 @@
22
22
  * exists. Every other import site keeps naming them from this module.
23
23
  */
24
24
 
25
+ import { analyzeCSharp } from "./csharp.mjs";
25
26
  import { analyzeGo } from "./go.mjs";
27
+ import { analyzeJava } from "./java.mjs";
28
+ import { analyzeKotlin } from "./kotlin.mjs";
26
29
  import { analyzePython } from "./python.mjs";
27
30
  import { LANGUAGE_BY_EXTENSION, languageOf } from "./registry.mjs";
28
31
  import { analyzeRust } from "./rust.mjs";
@@ -78,6 +81,15 @@ export { LANGUAGE_BY_EXTENSION, languageOf };
78
81
  * `./x` and `../x` in JavaScript, `crate::`/`self::`/`super::` and a sibling
79
82
  * crate target of the same Cargo package in Rust, a leading-dot import in
80
83
  * Python. It is the counter-evidence `noSelfCircularDependencies` looks for.
84
+ * @property {boolean} namesOnly The language's ONLY import spelling is a
85
+ * name — a Go module path, a Rust crate, a Python dotted module, a C#
86
+ * namespace, a Java or Kotlin package — so no specifier it produces is ever
87
+ * a filesystem path, and text beginning `libs/…` or `apps/…` is a name whose
88
+ * first segments are those words, not a path into a project. Constant on
89
+ * every record one analyzer produces, unlike `path` and `relative`, which
90
+ * answer per specifier. `isAbsoluteImportIntoAnotherProject` is gated on it
91
+ * (#376): false in the JavaScript family, where a bare `libs/x` is a deep
92
+ * import and a `/libs/x` an absolute path.
81
93
  */
82
94
 
83
95
  /**
@@ -170,6 +182,9 @@ const ANALYZER_BY_LANGUAGE = Object.freeze({
170
182
  go: analyzeGo,
171
183
  rust: analyzeRust,
172
184
  python: analyzePython,
185
+ java: analyzeJava,
186
+ kotlin: analyzeKotlin,
187
+ csharp: analyzeCSharp,
173
188
  });
174
189
 
175
190
  /**
@@ -71,23 +71,40 @@ beats every consumer remembering which convention this tool chose.
71
71
  ### How the specifier is spelled is a per-language fact, so the analyzer states it
72
72
 
73
73
  `specifier` is the raw text; `spelling` is what that text IS in the language it
74
- was written in. Two bits, because the rules ask two independent questions:
75
-
76
- | | `path` | `relative` |
77
- | --------------------------------- | :----: | :--------: |
78
- | `./x`, `../x`, `.`, `..` (JS) | yes | yes |
79
- | `/x` (JS) | yes | no |
80
- | `crate::x`, `self::x`, `super::x` | no | yes |
81
- | `rba_desktop_lib::run` from a bin | no | yes |
82
- | `.mod`, `..pkg.sub` (Python) | no | yes |
83
- | `react`, `serde`, `example.com/m` | no | no |
74
+ was written in. Three bits, because the rules ask three independent questions
75
+ the first two per specifier, the third per language and therefore constant on
76
+ every record one analyzer produces:
77
+
78
+ | | `path` | `relative` | `namesOnly` |
79
+ | --------------------------------- | :----: | :--------: | :---------: |
80
+ | `./x`, `../x`, `.`, `..` (JS) | yes | yes | no |
81
+ | `/x` (JS) | yes | no | no |
82
+ | `react` (JS, bare) | no | no | no |
83
+ | `crate::x`, `self::x`, `super::x` | no | yes | yes |
84
+ | `rba_desktop_lib::run` from a bin | no | yes | yes |
85
+ | `.mod`, `..pkg.sub` (Python) | no | yes | yes |
86
+ | `react`, `serde`, `example.com/m` | no | no | yes* |
87
+
88
+ \* the last row holds for `serde` and `example.com/m`; a JavaScript `react` is
89
+ `namesOnly: no` — the same text is a package name in one language and a name
90
+ that IS the only spelling in another, which is exactly why the bit rides on the
91
+ record instead of being derived from the text.
84
92
 
85
93
  `path` says the specifier is a **filesystem path**: resolvable by path
86
94
  arithmetic against the importing file, and naming no package. It decides
87
95
  whether a specifier may receive a synthesized external node, and which message
88
96
  an unresolvable one gets. `relative` says the specifier **reaches inside its
89
97
  own project without going out through the project's public name** — the
90
- counter-evidence `noSelfCircularDependencies` looks for.
98
+ counter-evidence `noSelfCircularDependencies` looks for. `namesOnly` says the
99
+ **language has no path spelling at all** — every specifier it produces is a
100
+ name, so no path-text rule applies to any of them. It gates
101
+ `isAbsoluteImportIntoAnotherProject`, which judges the JavaScript-family
102
+ spellings a bare `libs/x` deep import and a `/libs/x` absolute path: applied to
103
+ a language whose only spelling is the name, it read a Go `module libs/foo`'s
104
+ legal `import "libs/foo/bar"` as an absolute path into a project — an
105
+ unfixable verdict, because the name is the only spelling the language has
106
+ (#376). The edge such an import resolves to is still judged by every rule
107
+ below the spelling check; only the spelling check itself stands down.
91
108
 
92
109
  **Why the analyzer answers and not the rules.** The rules layer used to derive
93
110
  both from the text with one predicate — `.`, `..`, `./`, `../` — which is
@@ -101,10 +118,10 @@ import; the rules layer knows neither, and a language table there would be a
101
118
  second registry drifting from `LANGUAGE_BY_EXTENSION`. So the record carries the
102
119
  fact and the rule reads it.
103
120
 
104
- The field is **mandatory**, and `evaluate()` throws on a record that omits it
105
- rather than falling back to the JavaScript shape. A default is how the next
106
- analyzer inherits this bug silently; a throw is how it is told, once, at the
107
- first record it produces.
121
+ The field is **mandatory**, and `evaluate()` throws on a record that omits any
122
+ bit of it rather than falling back to the JavaScript shape. A default is how
123
+ the next analyzer inherits this bug silently; a throw is how it is told, once,
124
+ at the first record it produces.
108
125
 
109
126
  ### Intra-project imports are emitted too
110
127
 
@@ -208,9 +225,10 @@ rule cares about, and the pressure to fix it would be an ignore list that
208
225
  someone has to keep in sync with reality. An extension with no analyzer simply
209
226
  has no imports this tool can see, which is the truth.
210
227
 
211
- The registry is `LANGUAGE_BY_EXTENSION` in `analyze.mjs`, and it is the one
212
- place an extension is mapped. A language whose analyzer arrives adds itself
213
- there and nowhere else.
228
+ The registry is `LANGUAGE_BY_EXTENSION` in `registry.mjs` (re-exported by
229
+ `analyze.mjs`, so nothing that imports it from there has to change), and it is
230
+ the one place an extension is mapped. A language whose analyzer arrives adds
231
+ itself there and nowhere else.
214
232
 
215
233
  ## What an analyzer is handed
216
234