@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,240 @@
1
+ /**
2
+ * The `waivers` command: the whole `boundarySuppressions` surface, read-only —
3
+ * both the TEMPORARY rows (carrying `expiresAt`) and the PERMANENT ones (no
4
+ * `expiresAt`) — with each row's remaining time (waivers only) and the
5
+ * current violations it covers.
6
+ *
7
+ * ## What this command is for
8
+ *
9
+ * Waivers are the half-life half of `check`: `check` splits waived violations
10
+ * out of the findings list into an "accepted violations" section, and this
11
+ * command names every waiver still on the table and its term. A permanent
12
+ * suppression never appears in `check`'s findings at all — removing the
13
+ * violation outright is the mechanism working as designed
14
+ * (`../governance/waiver.mjs`) — which makes this command the ONLY surface
15
+ * that names one. Dropping that half used to mean a tree could carry a
16
+ * `boundarySuppressions` row with `path: "**"`, silencing every violation in
17
+ * it, and this command would still answer "no waivers — every boundary is
18
+ * enforced": a positive claim about a proposition it never measured, the
19
+ * exact silent direction `../../../../AGENTS.md`'s invariant forbids (an
20
+ * empty result is a claim, not a shrug). It is the surface a developer reads
21
+ * when deciding whether a row — waiver or suppression — is stale (covers
22
+ * nothing right now, or lapsed long ago) and worth removing — the same
23
+ * "recorded, never silently deleted" rule the waiver feature exists to hold,
24
+ * extended to the permanent half it always needed to cover.
25
+ *
26
+ * ## Read-only, like every command that is not `check`
27
+ *
28
+ * This is a descriptive command — it exits 0 when it completes, never 1 — and
29
+ * it never modifies the table. It states what the workspace DECLARES and what
30
+ * that declaration currently covers, so a reader can judge whether a row has
31
+ * earned its keep or is dead weight.
32
+ *
33
+ * ## Determinism
34
+ *
35
+ * The remaining-time column is computed against the injected clock
36
+ * (`../governance/clock.mjs`), so a test drives the same command with a fixed
37
+ * `now` and the output is reproducible byte-for-byte. Defaults to the wall
38
+ * clock, the same injection `evaluate` uses for waiver expiry.
39
+ */
40
+ import { isWholeFileFailure } from "../analysis/source-util.mjs";
41
+ import { suppressionCovers } from "../config.mjs";
42
+ import { referenceTime } from "../governance/clock.mjs";
43
+ import { isWaiver, remainingMs, waiverStatus } from "../governance/waiver.mjs";
44
+ import { jsonEnvelope, renderJson } from "../report/json.mjs";
45
+ import { formatWaiversReport } from "../report/waivers-text.mjs";
46
+ import { refuseIncompleteGraph } from "./drift.mjs";
47
+ import { resolveProvenance } from "./provenance.mjs";
48
+ import { evaluateRun } from "../rules/index.mjs";
49
+
50
+ /**
51
+ * The waivers verdict for a run: every waiver with its term and what it
52
+ * currently covers, plus every PERMANENT suppression (a `boundarySuppressions`
53
+ * row with no `expiresAt`) and what it is currently hiding. Pure per-row,
54
+ * given the raw violations.
55
+ *
56
+ * `rawViolations` is the run's raw superset — every candidate violation up to
57
+ * each site's surviving group, measured WITH the table in force, so a row
58
+ * whose hit sits behind another row's removal still counts as alive. Measuring
59
+ * against the first-candidate set instead (the pre-#216 baseline) read a row
60
+ * covering only a LATER check as stale while it was doing real work one
61
+ * suppression down the chain. `raw − evaluated = what the table hides` is the
62
+ * arithmetic this surface reports per row.
63
+ * A waiver that covers nothing is a stale row, named as such: waivers are
64
+ * recorded and never silently deleted, so the command surfaces a row whose
65
+ * reason has lapsed rather than hiding it. A permanent suppression that
66
+ * currently covers nothing is surfaced the same way — dead weight is dead
67
+ * weight whether or not it has a term.
68
+ *
69
+ * @param {object[]} suppressions The validated `boundarySuppressions` table —
70
+ * waivers and permanent suppressions together, undivided.
71
+ * @param {object[]} rawViolations Every violation the engine found, unfiltered.
72
+ * @param {string} now Reference instant (ISO-8601).
73
+ * @returns {{waivers: object[], covered: number, expired: number, stale: number,
74
+ * suppressions: object[], suppressed: number}}
75
+ */
76
+ export function computeWaivers(suppressions, rawViolations, now = referenceTime()) {
77
+ const waivers = suppressions
78
+ .filter(isWaiver)
79
+ .map((row) => ({
80
+ ...row,
81
+ status: waiverStatus(row, now),
82
+ remainingMs: remainingMs(row, now),
83
+ covered: rawViolations.filter((violation) => suppressionCovers(row, violation)).length,
84
+ }))
85
+ // Plain string comparison — never `localeCompare`, which depends on the
86
+ // locale and the Node build's ICU data and would let two machines order the
87
+ // same rows differently (the determinism rule every snapshot-state command
88
+ // shares; `graph`'s `buildProjects` documents the same refusal).
89
+ .sort(
90
+ (a, b) =>
91
+ (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) ||
92
+ (a.expiresAt < b.expiresAt ? -1 : a.expiresAt > b.expiresAt ? 1 : 0),
93
+ );
94
+
95
+ const covered = waivers.reduce((sum, w) => sum + (w.covered > 0 ? 1 : 0), 0);
96
+ const expired = waivers.filter((w) => w.status === "expired").length;
97
+ const stale = waivers.filter((w) => w.covered === 0).length;
98
+
99
+ // The other half of the table: a row with no `expiresAt` is a PERMANENT
100
+ // suppression — it never re-asserts and it never appears in `check`'s
101
+ // findings, so this command is the only surface that names it at all. It
102
+ // used to be filtered out by the `isWaiver` predicate above and reported
103
+ // nowhere, which is how a `path: "**"` row could hide every violation in a
104
+ // tree while this command still answered "no waivers — every boundary is
105
+ // enforced" (see this module's header). Same shape as a waiver — `path`,
106
+ // `reason`, `origin` when declared, and `covered` — minus the term fields a
107
+ // permanent row has none of, sorted the same deterministic way.
108
+ const permanentSuppressions = suppressions
109
+ .filter((row) => !isWaiver(row))
110
+ .map((row) => ({
111
+ ...row,
112
+ covered: rawViolations.filter((violation) => suppressionCovers(row, violation)).length,
113
+ }))
114
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
115
+
116
+ // Distinct violations hidden by AT LEAST ONE permanent suppression — a
117
+ // `Set` over the violation objects themselves (each one is a distinct
118
+ // object `evaluate()` built), not a sum of the per-row `covered` counts
119
+ // above, because two overlapping rows both matching the same violation
120
+ // would otherwise double-count it.
121
+ const suppressedViolations = new Set();
122
+ for (const row of permanentSuppressions) {
123
+ for (const violation of rawViolations) {
124
+ if (suppressionCovers(row, violation)) suppressedViolations.add(violation);
125
+ }
126
+ }
127
+
128
+ return {
129
+ waivers,
130
+ covered,
131
+ expired,
132
+ stale,
133
+ suppressions: permanentSuppressions,
134
+ suppressed: suppressedViolations.size,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Runs the `waivers` command: loads the boundary law, evaluates the tree over
140
+ * the raw candidate superset, and reports the waiver surface.
141
+ *
142
+ * @param {object} commandContext From `resolveCommandContext`.
143
+ * @param {object} boundaryConfig The run's boundary law, loaded and validated
144
+ * by the same three-way call `check` makes — `cli.mjs`'s `runWaivers`
145
+ * resolves `--config` against the working directory exactly as `runCheck`
146
+ * does, so the surface listed is the surface the law actually enforces.
147
+ * @param {{now?: string}} [io] The injected clock.
148
+ * @returns {Promise<{status: "ok", waivers: object, report: {text: string, json: string}}>}
149
+ * @throws {Error} whenever the run's law is malformed, or the tree has
150
+ * whole-file analysis failures — exit-3 class, the same posture `check` takes
151
+ * on a malformed config and `impact`/`drift` take on incomplete coverage.
152
+ */
153
+ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
154
+ const { root, provider, marker, analysis, graph } = commandContext;
155
+ const now = io.now ?? referenceTime();
156
+ const config = boundaryConfig;
157
+
158
+ // A waiver surface over a tree it could not fully read is a lottery ticket,
159
+ // not a surface: a file the analyzer never judged contributes no raw
160
+ // violation, so every waiver that names it reads as stale and the report
161
+ // says "covers nothing" about a finding the run never looked at. Refuse
162
+ // loudly on whole-file failures, the same posture `impact`, `drift`, and
163
+ // `history` take — "could not look" must never read as "looked and found
164
+ // nothing" (`./impact.mjs`'s refusal names the same silence).
165
+ const notAnalyzed = analysis.failures
166
+ .filter(isWholeFileFailure)
167
+ .map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
168
+
169
+ if (notAnalyzed.length > 0) {
170
+ throw new Error(
171
+ `archkeep: waivers has incomplete coverage — ${notAnalyzed.length} file` +
172
+ `${notAnalyzed.length === 1 ? "" : "s"} could not be analyzed, so every waiver naming one ` +
173
+ `would read as covering nothing it never saw. Fix the unanalyzed files and re-run.`,
174
+ );
175
+ }
176
+
177
+ // F07: a waiver surface measured against a graph that cannot see the
178
+ // workspace's polyglot edges is a lottery ticket too — a waiver naming a
179
+ // file the invisible graph never judged reads as "covers nothing" on a tree
180
+ // whose Go/Rust/Python edges were never drawn. `graph`/`drift`/`fitness`
181
+ // refuse this exact tree; `complete: true` may not be claimed where they
182
+ // refuse (`drift.mjs`'s `refuseIncompleteGraph` is the ONE shared guard).
183
+ refuseIncompleteGraph(commandContext, "measure waivers");
184
+
185
+ // Evaluate once, table in force: `evaluateRun`'s raw face is the candidate
186
+ // superset each row's coverage is measured against — see `computeWaivers`'s
187
+ // doc above. `now` is threaded so the expiry judgement is the same one
188
+ // `check` makes.
189
+ const rawViolations = evaluateRun(analysis.imports, graph, { ...config, now }).rawViolations;
190
+
191
+ const { waivers, covered, expired, stale, suppressions, suppressed } = computeWaivers(
192
+ config.suppressions ?? [],
193
+ rawViolations,
194
+ now,
195
+ );
196
+
197
+ const coverage = {
198
+ complete: true,
199
+ projects: Object.keys(graph.nodes).length,
200
+ analyzedFiles: analysis.analyzed,
201
+ imports: analysis.imports.length,
202
+ notAnalyzed,
203
+ blindSpots: analysis.failures
204
+ .filter((failure) => !isWholeFileFailure(failure))
205
+ .map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
206
+ // `remainingMs` reflects the wall clock at the moment of THIS run, not the
207
+ // workspace — it is expected to differ between two runs of an unchanged
208
+ // tree, by design (`../governance/clock.mjs`). Disclosed here, in-band,
209
+ // so a consumer diffing or hashing two envelopes to detect real drift
210
+ // knows to exclude it rather than read clock drift as architectural
211
+ // change; every other field is deterministic given the same law and tree.
212
+ notes: [
213
+ "remainingMs is the wall clock at the moment of this run, not a fact about the " +
214
+ "workspace — it is expected to differ between two runs of an unchanged tree and " +
215
+ "should be excluded from any diff or hash meant to detect real change. Every other " +
216
+ "field here is deterministic given the same law and the same tree.",
217
+ ],
218
+ };
219
+
220
+ const context = { root, provider, marker, provenance: resolveProvenance(root) };
221
+ const result = { waivers, covered, expired, stale, suppressions, suppressed };
222
+
223
+ const envelope = jsonEnvelope({
224
+ command: "waivers",
225
+ context,
226
+ status: "ok",
227
+ exitCode: 0,
228
+ coverage,
229
+ result,
230
+ });
231
+
232
+ return {
233
+ status: "ok",
234
+ waivers: result,
235
+ report: {
236
+ text: formatWaiversReport(result),
237
+ json: renderJson(envelope),
238
+ },
239
+ };
240
+ }