@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.
- package/LICENSE +202 -0
- package/README.md +262 -0
- package/cli.mjs +2792 -0
- package/index.mjs +85 -0
- package/lsp.mjs +81 -0
- package/nx.mjs +24 -0
- package/package.json +81 -0
- package/presets/clean-architecture.json +78 -0
- package/presets/ddd-bounded-contexts.json +88 -0
- package/presets/hexagonal.json +68 -0
- package/presets/layered.json +92 -0
- package/presets/modular-monolith.json +85 -0
- package/presets/vertical-slice.json +68 -0
- package/src/analysis/analyze.mjs +218 -0
- package/src/analysis/contract.md +259 -0
- package/src/analysis/go.mjs +414 -0
- package/src/analysis/manifest-util.mjs +68 -0
- package/src/analysis/python.mjs +1266 -0
- package/src/analysis/registry.mjs +74 -0
- package/src/analysis/rust.mjs +674 -0
- package/src/analysis/source-util.mjs +230 -0
- package/src/analysis/typescript.mjs +1034 -0
- package/src/analysis/vue.mjs +156 -0
- package/src/architecture-intent/intent-fingerprint.mjs +29 -0
- package/src/architecture-intent/judge.mjs +539 -0
- package/src/architecture-intent/model.mjs +703 -0
- package/src/architecture-intent/selectors.mjs +170 -0
- package/src/canonical.mjs +48 -0
- package/src/commands/README.md +266 -0
- package/src/commands/adr.mjs +248 -0
- package/src/commands/check.mjs +989 -0
- package/src/commands/context-command.mjs +212 -0
- package/src/commands/context.mjs +790 -0
- package/src/commands/custom-rules.mjs +428 -0
- package/src/commands/debt.mjs +218 -0
- package/src/commands/diff.mjs +523 -0
- package/src/commands/discover.mjs +159 -0
- package/src/commands/drift.mjs +473 -0
- package/src/commands/edge-constraints.mjs +355 -0
- package/src/commands/explain.mjs +359 -0
- package/src/commands/fitness.mjs +226 -0
- package/src/commands/graph.mjs +297 -0
- package/src/commands/health.mjs +213 -0
- package/src/commands/history.mjs +614 -0
- package/src/commands/impact.mjs +226 -0
- package/src/commands/plan-context-command.mjs +496 -0
- package/src/commands/policy.mjs +138 -0
- package/src/commands/provenance-command.mjs +352 -0
- package/src/commands/provenance.mjs +159 -0
- package/src/commands/reconcile.mjs +219 -0
- package/src/commands/report.mjs +553 -0
- package/src/commands/snapshot-meta.mjs +107 -0
- package/src/commands/waivers.mjs +240 -0
- package/src/config.mjs +1308 -0
- package/src/containment.mjs +234 -0
- package/src/custom-rules/evidence.mjs +340 -0
- package/src/custom-rules/host.mjs +1023 -0
- package/src/custom-rules/values.mjs +43 -0
- package/src/entry-point.mjs +55 -0
- package/src/errors.mjs +36 -0
- package/src/eslint-config.mjs +542 -0
- package/src/go-work.mjs +394 -0
- package/src/governance/adr-registry.mjs +539 -0
- package/src/governance/clock.mjs +69 -0
- package/src/governance/debt-ledger.mjs +274 -0
- package/src/governance/discovery-proposal.mjs +423 -0
- package/src/governance/fitness-registry.mjs +504 -0
- package/src/governance/fitness-rules.mjs +668 -0
- package/src/governance/metrics.mjs +392 -0
- package/src/governance/preset-fingerprints.json +16 -0
- package/src/governance/profile-registry.mjs +366 -0
- package/src/governance/provenance-record.mjs +177 -0
- package/src/governance/reconcile-candidates.mjs +301 -0
- package/src/governance/reconcile-score.mjs +503 -0
- package/src/governance/row-schema.mjs +208 -0
- package/src/governance/verdict.mjs +127 -0
- package/src/governance/waiver.mjs +105 -0
- package/src/graph/create-dependencies.mjs +96 -0
- package/src/intent/intent-manifest.json +347 -0
- package/src/intent/mask-non-code.mjs +640 -0
- package/src/lsp/boundary-config.mjs +225 -0
- package/src/lsp/diagnose.mjs +202 -0
- package/src/lsp/diagnostics.mjs +241 -0
- package/src/lsp/protocol.mjs +215 -0
- package/src/lsp/server.mjs +922 -0
- package/src/lsp/workspace-index.mjs +891 -0
- package/src/nx-json.mjs +95 -0
- package/src/options.mjs +611 -0
- package/src/process.mjs +91 -0
- package/src/providers/moon.mjs +733 -0
- package/src/providers/native/README.md +204 -0
- package/src/providers/native/coverage.mjs +74 -0
- package/src/providers/native/differential.fixtures.mjs +1277 -0
- package/src/providers/native/discover.mjs +431 -0
- package/src/providers/native/graph.mjs +234 -0
- package/src/providers/native/index.mjs +152 -0
- package/src/providers/native/model.mjs +755 -0
- package/src/providers/nx.mjs +178 -0
- package/src/report/README.md +89 -0
- package/src/report/adr-text.mjs +129 -0
- package/src/report/context-text.mjs +109 -0
- package/src/report/debt-text.mjs +105 -0
- package/src/report/diff-text.mjs +219 -0
- package/src/report/discover-text.mjs +186 -0
- package/src/report/drift-text.mjs +194 -0
- package/src/report/envelope-shape.mjs +161 -0
- package/src/report/evidence.mjs +157 -0
- package/src/report/explain-text.mjs +159 -0
- package/src/report/graph-text.mjs +116 -0
- package/src/report/health-text.mjs +123 -0
- package/src/report/history-text.mjs +204 -0
- package/src/report/impact-text.mjs +128 -0
- package/src/report/json.mjs +173 -0
- package/src/report/plan-context-text.mjs +159 -0
- package/src/report/provenance-text.mjs +78 -0
- package/src/report/reconcile-text.mjs +159 -0
- package/src/report/report-text.mjs +264 -0
- package/src/report/sarif.mjs +953 -0
- package/src/report/text.mjs +823 -0
- package/src/report/waivers-text.mjs +100 -0
- package/src/rules/README.md +123 -0
- package/src/rules/index.mjs +962 -0
- package/src/rules/match.mjs +1708 -0
- package/src/rules/messages.mjs +73 -0
- package/src/rules/reachability.mjs +224 -0
- package/src/rules/specifiers.mjs +300 -0
- package/src/rules/tags.mjs +238 -0
- package/src/rules/topology.mjs +333 -0
- package/src/tsconfig-paths.mjs +237 -0
- package/src/verdict.mjs +145 -0
- package/src/workspace.mjs +580 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `impact` command: reverse reachability from the project graph.
|
|
3
|
+
*
|
|
4
|
+
* Given a project name, `impact` lists every project that transitively depends
|
|
5
|
+
* on it — the set a developer needs to consider before changing that project.
|
|
6
|
+
* It is descriptive: it never exits 1, because a description of what depends
|
|
7
|
+
* on a project is never a finding.
|
|
8
|
+
*
|
|
9
|
+
* The result separates **direct** dependents (projects whose edges point
|
|
10
|
+
* straight at the target) from **transitive** ones (reachable only through
|
|
11
|
+
* another project), and the union of both is `dependents`. An empty
|
|
12
|
+
* `dependents` list is a claim — "nothing depends on this project" — not a
|
|
13
|
+
* shrug, and it is worded that way in the report so a reader never mistakes it
|
|
14
|
+
* for silence.
|
|
15
|
+
*
|
|
16
|
+
* When a boundary config is provided (via `--config` or the workspace's own
|
|
17
|
+
* declaration), `impact` also returns the **constraint context** for each
|
|
18
|
+
* dependent: which `depConstraints` rows govern that edge and whether it
|
|
19
|
+
* currently violates them. This is narrower than `check` — it checks only
|
|
20
|
+
* `depConstraints` (tag-based), not npm/circular/lazy-load rules that need
|
|
21
|
+
* import-site details. A consumer who needs the complete verdict should run
|
|
22
|
+
* `check`.
|
|
23
|
+
*
|
|
24
|
+
* What it needs from its caller is a `CommandContext` — the preamble every
|
|
25
|
+
* command shares (`./context.mjs`). What it gives back is a `status`, the
|
|
26
|
+
* payload for both the text and the JSON renderers, and enough coverage
|
|
27
|
+
* information to build a correct envelope. It does not print, and it does not
|
|
28
|
+
* decide the process's exit code — `../../cli.mjs` owns those
|
|
29
|
+
* (`./README.md`).
|
|
30
|
+
*
|
|
31
|
+
* ## The unregistered-plugin refusal
|
|
32
|
+
*
|
|
33
|
+
* Same as `graph` and `diff`: on an Nx workspace whose `nx.json` does not
|
|
34
|
+
* register this plugin but whose tracked files include polyglot manifests
|
|
35
|
+
* under project roots, `impact` refuses loudly rather than returning a result
|
|
36
|
+
* whose dependents silently under-represent the real architecture.
|
|
37
|
+
*/
|
|
38
|
+
import { isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
39
|
+
import { UsageError } from "../errors.mjs";
|
|
40
|
+
import { computeImpactConstraints } from "./edge-constraints.mjs";
|
|
41
|
+
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
42
|
+
import { formatImpactReport } from "../report/impact-text.mjs";
|
|
43
|
+
import { resolveProvenance } from "./provenance.mjs";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes the impact set: every project that transitively depends on
|
|
47
|
+
* `projectName`.
|
|
48
|
+
*
|
|
49
|
+
* Builds a reverse adjacency map from the graph's `dependencies`, then walks
|
|
50
|
+
* it breadth-first starting from the target project. The walk does NOT include
|
|
51
|
+
* the target project itself in the dependent set — a project does not depend
|
|
52
|
+
* on itself — but the returned `dependents` array is the union of `direct`
|
|
53
|
+
* and `transitive`, and the report header names the target separately.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} projectName The project whose impact is being queried.
|
|
56
|
+
* @param {object} graph The project graph: `{nodes, dependencies}`.
|
|
57
|
+
* @returns {{project: string, direct: string[], transitive: string[], dependents: string[]}}
|
|
58
|
+
* @throws {UsageError} when `projectName` is not in the graph.
|
|
59
|
+
*/
|
|
60
|
+
export function computeImpact(projectName, graph) {
|
|
61
|
+
const nodes = graph.nodes;
|
|
62
|
+
const deps = graph.dependencies;
|
|
63
|
+
|
|
64
|
+
if (!Object.hasOwn(nodes, projectName)) {
|
|
65
|
+
throw new UsageError(
|
|
66
|
+
`archkeep: no project named '${projectName}' in the graph — ` +
|
|
67
|
+
`available projects: ${Object.keys(nodes)
|
|
68
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
69
|
+
.join(", ")}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build reverse adjacency: target → [sources that depend on it]
|
|
74
|
+
const reverseAdj = Object.create(null);
|
|
75
|
+
for (const source of Object.keys(deps)) {
|
|
76
|
+
if (!Object.hasOwn(deps, source)) continue;
|
|
77
|
+
const targets = deps[source];
|
|
78
|
+
for (const edge of targets) {
|
|
79
|
+
if (!Object.hasOwn(reverseAdj, edge.target)) {
|
|
80
|
+
reverseAdj[edge.target] = [];
|
|
81
|
+
}
|
|
82
|
+
reverseAdj[edge.target].push(source);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Direct dependents: projects whose edges point straight at the target.
|
|
87
|
+
// Deduplicate (multiple edges between same pair are possible) and sort.
|
|
88
|
+
const directSet = new Set(reverseAdj[projectName] ?? []);
|
|
89
|
+
const direct = [...directSet].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
90
|
+
|
|
91
|
+
// BFS through reverse edges to find transitive dependents.
|
|
92
|
+
const visited = new Set(directSet);
|
|
93
|
+
const queue = [...directSet];
|
|
94
|
+
while (queue.length > 0) {
|
|
95
|
+
const current = queue.shift();
|
|
96
|
+
const parents = reverseAdj[current];
|
|
97
|
+
if (parents === undefined) continue;
|
|
98
|
+
for (const parent of parents) {
|
|
99
|
+
if (!visited.has(parent)) {
|
|
100
|
+
visited.add(parent);
|
|
101
|
+
queue.push(parent);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Transitive dependents: reachable through another project, but not direct.
|
|
107
|
+
const transitive = [...visited]
|
|
108
|
+
.filter((name) => !directSet.has(name))
|
|
109
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
110
|
+
|
|
111
|
+
// All dependents: direct + transitive, sorted.
|
|
112
|
+
const dependents = [...visited].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
113
|
+
|
|
114
|
+
return { project: projectName, direct, transitive, dependents };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Runs the `impact` command: resolves the command context, checks the
|
|
119
|
+
* unregistered-plugin condition, and computes the impact set.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} projectName The project whose dependents to list.
|
|
122
|
+
* @param {object} commandContext From `resolveCommandContext`.
|
|
123
|
+
* @param {object} [config] The loaded boundary config. When provided,
|
|
124
|
+
* constraint context and violations for each dependent edge are computed.
|
|
125
|
+
* @returns {{status: "ok"|"no-verdict", impact: object, coverage: object,
|
|
126
|
+
* report: {text: string, json: string}}}
|
|
127
|
+
* @throws {Error} when an Nx workspace has polyglot manifests but the plugin
|
|
128
|
+
* is not registered, or when the named project does not exist in the graph,
|
|
129
|
+
* or when the graph has incomplete coverage.
|
|
130
|
+
*/
|
|
131
|
+
export function impactCommand(projectName, commandContext, config = null) {
|
|
132
|
+
const { root, provider, marker, graph, pluginGap } = commandContext;
|
|
133
|
+
|
|
134
|
+
// Descriptive commands refuse when the graph is known to be incomplete.
|
|
135
|
+
if (provider === "nx" && !pluginGap.registered && pluginGap.manifests.length > 0) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`archkeep: refusing to compute impact for an Nx workspace where this plugin is ` +
|
|
138
|
+
`not registered but polyglot manifests exist under project roots ` +
|
|
139
|
+
`(${pluginGap.manifests.join(", ")}). The graph would carry no polyglot edges, ` +
|
|
140
|
+
`so the impact set would silently under-represent the real architecture. ` +
|
|
141
|
+
`Register the plugin in nx.json: ` +
|
|
142
|
+
`"plugins": [{ "plugin": "@ecoma-io/archkeep/nx" }], or remove the polyglot manifests ` +
|
|
143
|
+
`if they are not in use.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Validate the project exists before looking at coverage — a non-existent
|
|
148
|
+
// project is a caller error, not a workspace fact, and it should name the
|
|
149
|
+
// project before the run invests in anything else.
|
|
150
|
+
const impact = computeImpact(projectName, graph);
|
|
151
|
+
|
|
152
|
+
const notAnalyzed = commandContext.analysis.failures
|
|
153
|
+
.filter(isWholeFileFailure)
|
|
154
|
+
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
155
|
+
|
|
156
|
+
if (notAnalyzed.length > 0) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`archkeep: the graph has incomplete coverage — ${notAnalyzed.length} file` +
|
|
159
|
+
`${notAnalyzed.length === 1 ? "" : "s"} could not be analyzed, so the impact set may ` +
|
|
160
|
+
`under-represent the real architecture. Fix the unanalyzed files and re-run.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const complete = true;
|
|
165
|
+
const status = "ok";
|
|
166
|
+
const exitCode = 0;
|
|
167
|
+
|
|
168
|
+
const coverage = {
|
|
169
|
+
complete,
|
|
170
|
+
projects: Object.keys(graph.nodes).length,
|
|
171
|
+
analyzedFiles: commandContext.analysis.analyzed,
|
|
172
|
+
imports: commandContext.analysis.imports.length,
|
|
173
|
+
notAnalyzed: [],
|
|
174
|
+
blindSpots: commandContext.analysis.failures
|
|
175
|
+
.filter((f) => !isWholeFileFailure(f))
|
|
176
|
+
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
177
|
+
notes: [
|
|
178
|
+
"per-edge violations cover only depConstraints (3 of 15 violation types). " +
|
|
179
|
+
"A dependent with no violations here may still violate npm-ban, circular-dependency, " +
|
|
180
|
+
"lazy-load, or other rules that require import-site details. Run `check` for the " +
|
|
181
|
+
"complete verdict.",
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const context = { root, provider, marker, provenance: resolveProvenance(root) };
|
|
186
|
+
const result = {
|
|
187
|
+
project: impact.project,
|
|
188
|
+
direct: impact.direct,
|
|
189
|
+
transitive: impact.transitive,
|
|
190
|
+
dependents: impact.dependents,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Rule-impact analysis: when the boundary config is available, identify
|
|
194
|
+
// which constraint rows govern each dependent's edge to the target and
|
|
195
|
+
// whether the edge currently violates those rows. This is narrower than
|
|
196
|
+
// the full rule engine: it covers `depConstraints`, not checks that need
|
|
197
|
+
// import-site details such as npm bans or lazy loading.
|
|
198
|
+
if (config && config.depConstraints) {
|
|
199
|
+
result.constraintImpact = computeImpactConstraints(
|
|
200
|
+
projectName,
|
|
201
|
+
impact.dependents,
|
|
202
|
+
graph.nodes,
|
|
203
|
+
graph.dependencies,
|
|
204
|
+
config.depConstraints,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const envelope = jsonEnvelope({
|
|
209
|
+
command: "impact",
|
|
210
|
+
context,
|
|
211
|
+
status,
|
|
212
|
+
exitCode,
|
|
213
|
+
coverage,
|
|
214
|
+
result,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
impact: result,
|
|
220
|
+
coverage,
|
|
221
|
+
report: {
|
|
222
|
+
text: formatImpactReport({ impact: result, coverage }),
|
|
223
|
+
json: renderJson(envelope),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|