@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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal report for the `waivers` command.
|
|
3
|
+
*
|
|
4
|
+
* Each waiver row is its path (what it accepts), the term, and the current
|
|
5
|
+
* coverage — so a cell that reads "covers nothing" is a row whose reason has
|
|
6
|
+
* lapsed, surfaced loudly rather than tucked away. Each permanent-suppression
|
|
7
|
+
* row is the same shape minus the term, since it has none — a suppression
|
|
8
|
+
* accepts forever, so "currently hides N violations" is the only fact that
|
|
9
|
+
* can change about it. The summary states the counts so "no waivers" never
|
|
10
|
+
* collapses into "every boundary is enforced" while a permanent suppression
|
|
11
|
+
* on the table is doing exactly that, unmeasured — see this module's header.
|
|
12
|
+
*
|
|
13
|
+
* This module decides nothing. A formatter that filtered would be a rule
|
|
14
|
+
* wearing a formatter's name (`./README.md`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {{waivers: object[], covered: number, expired: number, stale: number,
|
|
19
|
+
* suppressions: object[], suppressed: number}} result
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function formatWaiversReport({
|
|
23
|
+
waivers,
|
|
24
|
+
covered,
|
|
25
|
+
expired,
|
|
26
|
+
stale,
|
|
27
|
+
suppressions,
|
|
28
|
+
suppressed,
|
|
29
|
+
}) {
|
|
30
|
+
const sections = [];
|
|
31
|
+
|
|
32
|
+
if (waivers.length === 0 && suppressions.length === 0) {
|
|
33
|
+
return `no waivers — every boundary is enforced, nothing is being accepted temporarily or permanently`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (waivers.length === 0) {
|
|
37
|
+
// The claim this branch must NOT make: "every boundary is enforced" —
|
|
38
|
+
// the permanent-suppressions section below is about to name at least one
|
|
39
|
+
// row this run measured and found accepting a violation forever.
|
|
40
|
+
sections.push(`no waivers — nothing is being accepted temporarily`);
|
|
41
|
+
} else {
|
|
42
|
+
sections.push(
|
|
43
|
+
`${waivers.length} waiver${waivers.length === 1 ? "" : "s"} on the table — ` +
|
|
44
|
+
`${covered} currently cover${covered === 1 ? "s" : ""} a violation, ` +
|
|
45
|
+
`${expired} expired, ${stale} cover${stale === 1 ? "s" : ""} nothing right now`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
for (const waiver of waivers) {
|
|
49
|
+
const messageId = waiver.messageId ? ` (${waiver.messageId})` : "";
|
|
50
|
+
const remaining =
|
|
51
|
+
waiver.status === "expired"
|
|
52
|
+
? `expired ${Math.abs(waiver.remainingMs)}ms ago`
|
|
53
|
+
: `${Math.floor(waiver.remainingMs / 86_400_000)}d ` +
|
|
54
|
+
`${Math.floor((waiver.remainingMs % 86_400_000) / 3_600_000)}h left`;
|
|
55
|
+
const coverage =
|
|
56
|
+
waiver.covered === 0
|
|
57
|
+
? "covers nothing right now — the violation it accepted may be fixed"
|
|
58
|
+
: `${waiver.covered} current violation${waiver.covered === 1 ? "" : "s"}`;
|
|
59
|
+
const lines = [
|
|
60
|
+
`- ${waiver.path}${messageId}: ${coverage}`,
|
|
61
|
+
` reason: ${waiver.reason}`,
|
|
62
|
+
` expires: ${waiver.expiresAt} (${remaining})`,
|
|
63
|
+
];
|
|
64
|
+
if (waiver.origin) lines.push(` origin: ${waiver.origin}`);
|
|
65
|
+
sections.push(lines.join("\n"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (stale > 0) {
|
|
69
|
+
sections.push(
|
|
70
|
+
`⚠ ${stale} waiver${stale === 1 ? "" : "s"} cover${stale === 1 ? "s" : ""} no violation ` +
|
|
71
|
+
`right now — consider removing it. Waivers are never silently deleted, they are ` +
|
|
72
|
+
`removed by an explicit edit; a row that covers nothing is dead weight.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (suppressions.length > 0) {
|
|
78
|
+
sections.push(
|
|
79
|
+
`${suppressions.length} permanent suppression${suppressions.length === 1 ? "" : "s"} on the ` +
|
|
80
|
+
`table — no expiry, no re-assertion — currently hiding ${suppressed} violation` +
|
|
81
|
+
`${suppressed === 1 ? "" : "s"} that would otherwise be reported`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
for (const suppression of suppressions) {
|
|
85
|
+
const messageId = suppression.messageId ? ` (${suppression.messageId})` : "";
|
|
86
|
+
const coverage =
|
|
87
|
+
suppression.covered === 0
|
|
88
|
+
? "covers nothing right now"
|
|
89
|
+
: `permanently hides ${suppression.covered} current violation${suppression.covered === 1 ? "" : "s"}`;
|
|
90
|
+
const lines = [
|
|
91
|
+
`- ${suppression.path}${messageId}: ${coverage}`,
|
|
92
|
+
` reason: ${suppression.reason}`,
|
|
93
|
+
];
|
|
94
|
+
if (suppression.origin) lines.push(` origin: ${suppression.origin}`);
|
|
95
|
+
sections.push(lines.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sections.join("\n\n");
|
|
100
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# `src/rules/` — the boundary rules
|
|
2
|
+
|
|
3
|
+
`@nx/enforce-module-boundaries` reproduced over analysis records instead of an
|
|
4
|
+
ESLint AST, so the same **15 violation types** under the same **8 options**
|
|
5
|
+
reach `.go`, `.rs` and `.py` — the languages ESLint cannot read, where a
|
|
6
|
+
`layer:`/`scope:`/`license:` tag has no mechanism behind it today. Measured, not
|
|
7
|
+
assumed: asked to lint a `.go` file, ESLint answers "File ignored because no
|
|
8
|
+
matching configuration was supplied."
|
|
9
|
+
|
|
10
|
+
**Vue is not on that list, and this file said it was.** `eslint.config.mjs`
|
|
11
|
+
supplies `vue-eslint-parser`, the boundary rule block there carries no `files`
|
|
12
|
+
filter, and both engines report the same `messageId` and message on the same
|
|
13
|
+
`.vue` violation — differing only in column, which is the difference they have
|
|
14
|
+
on every `.ts` file too. Two independent runs measured it, and
|
|
15
|
+
`banned-external-import-in-a-vue-single-file-component` pins it
|
|
16
|
+
(`../conformance/README.md`). Vue was never a blind spot.
|
|
17
|
+
|
|
18
|
+
Five of those types are decided on the raw import specifier rather than on the
|
|
19
|
+
project pair,
|
|
20
|
+
which is why the analysis record carries the specifier and its position and why
|
|
21
|
+
an Nx graph edge alone cannot serve them (`../analysis/contract.md`, "superset
|
|
22
|
+
of a graph edge"). It also carries how the specifier is SPELLED, because that
|
|
23
|
+
question has a different answer per language and this layer never learns which
|
|
24
|
+
language it is holding.
|
|
25
|
+
|
|
26
|
+
One entry point:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
evaluate(importSites, graph, config) -> Violation[]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Pure: analysis records and the loaded config, nothing else — no filesystem, no
|
|
33
|
+
git, no Nx. A rule never reads a file, never resolves a specifier, and never
|
|
34
|
+
decides which files to visit; a rule that reaches for those turns into a second,
|
|
35
|
+
weaker analyzer. That purity is what lets the CLI and the language server share
|
|
36
|
+
one verdict, and what lets all fifteen rules be driven from fixtures with no
|
|
37
|
+
workspace at all.
|
|
38
|
+
|
|
39
|
+
| module | what it owns |
|
|
40
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| `index.mjs` | the pipeline, in upstream's order; `allow`; the `Violation` record |
|
|
42
|
+
| `tags.mjs` | only / not / empty-only, and which constraints a source is held to |
|
|
43
|
+
| `topology.mjs` | circular, self-circular, apps, e2e, buildable, lazy, transitive |
|
|
44
|
+
| `specifiers.mjs` | relative-across, relative-externals, banned and nested-banned externals |
|
|
45
|
+
| `messages.mjs` | the fifteen message templates, copied verbatim from upstream |
|
|
46
|
+
| `match.mjs` | Nx's three pattern dialects (none of them minimatch), plus the brace-expansion cap in front of `path.posix.matchesGlob` |
|
|
47
|
+
| `reachability.mjs` | who reaches whom: the cycle check and the transitive tag check share it |
|
|
48
|
+
|
|
49
|
+
## Read these before changing anything here
|
|
50
|
+
|
|
51
|
+
Each is the opposite of the implementation a reader would write from the message
|
|
52
|
+
text alone, and each is marked at its call site.
|
|
53
|
+
|
|
54
|
+
1. **No matching constraint is an ERROR.** `findConstraintsFor` returning
|
|
55
|
+
nothing means the source project's tags match no row, and upstream reports
|
|
56
|
+
`projectWithoutTagsCannotHaveDependencies`. "No rule said no, so it's fine"
|
|
57
|
+
inverts it, and every mis-tagged project escapes silently.
|
|
58
|
+
2. **Several matching constraints are AND.** The result is an array and the loop
|
|
59
|
+
over it is a conjunction. A project on four tag axes is held to all four rows.
|
|
60
|
+
3. **`allow` matches the RAW SPECIFIER with Nx's own matcher**, whose fallback
|
|
61
|
+
branch is an unanchored `new RegExp(entry)`. Not the file path, not
|
|
62
|
+
minimatch. A glob library silently changes which escape hatches still work.
|
|
63
|
+
4. **The order is the semantics.** Upstream is a chain of `report(); return;`,
|
|
64
|
+
so most sites yield at most one violation and which one depends on the order.
|
|
65
|
+
Two places break that: the npm branch (transitive and banned are both
|
|
66
|
+
reported) and the nested-banned check (one report per offending package).
|
|
67
|
+
5. **`bannedExternalImports` is checked in two unrelated places** — directly,
|
|
68
|
+
for an npm target, before any tag rule runs; and nested, only under
|
|
69
|
+
`checkNestedExternalImports`. An npm target returns before the tag block, so
|
|
70
|
+
no external import can ever produce a no-constraint violation.
|
|
71
|
+
|
|
72
|
+
## Where this engine is deliberately stricter than ESLint
|
|
73
|
+
|
|
74
|
+
The dangerous failure is a false NEGATIVE — reporting clean while a violation
|
|
75
|
+
exists. That is worse than today, where ESLint is right about JS/TS and silent
|
|
76
|
+
elsewhere: silence you know about beats a green light you cannot trust. So every
|
|
77
|
+
judgement call resolves toward reporting. The ones that change a verdict:
|
|
78
|
+
|
|
79
|
+
- **Facts upstream reads off disk are optional graph fields, and absent means
|
|
80
|
+
"the exemption does not apply"** — `data.mfeRemote` (`noImportsOfApps`),
|
|
81
|
+
`data.entryPoints` (secondary entry points), `data.declaredPackages`
|
|
82
|
+
(`noTransitiveDependencies`). An adapter that has them supplies them; one that
|
|
83
|
+
does not gets the strict answer.
|
|
84
|
+
- **An external record with no external node in the graph is still checked.**
|
|
85
|
+
`src/graph/` deliberately registers no crates, PyPI distributions or Go
|
|
86
|
+
modules as external nodes, and upstream bails when it cannot find one — which
|
|
87
|
+
would leave `bannedExternalImports` unenforceable outside JavaScript. The
|
|
88
|
+
record's own answer is taken instead. Upstream's `node_modules` read is not a
|
|
89
|
+
second way in: it supplies only the `name@version` used to look a node up in a
|
|
90
|
+
map built from `externalNodes`, so an installed package with no node still
|
|
91
|
+
resolves to nothing (measured, `../conformance/README.md`). What that reach
|
|
92
|
+
amounts to in practice is narrower than the mechanism — a JS, TS or Vue
|
|
93
|
+
external record exists only for an installed package, which already has a
|
|
94
|
+
node, so the synthesis changes a verdict only for Go, Rust and Python.
|
|
95
|
+
**A specifier that is a path gets no node, synthesized or otherwise**,
|
|
96
|
+
because a package name is what the mechanism needs and a path is never one;
|
|
97
|
+
`isPathSpecifier` in `index.mjs` is
|
|
98
|
+
the same test the branch that reports `noRelativeOrAbsoluteExternals` uses, so
|
|
99
|
+
a path refused a target is a path that gets reported. "Is a path" is the
|
|
100
|
+
RECORD's answer, not this layer's — a Rust `use` path and a Python dotted
|
|
101
|
+
module are names, and a bare `.` is a path in one language and not in another.
|
|
102
|
+
- **`require()` and `require.resolve()` of a lazy-loaded library are reported**,
|
|
103
|
+
where ESLint exempts both. The analysis contract records either call as
|
|
104
|
+
`kind: "static"`, like an `import` statement, and the three cannot be told
|
|
105
|
+
apart from the record.
|
|
106
|
+
- **An `ignoredCircularDependencies` pattern this engine cannot expand exactly
|
|
107
|
+
is rejected at config load.** Nx expands them with minimatch, which this
|
|
108
|
+
project may not import, and an ignore list that expands to almost the right set
|
|
109
|
+
hides real cycles.
|
|
110
|
+
|
|
111
|
+
## What must not land here
|
|
112
|
+
|
|
113
|
+
- **A second copy of the constraint table.** The table has one home — the
|
|
114
|
+
boundary config at the consumer's workspace root — and `../config.mjs` is the
|
|
115
|
+
only thing that reads it.
|
|
116
|
+
- **A workspace's specific project names, areas, or tag values.** This tool is
|
|
117
|
+
installed into trees it has never seen, with names and a tag vocabulary it
|
|
118
|
+
cannot predict. Everything comes from the graph and the config — fixtures
|
|
119
|
+
included.
|
|
120
|
+
- **Judgment about which files to analyze.** A rule is handed records.
|
|
121
|
+
- **A `messageId` upstream does not use.** The ids are what make a differential
|
|
122
|
+
comparison against ESLint mean anything, and
|
|
123
|
+
`upstream.integration.test.mjs` fails when a copied message or option drifts.
|