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