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