@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,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
|
+
}
|