@ecoma-io/archkeep 0.13.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 (131) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +262 -0
  3. package/cli.mjs +2792 -0
  4. package/index.mjs +85 -0
  5. package/lsp.mjs +81 -0
  6. package/nx.mjs +24 -0
  7. package/package.json +81 -0
  8. package/presets/clean-architecture.json +78 -0
  9. package/presets/ddd-bounded-contexts.json +88 -0
  10. package/presets/hexagonal.json +68 -0
  11. package/presets/layered.json +92 -0
  12. package/presets/modular-monolith.json +85 -0
  13. package/presets/vertical-slice.json +68 -0
  14. package/src/analysis/analyze.mjs +218 -0
  15. package/src/analysis/contract.md +259 -0
  16. package/src/analysis/go.mjs +414 -0
  17. package/src/analysis/manifest-util.mjs +68 -0
  18. package/src/analysis/python.mjs +1266 -0
  19. package/src/analysis/registry.mjs +74 -0
  20. package/src/analysis/rust.mjs +674 -0
  21. package/src/analysis/source-util.mjs +230 -0
  22. package/src/analysis/typescript.mjs +1034 -0
  23. package/src/analysis/vue.mjs +156 -0
  24. package/src/architecture-intent/intent-fingerprint.mjs +29 -0
  25. package/src/architecture-intent/judge.mjs +539 -0
  26. package/src/architecture-intent/model.mjs +703 -0
  27. package/src/architecture-intent/selectors.mjs +170 -0
  28. package/src/canonical.mjs +48 -0
  29. package/src/commands/README.md +266 -0
  30. package/src/commands/adr.mjs +248 -0
  31. package/src/commands/check.mjs +989 -0
  32. package/src/commands/context-command.mjs +212 -0
  33. package/src/commands/context.mjs +790 -0
  34. package/src/commands/custom-rules.mjs +428 -0
  35. package/src/commands/debt.mjs +218 -0
  36. package/src/commands/diff.mjs +523 -0
  37. package/src/commands/discover.mjs +159 -0
  38. package/src/commands/drift.mjs +473 -0
  39. package/src/commands/edge-constraints.mjs +355 -0
  40. package/src/commands/explain.mjs +359 -0
  41. package/src/commands/fitness.mjs +226 -0
  42. package/src/commands/graph.mjs +297 -0
  43. package/src/commands/health.mjs +213 -0
  44. package/src/commands/history.mjs +614 -0
  45. package/src/commands/impact.mjs +226 -0
  46. package/src/commands/plan-context-command.mjs +496 -0
  47. package/src/commands/policy.mjs +138 -0
  48. package/src/commands/provenance-command.mjs +352 -0
  49. package/src/commands/provenance.mjs +159 -0
  50. package/src/commands/reconcile.mjs +219 -0
  51. package/src/commands/report.mjs +553 -0
  52. package/src/commands/snapshot-meta.mjs +107 -0
  53. package/src/commands/waivers.mjs +240 -0
  54. package/src/config.mjs +1308 -0
  55. package/src/containment.mjs +234 -0
  56. package/src/custom-rules/evidence.mjs +340 -0
  57. package/src/custom-rules/host.mjs +1023 -0
  58. package/src/custom-rules/values.mjs +43 -0
  59. package/src/entry-point.mjs +55 -0
  60. package/src/errors.mjs +36 -0
  61. package/src/eslint-config.mjs +542 -0
  62. package/src/go-work.mjs +394 -0
  63. package/src/governance/adr-registry.mjs +539 -0
  64. package/src/governance/clock.mjs +69 -0
  65. package/src/governance/debt-ledger.mjs +274 -0
  66. package/src/governance/discovery-proposal.mjs +423 -0
  67. package/src/governance/fitness-registry.mjs +504 -0
  68. package/src/governance/fitness-rules.mjs +668 -0
  69. package/src/governance/metrics.mjs +392 -0
  70. package/src/governance/preset-fingerprints.json +16 -0
  71. package/src/governance/profile-registry.mjs +366 -0
  72. package/src/governance/provenance-record.mjs +177 -0
  73. package/src/governance/reconcile-candidates.mjs +301 -0
  74. package/src/governance/reconcile-score.mjs +503 -0
  75. package/src/governance/row-schema.mjs +208 -0
  76. package/src/governance/verdict.mjs +127 -0
  77. package/src/governance/waiver.mjs +105 -0
  78. package/src/graph/create-dependencies.mjs +96 -0
  79. package/src/intent/intent-manifest.json +347 -0
  80. package/src/intent/mask-non-code.mjs +640 -0
  81. package/src/lsp/boundary-config.mjs +225 -0
  82. package/src/lsp/diagnose.mjs +202 -0
  83. package/src/lsp/diagnostics.mjs +241 -0
  84. package/src/lsp/protocol.mjs +215 -0
  85. package/src/lsp/server.mjs +922 -0
  86. package/src/lsp/workspace-index.mjs +891 -0
  87. package/src/nx-json.mjs +95 -0
  88. package/src/options.mjs +611 -0
  89. package/src/process.mjs +91 -0
  90. package/src/providers/moon.mjs +733 -0
  91. package/src/providers/native/README.md +204 -0
  92. package/src/providers/native/coverage.mjs +74 -0
  93. package/src/providers/native/differential.fixtures.mjs +1277 -0
  94. package/src/providers/native/discover.mjs +431 -0
  95. package/src/providers/native/graph.mjs +234 -0
  96. package/src/providers/native/index.mjs +152 -0
  97. package/src/providers/native/model.mjs +755 -0
  98. package/src/providers/nx.mjs +178 -0
  99. package/src/report/README.md +89 -0
  100. package/src/report/adr-text.mjs +129 -0
  101. package/src/report/context-text.mjs +109 -0
  102. package/src/report/debt-text.mjs +105 -0
  103. package/src/report/diff-text.mjs +219 -0
  104. package/src/report/discover-text.mjs +186 -0
  105. package/src/report/drift-text.mjs +194 -0
  106. package/src/report/envelope-shape.mjs +161 -0
  107. package/src/report/evidence.mjs +157 -0
  108. package/src/report/explain-text.mjs +159 -0
  109. package/src/report/graph-text.mjs +116 -0
  110. package/src/report/health-text.mjs +123 -0
  111. package/src/report/history-text.mjs +204 -0
  112. package/src/report/impact-text.mjs +128 -0
  113. package/src/report/json.mjs +173 -0
  114. package/src/report/plan-context-text.mjs +159 -0
  115. package/src/report/provenance-text.mjs +78 -0
  116. package/src/report/reconcile-text.mjs +159 -0
  117. package/src/report/report-text.mjs +264 -0
  118. package/src/report/sarif.mjs +953 -0
  119. package/src/report/text.mjs +823 -0
  120. package/src/report/waivers-text.mjs +100 -0
  121. package/src/rules/README.md +123 -0
  122. package/src/rules/index.mjs +962 -0
  123. package/src/rules/match.mjs +1708 -0
  124. package/src/rules/messages.mjs +73 -0
  125. package/src/rules/reachability.mjs +224 -0
  126. package/src/rules/specifiers.mjs +300 -0
  127. package/src/rules/tags.mjs +238 -0
  128. package/src/rules/topology.mjs +333 -0
  129. package/src/tsconfig-paths.mjs +237 -0
  130. package/src/verdict.mjs +145 -0
  131. package/src/workspace.mjs +580 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Edge-constraint analysis: which boundary-rule violations an edge introduces
3
+ * or removes, and which constraint rows govern a given edge.
4
+ *
5
+ * This is the bridge between the structural commands (`diff`, `impact`) and the
6
+ * boundary rules. Both commands operate on graph edges — not on import sites —
7
+ * so they cannot call `evaluate` directly. Instead, this module provides a
8
+ * narrower function that judges a single edge against the `depConstraints`
9
+ * table, which is the part of the boundary rules that depends only on project
10
+ * tags (not on npm imports, circular dependencies, lazy loading, etc.).
11
+ *
12
+ * `check` is a third caller, through `declaredEdgeViolationsForCheck` below —
13
+ * not for every edge, only the ones `evaluate()` structurally cannot reach: an
14
+ * `implicit`-typed edge (Nx's/`archkeep.json`'s `implicitDependencies`) has no
15
+ * import site behind it at all, so it never becomes an `importSites` record for
16
+ * `evaluate()` to iterate. Without this, `check` could report a clean tree
17
+ * while `context`/`impact` showed the exact same edge as a tag violation — the
18
+ * "empty result is a claim, not a shrug" invariant (`../../../AGENTS.md`)
19
+ * broken by omission rather than by a wrong answer.
20
+ *
21
+ * ## What it checks and what it does not
22
+ *
23
+ * It checks the three `depConstraints` verdicts:
24
+ * - `onlyDependOnLibsWithTags` — the target must carry at least one listed tag
25
+ * - `notDependOnLibsWithTags` — no project reachable from the target may carry
26
+ * a listed tag (transitive)
27
+ * - `projectWithoutTagsCannotHaveDependencies` — a source project whose tags
28
+ * match no constraint row
29
+ *
30
+ * It does NOT check npm/external imports, circular dependencies, lazy-loading,
31
+ * or the `allow` list. Those checks depend on import-site details (specifier
32
+ * text, import kind, file path) that graph edges do not carry — true of every
33
+ * edge `judgeEdge` judges, `check`'s own `implicit`-edge callers included: an
34
+ * edge with no import site cannot gain one by being judged from `check` rather
35
+ * than `impact`. A consumer who needs the full verdict should run `check`.
36
+ *
37
+ * ## Why this lives here and not in `src/rules/`
38
+ *
39
+ * `src/rules/` judges import sites; this module judges graph edges. They share
40
+ * the tag-matching functions (`findConstraintsFor`, `onlyTagsViolation`,
41
+ * `notTagsViolation`, `emptyOnlyTagsViolation`) but the input is different
42
+ * enough that merging them would blur the layer boundary the AGENTS.md guards.
43
+ */
44
+
45
+ import { renderMessage } from "../rules/messages.mjs";
46
+ import { buildReachability } from "../rules/reachability.mjs";
47
+ import {
48
+ emptyOnlyTagsViolation,
49
+ findConstraintsFor,
50
+ notTagsViolation,
51
+ onlyTagsViolation,
52
+ } from "../rules/tags.mjs";
53
+
54
+ /**
55
+ * Judges a single edge against the `depConstraints` table.
56
+ *
57
+ * Returns an array of violations (zero or more), each carrying the constraint
58
+ * row that fired. An empty array means the edge is allowed by the constraint
59
+ * table — it does NOT mean the edge is free of all boundary violations (see
60
+ * this module's header for what is not checked).
61
+ *
62
+ * @param {{source: string, target: string}} edge The graph edge to judge.
63
+ * @param {object} nodes The project graph's `nodes` map (carries `data.tags`).
64
+ * @param {object} dependencies The project graph's `dependencies` map (for `notDependOnLibsWithTags` reachability).
65
+ * @param {object[]} depConstraints The constraint table from the boundary config.
66
+ * @param {object} [reachability] Pre-computed reachability from `buildReachability`.
67
+ * When omitted, it is built on demand (the slow path for single-edge callers).
68
+ * When provided by a batch caller like `computeRuleImpact`, it is reused across
69
+ * all edges in the same graph, avoiding O(E) rebuilds of the same structure.
70
+ * @returns {{messageId: string, constraint: object|null, source: string, target: string, data: object, message: string}[]}
71
+ */
72
+ export function judgeEdge(edge, nodes, dependencies, depConstraints, reachability) {
73
+ const sourceNode = nodes[edge.source];
74
+ const targetNode = nodes[edge.target];
75
+
76
+ // If either project is not in the graph (external node, or removed), this
77
+ // edge cannot be judged against tag constraints — it is outside the
78
+ // depConstraints domain.
79
+ if (!sourceNode || !targetNode) return [];
80
+
81
+ const matchedConstraints = findConstraintsFor(depConstraints, sourceNode);
82
+
83
+ // No matching constraint row → `projectWithoutTagsCannotHaveDependencies`.
84
+ // This is the same semantics `findConstraintsFor` documents: an empty list
85
+ // is an error, not a pass.
86
+ if (matchedConstraints.length === 0) {
87
+ const messageId = "projectWithoutTagsCannotHaveDependencies";
88
+ return [
89
+ {
90
+ messageId,
91
+ constraint: null,
92
+ source: edge.source,
93
+ target: edge.target,
94
+ data: {},
95
+ message: renderMessage(messageId, {}),
96
+ },
97
+ ];
98
+ }
99
+
100
+ const violations = [];
101
+
102
+ for (const constraint of matchedConstraints) {
103
+ const onlyTags = onlyTagsViolation(constraint, targetNode);
104
+ if (onlyTags) {
105
+ violations.push({
106
+ messageId: onlyTags.messageId,
107
+ constraint,
108
+ source: edge.source,
109
+ target: edge.target,
110
+ data: onlyTags.data,
111
+ message: renderMessage(onlyTags.messageId, onlyTags.data),
112
+ });
113
+ continue;
114
+ }
115
+
116
+ const emptyOnly = emptyOnlyTagsViolation(constraint, targetNode);
117
+ if (emptyOnly) {
118
+ violations.push({
119
+ messageId: emptyOnly.messageId,
120
+ constraint,
121
+ source: edge.source,
122
+ target: edge.target,
123
+ data: emptyOnly.data,
124
+ message: renderMessage(emptyOnly.messageId, emptyOnly.data),
125
+ });
126
+ continue;
127
+ }
128
+
129
+ // `notDependOnLibsWithTags` is transitive — it needs reachability.
130
+ if (constraint.notDependOnLibsWithTags?.length > 0) {
131
+ const reach = reachability ?? buildReachability({ nodes, dependencies });
132
+ const notTags = notTagsViolation(constraint, targetNode, { nodes }, reach);
133
+ if (notTags) {
134
+ violations.push({
135
+ messageId: notTags.messageId,
136
+ constraint,
137
+ source: edge.source,
138
+ target: edge.target,
139
+ data: notTags.data,
140
+ message: renderMessage(notTags.messageId, notTags.data),
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ return violations;
147
+ }
148
+
149
+ /**
150
+ * Judges every `implicit`-typed edge in the graph against `depConstraints` —
151
+ * the declaration-only counterpart to `evaluate()`'s import-site judgment,
152
+ * for `check` specifically.
153
+ *
154
+ * `implicit` is the one edge type this package's own providers agree means
155
+ * "declared, not derived from code" — Nx emits it for a `project.json`'s
156
+ * `implicitDependencies` (verified against a real `nx graph --file=` run),
157
+ * and the native provider's `buildDependencies` (`../providers/native/graph.mjs`)
158
+ * assigns it for `archkeep.json`'s/`project.json`'s own `implicitDependencies`
159
+ * row. It is also the exact criterion `drift.mjs`'s `buildObserved` and
160
+ * `discover.mjs` already exclude architecture edges by — reused here rather
161
+ * than re-derived, so this check and `drift`'s exclusion can never disagree
162
+ * about which edges count as "declared".
163
+ *
164
+ * Only the three `depConstraints` verdicts `judgeEdge` covers are judged —
165
+ * the same 3-of-15 limit `context`/`impact` already document, and for the same
166
+ * reason: the other twelve rules need import-site details a declaration-only
167
+ * edge structurally does not carry.
168
+ *
169
+ * An empty `depConstraints` table returns no violations, matching
170
+ * `evaluate()`'s own early exit in `../rules/index.mjs`'s tag block
171
+ * — a workspace declaring no constraint table has opted out of dep-constraint
172
+ * enforcement entirely, on both the import-site and the declared-edge path.
173
+ * `judgeEdge` itself has no such guard (an empty table makes every project's
174
+ * `findConstraintsFor` return `[]`, i.e. `projectWithoutTagsCannotHaveDependencies`
175
+ * on every edge) because `context`/`impact` intentionally show that as a
176
+ * per-edge fact; folding the same into `check`'s pass/fail verdict would flag
177
+ * an opted-out workspace on every implicit edge for a reason `check`'s own
178
+ * import-site rules never would.
179
+ *
180
+ * @param {{nodes: object, dependencies: object}} graph
181
+ * @param {object[]} depConstraints The constraint table from the boundary config.
182
+ * @returns {{messageId: string, constraint: object|null, source: string, target: string, data: object, message: string}[]}
183
+ */
184
+ export function declaredEdgeViolationsForCheck(graph, depConstraints) {
185
+ if (depConstraints.length === 0) return [];
186
+ const reachability = buildReachability(graph);
187
+ const violations = [];
188
+ for (const [source, edges] of Object.entries(graph.dependencies ?? {})) {
189
+ for (const edge of edges) {
190
+ if (edge.type !== "implicit") continue;
191
+ violations.push(
192
+ ...judgeEdge(
193
+ { source, target: edge.target },
194
+ graph.nodes,
195
+ graph.dependencies,
196
+ depConstraints,
197
+ reachability,
198
+ ),
199
+ );
200
+ }
201
+ }
202
+ return violations;
203
+ }
204
+
205
+ /**
206
+ * Computes the rule-impact of a diff: which boundary violations the added
207
+ * edges introduce and which the removed edges resolve.
208
+ *
209
+ * For each added edge, judges it against the head graph's tags and the
210
+ * constraint table. For each removed edge, judges it against the baseline
211
+ * graph's tags (reconstructed from the baseline's project list) and the same
212
+ * constraint table. The constraint table is the current one — it is what `check`
213
+ * would judge from today, not what some past version judged from.
214
+ *
215
+ * @param {{addedEdges: object[], removedEdges: object[]}} diff From `computeDiff`.
216
+ * @param {object} headNodes The head graph's `nodes` map (for tag lookups).
217
+ * @param {object} headDependencies The head graph's `dependencies` map (for reachability).
218
+ * @param {object[]} baselineProjects The baseline snapshot's project list (each
219
+ * carries `tags`).
220
+ * @param {object[]} baselineDependencies The baseline snapshot's flat edge list.
221
+ * @param {object[]} depConstraints The constraint table from the boundary config.
222
+ * @returns {{introduced: object[], resolved: object[]}}
223
+ */
224
+ export function computeRuleImpact(
225
+ diff,
226
+ headNodes,
227
+ headDependencies,
228
+ baselineProjects,
229
+ baselineDependencies,
230
+ depConstraints,
231
+ ) {
232
+ // Build a node-map shape from the baseline's project list so `findConstraintsFor`
233
+ // can read tags from it the same way it reads from the live graph.
234
+ const baselineNodes = Object.create(null);
235
+ for (const project of baselineProjects) {
236
+ baselineNodes[project.name] = {
237
+ name: project.name,
238
+ data: { root: project.root, tags: project.tags ?? [] },
239
+ };
240
+ }
241
+
242
+ // Build a source-keyed dependency map from the baseline's flat edge list for
243
+ // reachability analysis in `notDependOnLibsWithTags` checks.
244
+ const baselineDepsMap = Object.create(null);
245
+ for (const edge of baselineDependencies) {
246
+ if (!baselineDepsMap[edge.source]) baselineDepsMap[edge.source] = [];
247
+ baselineDepsMap[edge.source].push({ target: edge.target, type: edge.type });
248
+ }
249
+
250
+ // Build reachability once for the head graph and once for the baseline graph,
251
+ // so that repeated `judgeEdge` calls with `notDependOnLibsWithTags` constraints
252
+ // do not rebuild the same structure per edge (the performance concern this
253
+ // memoization addresses).
254
+ const headReachability = buildReachability({ nodes: headNodes, dependencies: headDependencies });
255
+ const baselineReachability = buildReachability({
256
+ nodes: baselineNodes,
257
+ dependencies: baselineDepsMap,
258
+ });
259
+
260
+ const introduced = [];
261
+ for (const edge of diff.addedEdges) {
262
+ const violations = judgeEdge(
263
+ edge,
264
+ headNodes,
265
+ headDependencies,
266
+ depConstraints,
267
+ headReachability,
268
+ );
269
+ for (const v of violations) {
270
+ introduced.push(v);
271
+ }
272
+ }
273
+
274
+ const resolved = [];
275
+ for (const edge of diff.removedEdges) {
276
+ const violations = judgeEdge(
277
+ edge,
278
+ baselineNodes,
279
+ baselineDepsMap,
280
+ depConstraints,
281
+ baselineReachability,
282
+ );
283
+ for (const v of violations) {
284
+ resolved.push(v);
285
+ }
286
+ }
287
+
288
+ return { introduced, resolved };
289
+ }
290
+
291
+ /**
292
+ * Computes the constraint context for each dependent in an impact set.
293
+ *
294
+ * For each project that depends on the target, this identifies which constraint
295
+ * rows govern that edge and whether the edge currently passes or violates. The
296
+ * result carries enough detail for a report to name the constraint, state the
297
+ * verdict, and (for violations) show what the constraint requires.
298
+ *
299
+ * @param {string} targetProject The project whose dependents are being analysed.
300
+ * @param {string[]} dependents The impact set (direct + transitive).
301
+ * @param {object} nodes The project graph's `nodes` map.
302
+ * @param {object} dependencies The project graph's `dependencies` map.
303
+ * @param {object[]} depConstraints The constraint table from the boundary config.
304
+ * @returns {{project: string, edges: {target: string, type: string}[], constraintRows: object[], violations: object[]}[]}
305
+ */
306
+ export function computeImpactConstraints(
307
+ targetProject,
308
+ dependents,
309
+ nodes,
310
+ dependencies,
311
+ depConstraints,
312
+ ) {
313
+ // Build reachability once for the whole graph, so `judgeEdge` calls with
314
+ // `notDependOnLibsWithTags` constraints reuse the same structure instead of
315
+ // rebuilding it per dependent.
316
+ const reachability = buildReachability({ nodes, dependencies });
317
+
318
+ const results = [];
319
+
320
+ for (const dependent of dependents) {
321
+ // Find all edges from this dependent to the target.
322
+ const deps = dependencies[dependent] ?? [];
323
+ const edgesToTarget = deps.filter((e) => e.target === targetProject);
324
+
325
+ const sourceNode = nodes[dependent];
326
+ const constraintRows = sourceNode ? findConstraintsFor(depConstraints, sourceNode) : [];
327
+
328
+ const violations = [];
329
+ for (const edge of edgesToTarget) {
330
+ // The dependencies map is keyed by source — the edge object carries only
331
+ // `target` and `type`, so `source` must be added before `judgeEdge` can
332
+ // look up the source project's tags.
333
+ const edgeWithSource = { source: dependent, target: edge.target, type: edge.type };
334
+ const edgeViolations = judgeEdge(
335
+ edgeWithSource,
336
+ nodes,
337
+ dependencies,
338
+ depConstraints,
339
+ reachability,
340
+ );
341
+ for (const v of edgeViolations) {
342
+ violations.push(v);
343
+ }
344
+ }
345
+
346
+ results.push({
347
+ project: dependent,
348
+ edges: edgesToTarget.map((e) => ({ target: e.target, type: e.type })),
349
+ constraintRows,
350
+ violations,
351
+ });
352
+ }
353
+
354
+ return results;
355
+ }