@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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `context` command: the architecture constraints that apply to a project.
|
|
3
|
+
*
|
|
4
|
+
* Given a project name, `context` shows the project's tags, the constraint rows
|
|
5
|
+
* that match those tags, and what each constraint allows or bans — the
|
|
6
|
+
* architecture context a developer (or an AI agent) needs before editing a
|
|
7
|
+
* project. It is descriptive: it never exits 1, because a description of what
|
|
8
|
+
* the rules say is never a finding.
|
|
9
|
+
*
|
|
10
|
+
* What it needs from its caller is a project name and a `CommandContext` — the
|
|
11
|
+
* preamble every command shares (`./context.mjs`) — plus the loaded boundary
|
|
12
|
+
* config, because the constraint rows live there. What it gives back is a
|
|
13
|
+
* `status`, the context payload for both the text and the JSON renderers, and
|
|
14
|
+
* enough coverage information to build a correct envelope. It does not print,
|
|
15
|
+
* and it does not decide the process's exit code — `../../cli.mjs` owns those
|
|
16
|
+
* (`./README.md`).
|
|
17
|
+
*
|
|
18
|
+
* ## Why this command exists
|
|
19
|
+
*
|
|
20
|
+
* A developer opening a project for the first time — or an AI agent given a
|
|
21
|
+
* task that touches one — needs to know what the boundary rules allow before
|
|
22
|
+
* writing an import that violates them. Running `check` after the fact is a
|
|
23
|
+
* lint cycle; `context` is the architecture answer before the first line is
|
|
24
|
+
* written. It is the same constraint table `check` judges from, rendered as
|
|
25
|
+
* a readable summary rather than as a list of violations.
|
|
26
|
+
*
|
|
27
|
+
* ## The unregistered-plugin refusal
|
|
28
|
+
*
|
|
29
|
+
* Same as `graph`, `diff`, `impact`, and `explain`: on an Nx workspace whose
|
|
30
|
+
* `nx.json` does not register this plugin but whose tracked files include
|
|
31
|
+
* polyglot manifests under project roots, `context` refuses loudly rather
|
|
32
|
+
* than explaining constraints from a graph whose edges silently under-represent
|
|
33
|
+
* the real architecture.
|
|
34
|
+
*/
|
|
35
|
+
import { isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
36
|
+
import { UsageError } from "../errors.mjs";
|
|
37
|
+
import { judgeEdge } from "./edge-constraints.mjs";
|
|
38
|
+
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
39
|
+
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
40
|
+
import { formatContextReport } from "../report/context-text.mjs";
|
|
41
|
+
import { resolveProvenance } from "./provenance.mjs";
|
|
42
|
+
import { readAdrContext } from "./adr.mjs";
|
|
43
|
+
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Collects the architecture context for a project: its tags, which constraint
|
|
47
|
+
* rows match, what each row allows or bans, and the project's current
|
|
48
|
+
* dependencies with per-edge constraint verdicts.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} projectName The project whose context is being queried.
|
|
51
|
+
* @param {object} graph The project graph: `{nodes, dependencies}`.
|
|
52
|
+
* @param {object} config The loaded boundary config (from `loadBoundaryConfig`).
|
|
53
|
+
* @returns {{project: string, tags: string[], constraints: object[],
|
|
54
|
+
* dependencies: {target: string, type: string, violations: object[]}[]}}
|
|
55
|
+
* @throws {UsageError} when `projectName` is not in the graph.
|
|
56
|
+
*/
|
|
57
|
+
export function collectProjectContext(projectName, graph, config) {
|
|
58
|
+
const nodes = graph.nodes;
|
|
59
|
+
const dependencies = graph.dependencies;
|
|
60
|
+
|
|
61
|
+
if (!Object.hasOwn(nodes, projectName)) {
|
|
62
|
+
throw new UsageError(
|
|
63
|
+
`archkeep: no project named '${projectName}' in the graph — ` +
|
|
64
|
+
`available projects: ${Object.keys(nodes)
|
|
65
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
66
|
+
.join(", ")}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const node = nodes[projectName];
|
|
71
|
+
const tags = node.data?.tags ?? [];
|
|
72
|
+
|
|
73
|
+
const matchedConstraints = findConstraintsFor(config.depConstraints, node);
|
|
74
|
+
|
|
75
|
+
// Collect the project's outgoing edges and judge each one.
|
|
76
|
+
const outgoing = dependencies[projectName] ?? [];
|
|
77
|
+
const deps = [];
|
|
78
|
+
for (const edge of outgoing) {
|
|
79
|
+
const violations = judgeEdge(
|
|
80
|
+
{ source: projectName, target: edge.target },
|
|
81
|
+
nodes,
|
|
82
|
+
dependencies,
|
|
83
|
+
config.depConstraints,
|
|
84
|
+
);
|
|
85
|
+
deps.push({
|
|
86
|
+
target: edge.target,
|
|
87
|
+
type: edge.type,
|
|
88
|
+
violations,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { project: projectName, tags, constraints: matchedConstraints, dependencies: deps };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Runs the `context` command: resolves the command context, checks the
|
|
97
|
+
* unregistered-plugin condition, and collects the project's architecture
|
|
98
|
+
* context.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} projectName The project whose architecture context to show.
|
|
101
|
+
* @param {object} commandContext From `resolveCommandContext`.
|
|
102
|
+
* @param {object} config The loaded boundary config (from `loadBoundaryConfig`).
|
|
103
|
+
* @returns {{status: "ok"|"no-verdict", projectContext: object, coverage: object,
|
|
104
|
+
* report: {text: string, json: string}}}
|
|
105
|
+
* @throws {Error} when an Nx workspace has polyglot manifests but the plugin
|
|
106
|
+
* is not registered, or when the named project does not exist in the graph.
|
|
107
|
+
*/
|
|
108
|
+
export function contextCommand(projectName, commandContext, config) {
|
|
109
|
+
const { root, provider, marker, graph, pluginGap, tracked } = commandContext;
|
|
110
|
+
|
|
111
|
+
// Descriptive commands refuse when the graph is known to be incomplete.
|
|
112
|
+
if (provider === "nx" && !pluginGap.registered && pluginGap.manifests.length > 0) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`archkeep: refusing to show context for an Nx workspace where this plugin is ` +
|
|
115
|
+
`not registered but polyglot manifests exist under project roots ` +
|
|
116
|
+
`(${pluginGap.manifests.join(", ")}). The graph would carry no polyglot edges, ` +
|
|
117
|
+
`so the constraints shown would be against an incomplete graph. ` +
|
|
118
|
+
`Register the plugin in nx.json: ` +
|
|
119
|
+
`"plugins": [{ "plugin": "@ecoma-io/archkeep/nx" }], or remove the polyglot manifests ` +
|
|
120
|
+
`if they are not in use.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate the project exists before investing in anything else.
|
|
125
|
+
const projectContext = collectProjectContext(projectName, graph, config);
|
|
126
|
+
|
|
127
|
+
// A matched constraint row's `decisionRef` names the ADR (or rule/fitness
|
|
128
|
+
// id) that supposedly authorizes it, unverified until now — the same gap
|
|
129
|
+
// `cli.mjs`'s `check` closes for the identical `depConstraints` table,
|
|
130
|
+
// through the same `readAdrContext`/`unresolvedDecisionRefRows`
|
|
131
|
+
// (`../governance/adr-registry.mjs`). Only the rows this report actually
|
|
132
|
+
// renders are checked; a workspace that never uses `decisionRef` pays no
|
|
133
|
+
// extra read.
|
|
134
|
+
const decisionRefRows = projectContext.constraints
|
|
135
|
+
.map((row, index) => ({ kind: `constraints[${index}]`, row }))
|
|
136
|
+
.filter(({ row }) => typeof row?.decisionRef === "string" && row.decisionRef.trim() !== "");
|
|
137
|
+
let unresolvedDecisionRefs = new Set();
|
|
138
|
+
if (decisionRefRows.length > 0) {
|
|
139
|
+
const adrContext = readAdrContext(root, { tracked });
|
|
140
|
+
unresolvedDecisionRefs = new Set(
|
|
141
|
+
// F04: the fitness half resolves against the ids THIS policy declares
|
|
142
|
+
// (`declaredFitnessNames(config)`), never the ADRs' own `bindings`.
|
|
143
|
+
unresolvedDecisionRefRows(decisionRefRows, adrContext.byId, declaredFitnessNames(config)).map(
|
|
144
|
+
(row) => row.decisionRef,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const notAnalyzed = commandContext.analysis.failures
|
|
150
|
+
.filter(isWholeFileFailure)
|
|
151
|
+
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
152
|
+
|
|
153
|
+
const complete = notAnalyzed.length === 0;
|
|
154
|
+
const status = complete ? "ok" : "no-verdict";
|
|
155
|
+
const exitCode = complete ? 0 : 3;
|
|
156
|
+
|
|
157
|
+
const coverage = {
|
|
158
|
+
complete,
|
|
159
|
+
projects: Object.keys(graph.nodes).length,
|
|
160
|
+
analyzedFiles: commandContext.analysis.analyzed,
|
|
161
|
+
imports: commandContext.analysis.imports.length,
|
|
162
|
+
notAnalyzed,
|
|
163
|
+
blindSpots: commandContext.analysis.failures
|
|
164
|
+
.filter((f) => !isWholeFileFailure(f))
|
|
165
|
+
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
166
|
+
notes: [
|
|
167
|
+
"per-edge violations cover only depConstraints (3 of 15 violation types). " +
|
|
168
|
+
"A dependency with no violations here may still violate npm-ban, circular-dependency, " +
|
|
169
|
+
"lazy-load, or other rules that require import-site details. Run `check` for the " +
|
|
170
|
+
"complete verdict.",
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const context = { root, provider, marker, provenance: resolveProvenance(root) };
|
|
175
|
+
const result = {
|
|
176
|
+
project: projectContext.project,
|
|
177
|
+
tags: projectContext.tags,
|
|
178
|
+
constraints: projectContext.constraints,
|
|
179
|
+
dependencies: projectContext.dependencies,
|
|
180
|
+
// Additive and optional: absent when every matched row's decisionRef
|
|
181
|
+
// resolves (or none carries one) — a project with no governance-cited
|
|
182
|
+
// rows reads exactly as it did before this field existed. `constraints`
|
|
183
|
+
// above keeps every row's raw `decisionRef` untouched; this is the
|
|
184
|
+
// separate, resolved fact a consumer cross-checks it against.
|
|
185
|
+
...(unresolvedDecisionRefs.size > 0
|
|
186
|
+
? {
|
|
187
|
+
unresolvedDecisionRefs: [...unresolvedDecisionRefs].sort((a, b) =>
|
|
188
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
189
|
+
),
|
|
190
|
+
}
|
|
191
|
+
: {}),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const envelope = jsonEnvelope({
|
|
195
|
+
command: "context",
|
|
196
|
+
context,
|
|
197
|
+
status,
|
|
198
|
+
exitCode,
|
|
199
|
+
coverage,
|
|
200
|
+
result,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status,
|
|
205
|
+
projectContext: result,
|
|
206
|
+
coverage,
|
|
207
|
+
report: {
|
|
208
|
+
text: formatContextReport({ projectContext: result, coverage, unresolvedDecisionRefs }),
|
|
209
|
+
json: renderJson(envelope),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|