@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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exact-match selector engine for Architecture Intent.
|
|
3
|
+
*
|
|
4
|
+
* A boundary's `match[]` values select projects. This is deliberately NOT
|
|
5
|
+
* `findMatchingProjects` (`../rules/match.mjs`): that matcher is a faithful port
|
|
6
|
+
* of Nx's own, and Nx's unlabeled patterns fall back to a case-insensitive
|
|
7
|
+
* substring regular expression over project names (`applyName`, match.mjs:193).
|
|
8
|
+
* Two things make that fallback wrong here:
|
|
9
|
+
*
|
|
10
|
+
* - A boundary is a claim about which projects share an architecture role.
|
|
11
|
+
* `{ "match": ["domain"] }` quietly pulling in `platform-domain` because a
|
|
12
|
+
* substring regex matched is a boundary that means something other than what
|
|
13
|
+
* its author wrote — a silent false negative (a real violation routed
|
|
14
|
+
* through the stray member is never reported). Nx tolerates the
|
|
15
|
+
* over-approximation because its matcher already guards other text; intent
|
|
16
|
+
* IS the contract, so it must mean exactly what it says.
|
|
17
|
+
* - The fallback interpolates the selector into `new RegExp(...)`. In intent
|
|
18
|
+
* the selector is attacker-adjacent input (anything a pull request can add),
|
|
19
|
+
* and building a RegExp from it is a rejection / injection surface. Exact
|
|
20
|
+
* string comparison constructs no RegExp, so neither the over-approximation
|
|
21
|
+
* nor an injection surface exists.
|
|
22
|
+
*
|
|
23
|
+
* So intent owns this tiny grammar, matched by string equality only. Three
|
|
24
|
+
* labeled forms plus a bare name, `*` for everything, and `!` for set
|
|
25
|
+
* difference — nothing else. No glob, no `?`, no character class, no regex. A
|
|
26
|
+
* `label:` prefix that is not one of the three is a load error (a `tagz:x` typo
|
|
27
|
+
* surfaces red, never as a silent zero-match), handled by
|
|
28
|
+
* `../architecture-intent/model.mjs` calling `selectorType` — this module
|
|
29
|
+
* resolves members, it does not validate the file.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The three selector labels. `unlabeled` is a bare project name (equals `name:`). */
|
|
33
|
+
export const SELECTOR_LABELS = Object.freeze(["name", "tag", "directory"]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Split a selector into `{exclude, label, value}` — `!` prefix removed, `*`
|
|
37
|
+
* kept as `*`. The `!`-prefix is the only leading signal a selector has; it
|
|
38
|
+
* does not participate in the label or value lookup.
|
|
39
|
+
*/
|
|
40
|
+
export function splitSelector(value) {
|
|
41
|
+
const exclude = value.startsWith("!");
|
|
42
|
+
const body = exclude ? value.substring(1) : value;
|
|
43
|
+
const separator = body.indexOf(":");
|
|
44
|
+
const label = separator === -1 ? null : body.substring(0, separator);
|
|
45
|
+
const val = separator === -1 ? body : body.substring(separator + 1);
|
|
46
|
+
return { exclude, label, value: val };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether `value` is a syntactically valid selector — `!` prefix, then a known label or none. */
|
|
50
|
+
export function isValidSelector(value) {
|
|
51
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
52
|
+
const { exclude, label, value: body } = splitSelector(value);
|
|
53
|
+
if (label !== null && !SELECTOR_LABELS.includes(label)) return false;
|
|
54
|
+
if (body.length === 0) return false;
|
|
55
|
+
return exclude === true || exclude === false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The projects positively selected by one selector — a fresh array every call,
|
|
60
|
+
* so callers can union results, and a sorted array, so the first thing a caller
|
|
61
|
+
* doing set-math sees is a deterministic order. The `!` prefix is ignored here:
|
|
62
|
+
* exclusion is a LIST-level operation over a boundary's whole `match[]`
|
|
63
|
+
* (`resolveMembers` below), because `!a` means "everything already matched,
|
|
64
|
+
* minus `a`" and so has no meaning as a single selector.
|
|
65
|
+
*
|
|
66
|
+
* `nodes` is the provider-neutral project map `{name: {data: {root, tags}}}`
|
|
67
|
+
* every command receives.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} selector
|
|
70
|
+
* @param {Record<string, {data?: {root?: string, tags?: string[]}}>} nodes
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
export function selectProjects(selector, nodes) {
|
|
74
|
+
const { label, value } = splitSelector(selector);
|
|
75
|
+
|
|
76
|
+
let found;
|
|
77
|
+
// `*` is a TOKEN of this grammar, not a wildcard character in it, so only the
|
|
78
|
+
// BARE `*` means "every project" — `label === null` is what says "bare".
|
|
79
|
+
// `docs/reference/architecture-intent.md`'s selector table gives `*` its own
|
|
80
|
+
// row ("every project") separate from the three labeled rows, each of which
|
|
81
|
+
// reads as an exact-equality test: `name:<name>` is "the project with that
|
|
82
|
+
// exact name", `tag:<tag>` is "every project carrying that tag",
|
|
83
|
+
// `directory:<d>` is "every project whose root directory is exactly `<d>`".
|
|
84
|
+
// Testing `value === "*"` FIRST discarded the label, so `name:*`, `tag:*` and
|
|
85
|
+
// `directory:*` each silently returned every project in the graph — a
|
|
86
|
+
// boundary that reads as "the project literally named `*`" quietly became
|
|
87
|
+
// "all of them", and, worse, it could never reach the zero-member no-verdict
|
|
88
|
+
// in `./judge.mjs` that a selector matching nothing is supposed to raise.
|
|
89
|
+
//
|
|
90
|
+
// So a label puts `*` back in the value's own alphabet: `name:*` selects the
|
|
91
|
+
// project actually named `*` (none, in every real workspace, so the boundary
|
|
92
|
+
// is loudly unresolvable); `tag:*` selects the projects carrying a tag
|
|
93
|
+
// literally spelled `*`, NOT "any project carrying any tag"; `directory:*`
|
|
94
|
+
// the projects rooted at a directory literally named `*`. That reading is
|
|
95
|
+
// forced twice over — this module's header admits no glob, no `?`, no
|
|
96
|
+
// character class and no regex, and the doc's table defines each labeled form
|
|
97
|
+
// as an exact match — and it is also the fail-loud one: every labeled `*`
|
|
98
|
+
// that an author meant as a wildcard now resolves to nothing and surfaces as
|
|
99
|
+
// a no-verdict, where the old behaviour answered a question nobody asked with
|
|
100
|
+
// a set nobody wrote.
|
|
101
|
+
if (label === null && value === "*") {
|
|
102
|
+
found = Object.keys(nodes);
|
|
103
|
+
} else if (label === "tag") {
|
|
104
|
+
found = Object.keys(nodes).filter((name) => (nodes[name].data?.tags ?? []).includes(value));
|
|
105
|
+
} else if (label === "directory") {
|
|
106
|
+
found = Object.keys(nodes).filter((name) => nodes[name].data?.root === value);
|
|
107
|
+
} else {
|
|
108
|
+
// name: and unlabeled both mean an exact project name. Exact equality —
|
|
109
|
+
// no substring, no case folding — so `domain` selects nothing when only
|
|
110
|
+
// `platform-domain` exists, and the boundary is accurately empty.
|
|
111
|
+
//
|
|
112
|
+
// `Object.hasOwn`, never `nodes[value]`: `nodes` is a caller-supplied map
|
|
113
|
+
// whose keys are project NAMES, and most providers hand over a plain object
|
|
114
|
+
// (`JSON.parse` of `nx graph --file=`, `Object.fromEntries` in a test) that
|
|
115
|
+
// still inherits from `Object.prototype`. A truthiness test on it answered
|
|
116
|
+
// `constructor`, `toString`, `valueOf`, `hasOwnProperty` and `__proto__`
|
|
117
|
+
// with an inherited member and reported the selector as matching a project
|
|
118
|
+
// of that name — measured: `selectProjects("constructor", nodes)` returned
|
|
119
|
+
// `["constructor"]` on a graph with no such project, while `"nosuch"`
|
|
120
|
+
// correctly returned `[]`. That is the silent direction twice: the phantom
|
|
121
|
+
// member kept the boundary's zero-member check in `./judge.mjs` from ever
|
|
122
|
+
// firing, so `intentUnresolved` stayed 0 and `check` exited 0 where its
|
|
123
|
+
// contract owes 3, and every row anchored on that boundary was judged
|
|
124
|
+
// against a project the workspace does not contain. The same names reach
|
|
125
|
+
// `dependencies.forbidden` through a `Map` and are loud there; this branch
|
|
126
|
+
// was the one that answered from the prototype.
|
|
127
|
+
found = Object.hasOwn(nodes, value) ? [value] : [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return found.sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The members of a boundary: the union of its positive selectors, minus
|
|
135
|
+
* whatever its `!` selectors carve out — order-independent, because
|
|
136
|
+
* `docs/reference/architecture-intent.md` documents `match[]` as a set
|
|
137
|
+
* expression ("the union of its positive selectors minus its `!` selectors"),
|
|
138
|
+
* not a sequence of edits applied left to right. A list with NO positive
|
|
139
|
+
* selector at all, like `["!tag:type-package"]`, means "everything except…",
|
|
140
|
+
* so an implicit `*` seeds the union in that case only — decided by whether
|
|
141
|
+
* any entry lacks `!`, never by which entry the list happens to start with.
|
|
142
|
+
*
|
|
143
|
+
* This deliberately does NOT reuse `findMatchingProjects`'s
|
|
144
|
+
* (`../rules/match.mjs`) upstream-Nx rule of checking only `patterns[0]`:
|
|
145
|
+
* that check is a faithful port of Nx's own `find-matching-projects.js`
|
|
146
|
+
* (`isExcludePattern(patterns[0])`), and it makes the result depend on
|
|
147
|
+
* selector ORDER — `["!tag:legacy", "tag:layer:app"]` seeds the wildcard
|
|
148
|
+
* (exclusion first) and then the later positive selector re-adds a legacy
|
|
149
|
+
* project the exclusion just removed, while the reverse order never seeds the
|
|
150
|
+
* wildcard and correctly excludes it. That is upstream Nx's documented
|
|
151
|
+
* behaviour and stays correct in `../rules/match.mjs`, which exists to match
|
|
152
|
+
* Nx byte-for-byte; it would be wrong here, where the same list must resolve
|
|
153
|
+
* the same way regardless of how the author happened to order it.
|
|
154
|
+
*
|
|
155
|
+
* @param {string[]} patterns A boundary's `match[]`, already validated.
|
|
156
|
+
* @param {Record<string, {data?: {root?: string, tags?: string[]}}>} nodes
|
|
157
|
+
* @returns {string[]} Sorted member names.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveMembers(patterns, nodes) {
|
|
160
|
+
const positives = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
161
|
+
const negatives = patterns.filter((pattern) => pattern.startsWith("!"));
|
|
162
|
+
const members = new Set();
|
|
163
|
+
for (const pattern of positives.length > 0 ? positives : ["*"]) {
|
|
164
|
+
for (const name of selectProjects(pattern, nodes)) members.add(name);
|
|
165
|
+
}
|
|
166
|
+
for (const pattern of negatives) {
|
|
167
|
+
for (const name of selectProjects(pattern, nodes)) members.delete(name);
|
|
168
|
+
}
|
|
169
|
+
return Array.from(members).sort();
|
|
170
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON — the deterministic serialization Contract K's fingerprints
|
|
3
|
+
* are built on: plain-object keys sorted at every depth so two objects meaning
|
|
4
|
+
* the same thing serialize identically.
|
|
5
|
+
*
|
|
6
|
+
* The one rule that matters: **only plain-object keys are sorted.** Array
|
|
7
|
+
* element order is never re-ordered, on purpose — for a boundary policy's
|
|
8
|
+
* `depConstraints` and an intent's `dependencies.forbidden` rows, order is
|
|
9
|
+
* semantic, and a fingerprint that ignored it could not tell two arrangements
|
|
10
|
+
* apart. A fingerprint therefore *changes* when rows are re-ordered, which is
|
|
11
|
+
* correct.
|
|
12
|
+
*
|
|
13
|
+
* Used by `computePolicyFingerprint` (`../commands/graph.mjs`), the intent
|
|
14
|
+
* fingerprint (`./intent-fingerprint.mjs`), and anything else a fingerprint
|
|
15
|
+
* is computed over — one canonicalizer, in one place, so two serializations
|
|
16
|
+
* cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Serialize `value` with keys sorted at every object level.
|
|
21
|
+
*
|
|
22
|
+
* @param {*} value Any JSON-serializable value.
|
|
23
|
+
* @returns {string} The canonical serialization.
|
|
24
|
+
*/
|
|
25
|
+
export function canonicalizeJson(value) {
|
|
26
|
+
return JSON.stringify(value, (key, current) => {
|
|
27
|
+
if (current !== null && typeof current === "object" && !Array.isArray(current)) {
|
|
28
|
+
// A null-prototype accumulator, not `{}`: `JSON.parse('{"__proto__":…}')`
|
|
29
|
+
// produces an OWN key literally named "__proto__" (JSON has no notion of
|
|
30
|
+
// prototypes), and `sorted[keyName] = …` on an ordinary object treats
|
|
31
|
+
// that one key specially — it sets the object's prototype instead of
|
|
32
|
+
// creating an own property, so the key silently vanishes from
|
|
33
|
+
// `JSON.stringify`'s output. Two documents that disagree only in a
|
|
34
|
+
// `__proto__` field would then canonicalize identically, a silent
|
|
35
|
+
// fingerprint collision (`../../../AGENTS.md`, "An empty result is a claim,
|
|
36
|
+
// not a shrug" — this is the same failure shape: two different inputs
|
|
37
|
+
// must never produce one indistinguishable output). `Object.create(null)`
|
|
38
|
+
// has no `__proto__` accessor to intercept the assignment, so every key
|
|
39
|
+
// — "__proto__" included — always becomes a real own property.
|
|
40
|
+
const sorted = Object.create(null);
|
|
41
|
+
for (const keyName of Object.keys(current).sort()) {
|
|
42
|
+
sorted[keyName] = current[keyName];
|
|
43
|
+
}
|
|
44
|
+
return sorted;
|
|
45
|
+
}
|
|
46
|
+
return current;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# `src/commands/` — one module per CLI command
|
|
2
|
+
|
|
3
|
+
One module per CLI command, holding the computation and nothing about argv,
|
|
4
|
+
exit codes or where output goes. `../../cli.mjs` owns those three. A module
|
|
5
|
+
here may read the graph, the workspace and the policy; it may not print, and
|
|
6
|
+
it may not decide the process's exit code — it returns a status and `cli.mjs`
|
|
7
|
+
maps it.
|
|
8
|
+
|
|
9
|
+
`context.mjs` and `policy.mjs` are the exceptions in kind, not in rule: each is
|
|
10
|
+
a preamble the commands share rather than a command. `context.mjs` composes
|
|
11
|
+
`../workspace.mjs`, `../providers/` and `../options.mjs`; it does not
|
|
12
|
+
reimplement any of them. `policy.mjs` holds the one boundary-policy ladder every
|
|
13
|
+
command that reads a law resolves through, so no command grows a second copy of
|
|
14
|
+
the resolution order.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
- **`check`** (`./check.mjs`'s `check`, driven by `../../cli.mjs`'s `runCheck`) —
|
|
19
|
+
judges every import site against the boundary rules and folds in every other
|
|
20
|
+
finding class a verdict counts:
|
|
21
|
+
declared-edge violations, go.work drift, dead tsconfig path aliases, intent
|
|
22
|
+
drift, a failing fitness gate, and a failing custom rule
|
|
23
|
+
(`./custom-rules.mjs`). Exits 1 on any of them, and it is the only
|
|
24
|
+
command holding all four exit codes
|
|
25
|
+
([which verbs carry exit 1 is settled in `docs/concepts/architecture.md`](../../../../docs/concepts/architecture.md)
|
|
26
|
+
— `fitness` is the other one).
|
|
27
|
+
|
|
28
|
+
- **`graph`** (`./graph.mjs`'s `graphCommand`) — the project graph as a
|
|
29
|
+
deterministic, serialisable snapshot: projects (with `targets` and `tags`) and
|
|
30
|
+
dependencies, each as a flat sorted array. Strips internal fields
|
|
31
|
+
(`mfeRemote`, `entryPoints`, `declaredPackages`). Includes
|
|
32
|
+
`workspaceLayout`/`workspaceLayoutSource`. Refuses an Nx workspace with
|
|
33
|
+
polyglot manifests but no plugin registration. Descriptive: never exits 1.
|
|
34
|
+
|
|
35
|
+
- **`diff`** (`./diff.mjs`'s `diffCommand`) — two graph snapshots compared edge
|
|
36
|
+
by edge. Takes a baseline file (not a git ref). When a boundary config is
|
|
37
|
+
available (via `--config` or the workspace's declared config), also reports
|
|
38
|
+
which boundary violations the diff introduces and which it resolves — this
|
|
39
|
+
is narrower than `check`: it checks only `depConstraints` (tag-based), not
|
|
40
|
+
npm/circular/lazy-load rules that need import-site details.
|
|
41
|
+
Refuses an incomplete baseline or head.
|
|
42
|
+
Refuses an Nx workspace with polyglot manifests but no plugin registration.
|
|
43
|
+
Descriptive: never exits 1.
|
|
44
|
+
|
|
45
|
+
- **`impact`** (`./impact.mjs`'s `impactCommand`) — reverse reachability from
|
|
46
|
+
the project graph: given a project name, lists every project that transitively
|
|
47
|
+
depends on it. Separates direct from transitive dependents. When a boundary
|
|
48
|
+
config is available, also shows the constraint context for each dependent:
|
|
49
|
+
which constraint rows govern its edge and whether that edge violates them.
|
|
50
|
+
Refuses incomplete coverage (whole-file analysis failures).
|
|
51
|
+
Refuses an Nx workspace with polyglot manifests but no plugin registration.
|
|
52
|
+
Descriptive: never exits 1.
|
|
53
|
+
|
|
54
|
+
- **`explain`** (`./explain.mjs`'s `explainCommand`) — the judgment for one import
|
|
55
|
+
site, explained. Takes a `file:line:column` site, finds the matching import
|
|
56
|
+
record, and explains: which constraint row matched, which tags applied,
|
|
57
|
+
whether it is a violation and why. Reports an `UNRESOLVABLE` verdict for a
|
|
58
|
+
site-level failure (dynamic import with non-literal argument). Refuses an Nx
|
|
59
|
+
workspace with polyglot manifests but no plugin registration. Descriptive:
|
|
60
|
+
never exits 1.
|
|
61
|
+
|
|
62
|
+
- **`context`** (`./context-command.mjs`'s `contextCommand`) — the architecture
|
|
63
|
+
constraints that apply to one project. Takes a project name and returns the
|
|
64
|
+
project's tags plus every matching `depConstraints` row, including optional
|
|
65
|
+
descriptions and remediation guidance. The filename deliberately avoids
|
|
66
|
+
colliding with `./context.mjs`, which is the shared command preamble. Refuses
|
|
67
|
+
an Nx workspace with polyglot manifests but no plugin registration.
|
|
68
|
+
Descriptive: never exits 1.
|
|
69
|
+
|
|
70
|
+
- **`context --plan`** (`./plan-context-command.mjs`'s `planContextCommand`) —
|
|
71
|
+
the `--plan` face of the command above: the deterministic facts an agent needs
|
|
72
|
+
before it reasons about, plans and executes a change, scoped to the target
|
|
73
|
+
project plus optional paths. Current architecture, the applicable policy rows
|
|
74
|
+
with their authored description and remediation plus the policy fingerprint,
|
|
75
|
+
impact (dependents capped, with an explicit overflow note), current
|
|
76
|
+
violations, go.work and tsconfig-path drift, the canonical architecture-intent
|
|
77
|
+
verdict, coverage, and the commands that verify the change afterwards. It
|
|
78
|
+
never generates a plan, decides an implementation strategy, modifies source
|
|
79
|
+
code, or weakens policy — every field is a fact the tree or the boundary law
|
|
80
|
+
states, and a section that cannot state one says so (`null`, `[]`, or an
|
|
81
|
+
explicit `no-verdict`). The rule verdict is computed over the WHOLE
|
|
82
|
+
analyzeable tree and then filtered to the scoped reporting set, because the
|
|
83
|
+
circular-dependency and lazy-load rules need the whole file index; that is
|
|
84
|
+
what makes the verdict correct on every provider. Descriptive: never exits 1.
|
|
85
|
+
|
|
86
|
+
- **`history`** (`./history.mjs`'s `historyCommand`) — the architecture's
|
|
87
|
+
evolution across a consumer-managed directory of `graph --format json`
|
|
88
|
+
snapshots. Reads every snapshot (the directory is the sole source of truth —
|
|
89
|
+
no index, no database), in filename byte-sort (history) order, and classifies
|
|
90
|
+
each transition by what the snapshots carry: graph diff (architecture),
|
|
91
|
+
`policy.fingerprint` (policy/intent), `workspace.provider` (provider), and
|
|
92
|
+
provenance advance with neither changed (code drift). One-sided or cross-repo
|
|
93
|
+
signals are disclosed as incomparable rather than read as unchanged.
|
|
94
|
+
`--capture` writes `<sequence>-<sha8>.json` (deduplicating when the
|
|
95
|
+
architecture identity already is the last snapshot) and refuses incomplete
|
|
96
|
+
head coverage. Refuses an empty or unreadable directory, and a snapshot that
|
|
97
|
+
parses as an incomplete envelope. Descriptive: never exits 1.
|
|
98
|
+
|
|
99
|
+
- **`provenance`** (`./provenance-command.mjs`'s `provenanceCommand`) — where
|
|
100
|
+
this run's facts came from and which governance rows carry an origin.
|
|
101
|
+
Two surfaces: repository provenance (the git commit, remote and dirty state
|
|
102
|
+
`./provenance.mjs` exposes to every envelope) and decision provenance (each
|
|
103
|
+
`architecture-intent.json` row and each boundary-config `depConstraints` row,
|
|
104
|
+
and whether it carries an `origin` record — a row without one is flagged
|
|
105
|
+
`no origin recorded — cannot attest`). Reads the workspace's OWN declared
|
|
106
|
+
intent and config, and its own provider via `resolveCommandContext`; never
|
|
107
|
+
changes a verdict and never exits 1. Refuses out of a malformed intent or
|
|
108
|
+
boundary config the way `drift` does — a row list built from a file it could
|
|
109
|
+
not read is a claim about rows that do not exist. Descriptive: never exits 1.
|
|
110
|
+
|
|
111
|
+
- **`report`** (`./report.mjs`'s `reportCommand`) — one architecture governance
|
|
112
|
+
document: how healthy the architecture is, and why. Composes `healthCommand`,
|
|
113
|
+
`waiversCommand`, `fitnessCommand`, the ADR registry (`readAdrContext` plus
|
|
114
|
+
`resolveDecisionRef`) and `resolveProvenance` — every number through the
|
|
115
|
+
function that owns it, so no section can disagree with the command it came
|
|
116
|
+
from — over ONE boundary law `cli.mjs` resolved for the whole page. Links each
|
|
117
|
+
governed row carrying a `decisionRef` to the record it cites, and each declared
|
|
118
|
+
fitness gate to the ADRs binding it; a citation that resolves to nothing reads
|
|
119
|
+
`unknown`, never a pass. `result.uninspectable` names every surface the run
|
|
120
|
+
could not establish and is what makes the status `no-verdict` (exit 3).
|
|
121
|
+
Descriptive: never exits 1 — a live violation or a failing gate is reported
|
|
122
|
+
over exit 0.
|
|
123
|
+
|
|
124
|
+
- **`discover`** (`./discover.mjs`'s `discoverCommand`) — the observed
|
|
125
|
+
architecture, and under `--propose` the candidate architecture those
|
|
126
|
+
observations imply. Builds the observed `{projects, edges}` from the same
|
|
127
|
+
model `graph`/`drift` read, reports projects/edges/tags plus the coverage a
|
|
128
|
+
verdict could trust, and optionally drives
|
|
129
|
+
`../../src/governance/discovery-proposal.mjs`'s pure evaluator over it.
|
|
130
|
+
Proposal-only: every candidate carries `proposed: true` and
|
|
131
|
+
`notAuthoritative: true`, and the command never writes
|
|
132
|
+
`architecture-intent.json`. Returns `status: "no-verdict"` (exit 3) over
|
|
133
|
+
incomplete coverage and refuses `--propose` over it; refuses an Nx workspace
|
|
134
|
+
with polyglot manifests but no plugin registration; a zero-project workspace
|
|
135
|
+
is the empty `unknown` proposal, not a refusal. Descriptive: never exits 1.
|
|
136
|
+
|
|
137
|
+
- **`drift`** (`./drift.mjs`'s `driftCommand`) — the observed architecture
|
|
138
|
+
compared against the declared intended one. The intended side is the one
|
|
139
|
+
canonical contract the workspace declares — `architecture-intent.json` at its
|
|
140
|
+
root, loaded and judged by the same `../architecture-intent/model.mjs` and
|
|
141
|
+
`../architecture-intent/judge.mjs` the `check` command uses, so there is no
|
|
142
|
+
parallel intent grammar and no `intentConfig` option. Reads only the resolved
|
|
143
|
+
`CommandContext` and never a provider, so the same intent produces the same
|
|
144
|
+
verdict under Nx, Moon or native. Refuses an intent that cannot be read or
|
|
145
|
+
parsed, incomplete observed coverage, an Nx workspace with polyglot manifests
|
|
146
|
+
but no plugin registration, and a boundary or row side that matched no
|
|
147
|
+
observed project. Resolving each intent row's `decisionRef` against the ADR
|
|
148
|
+
registry is a separate, NON-VERDICT axis — it never becomes a finding and
|
|
149
|
+
never changes the exit code — and it is the only thing here that reads the
|
|
150
|
+
boundary law, so `cli.mjs` hands that load's failure over as `io.configError`
|
|
151
|
+
rather than throwing it where it happens: rethrown unchanged at the one site
|
|
152
|
+
that reads the policy when a row does carry a citation, and otherwise stated
|
|
153
|
+
as a coverage note rather than dropped. Descriptive: never exits 1.
|
|
154
|
+
|
|
155
|
+
- **`reconcile`** (`./reconcile.mjs`'s `reconcileCommand`) — the two-sided
|
|
156
|
+
mirror of `drift`: where drift asks which intended rows reality violates,
|
|
157
|
+
reconcile asks, element by element, what the model says about reality and what
|
|
158
|
+
it would take to make the two agree. Read-only by design; `--propose` emits a
|
|
159
|
+
ranked candidate list of model edits — add-only, removal, tag-change,
|
|
160
|
+
boundary-change — each carrying the evidence that supports it and an explicit
|
|
161
|
+
`proposed: true` / `notAuthoritative` marker, and the command never writes back
|
|
162
|
+
into `architecture-intent.json`. Makes the same four refusals `drift` makes,
|
|
163
|
+
and reads no boundary law, so it makes only those four. Those refusals mean an
|
|
164
|
+
`unknown` score can only ever come from a whole-file failure the command
|
|
165
|
+
already refused on — and `../governance/reconcile-score.mjs` marks it anyway,
|
|
166
|
+
so the scoring module can never render a partial read as a claim on its own.
|
|
167
|
+
Descriptive: never exits 1 — divergence is described, never gated.
|
|
168
|
+
|
|
169
|
+
- **`waivers`** (`./waivers.mjs`'s `waiversCommand`) — the whole
|
|
170
|
+
`boundarySuppressions` surface, read-only: the temporary rows carrying an
|
|
171
|
+
`expiresAt`, with their remaining term, and the permanent ones carrying none,
|
|
172
|
+
with what each currently covers. Evaluates the tree with the suppression table
|
|
173
|
+
REMOVED, so every row's coverage is judged against the full finding set rather
|
|
174
|
+
than against the run the table already cleaned — a row that covers nothing is
|
|
175
|
+
named as stale instead of silently doing nothing. A permanent suppression
|
|
176
|
+
never appears in `check`'s findings at all, which makes this the only surface
|
|
177
|
+
that names one. Refuses whole-file analysis failures, and refuses an Nx
|
|
178
|
+
workspace with polyglot manifests but no plugin registration through
|
|
179
|
+
`./drift.mjs`'s `refuseIncompleteGraph` — the one shared guard for that state —
|
|
180
|
+
because a row measured against a graph that never drew the workspace's
|
|
181
|
+
polyglot edges reads as covering nothing it was never shown. The
|
|
182
|
+
remaining-time column is computed against the injected clock
|
|
183
|
+
(`../governance/clock.mjs`), so a fixed `now` reproduces the report byte for
|
|
184
|
+
byte. Descriptive: never exits 1, and it never modifies the table.
|
|
185
|
+
|
|
186
|
+
- **`fitness`** (`./fitness.mjs`'s `fitnessCommand`) — every declared fitness
|
|
187
|
+
function judged against the observed workspace, as a verdict table. The
|
|
188
|
+
functions are the policy's own `fitness` export, validated by `../config.mjs`
|
|
189
|
+
and judged through `../governance/fitness-registry.mjs`, which reuses the
|
|
190
|
+
shared member resolution and verdict envelope rather than duplicating a judge,
|
|
191
|
+
against the same observed facts `check` reads. A verdict rather than a
|
|
192
|
+
description: a failing function is a finding (exit 1) and an undetermined one
|
|
193
|
+
is a could-not-determine (exit 3). A function that cannot be determined is
|
|
194
|
+
`unknown`, never `pass`; one whose `match` selects no project is `skipped` —
|
|
195
|
+
loud ("declared but matches nothing"), never folded into `pass` — and a
|
|
196
|
+
`coverage-minimum` row judged from a path-scoped run joins it there, because a
|
|
197
|
+
scoped run structurally cannot answer a whole-tree coverage question.
|
|
198
|
+
|
|
199
|
+
- **`health`** (`./health.mjs`'s `healthCommand`) — deterministic
|
|
200
|
+
architecture-health metrics and trends for the current workspace: violation
|
|
201
|
+
count, waiver surface, debt rows, coverage ratio, project and edge counts,
|
|
202
|
+
cycle count, edge density and the intent verdict. Every number is re-derived
|
|
203
|
+
from records the run already holds, through the same functions
|
|
204
|
+
`check`/`graph`/`drift` use, so it performs no new scans and cannot disagree
|
|
205
|
+
with those commands about the same tree. Each metric is decided over complete
|
|
206
|
+
evidence or says it could not look — no evidence reads `not_applicable`,
|
|
207
|
+
partial evidence reads `unknown`, and a metric is never reported as a bare
|
|
208
|
+
zero over evidence the run could not inspect. Trends come from the same
|
|
209
|
+
snapshot directory `history` reads, limited to what a `graph` snapshot
|
|
210
|
+
carries. `status` is `"ok"` only when every metric reached a verdict and
|
|
211
|
+
`"no-verdict"` (exit 3) when any is `unknown`. Descriptive: never exits 1, and
|
|
212
|
+
purely additive — it changes no other command's verdict or exit code.
|
|
213
|
+
|
|
214
|
+
- **`debt`** (`./debt.mjs`'s `debtCommand`) — the architecture-debt ledger: the
|
|
215
|
+
exemptions, gaps and violations a workspace is carrying, each aged across the
|
|
216
|
+
history directory `history` reads. Computes today's facts the same way
|
|
217
|
+
`check`/`drift` do — the boundary config's `boundarySuppressions`, and
|
|
218
|
+
`judgeIntent`'s findings, notes and unresolved — over a config the caller
|
|
219
|
+
resolved exactly as `diff`'s is, so a debt run and a `check` run never
|
|
220
|
+
disagree about the current boundary law. Fewer than two snapshots sets
|
|
221
|
+
`agings: false`, stating that every entry is really age-0 because the record
|
|
222
|
+
cannot establish age, rather than guessing one. Refuses incomplete graph
|
|
223
|
+
coverage, an intent that cannot be verified, and a directory that cannot be
|
|
224
|
+
read or parsed — a missing directory is a no-verdict, never an empty ledger.
|
|
225
|
+
Descriptive: never exits 1 — a report, not a gate.
|
|
226
|
+
|
|
227
|
+
- **`adr`** (`./adr.mjs`'s `adrCommand`) — the workspace's recorded architecture
|
|
228
|
+
decisions and the rule/fitness ids each makes enforceable, read from
|
|
229
|
+
`docs/adr/NNN-slug.md` at the workspace root
|
|
230
|
+
(`../governance/adr-registry.mjs` owns the format and the index). With no
|
|
231
|
+
argument it dumps the whole registry: every record, its status, its
|
|
232
|
+
supersession chain, and what it binds. Given an id it shows that record, and
|
|
233
|
+
for a `rule:`/`fitness:`-prefixed ref the reverse lookup naming which ADRs
|
|
234
|
+
bind it. Needs no project graph, no Nx and no boundary config — the registry
|
|
235
|
+
is self-contained in the tree, so `resolveCommandContext`'s heavy preamble is
|
|
236
|
+
skipped and `adr` runs on a tree with no Nx at all. Refuses an unreadable
|
|
237
|
+
registry and an unresolvable reference; a binding to a fitness id no
|
|
238
|
+
declaration carries is listed `unknown` — named, never hidden. Descriptive:
|
|
239
|
+
never exits 1.
|
|
240
|
+
|
|
241
|
+
## Shared modules
|
|
242
|
+
|
|
243
|
+
- **`snapshot-meta.mjs`** — `compareSnapshotMetadata`, shared by `diff` and
|
|
244
|
+
`history`: the provider, provenance (with cross-repo and one-sided
|
|
245
|
+
detection) and policy-fingerprint comparison between a baseline and a head.
|
|
246
|
+
|
|
247
|
+
- **`custom-rules.mjs`** — the custom-rules fold `check` runs by presence: each
|
|
248
|
+
row the policy's `customRules` list declares, loaded from its artifact's bytes
|
|
249
|
+
and judged over the evidence the run already computed. It owns the file
|
|
250
|
+
reading the host deliberately does not (`../custom-rules/host.mjs` takes
|
|
251
|
+
bytes, never a path) and the routing of the host's two failure classes: a
|
|
252
|
+
LOAD failure throws, so the run refuses the way it refuses a malformed
|
|
253
|
+
boundary config, and an EVALUATE failure becomes that rule's `unknown`
|
|
254
|
+
verdict with the host's own reason. A path-scoped run answers
|
|
255
|
+
`not_applicable` for every declared rule before reading anything, the posture
|
|
256
|
+
`coverage-minimum` takes for the same reason. Returns the verdict records
|
|
257
|
+
`check` folds into the fitness exit lanes plus the finding catalogue SARIF's
|
|
258
|
+
descriptors are built from; it prints nothing and decides no exit code.
|
|
259
|
+
|
|
260
|
+
- **`edge-constraints.mjs`** — edge-constraint analysis shared by `diff` and
|
|
261
|
+
`impact`. Judges a single graph edge against the `depConstraints` table,
|
|
262
|
+
producing violations with their constraint rows. Checks only tag-based rules
|
|
263
|
+
(`onlyDependOnLibsWithTags`, `notDependOnLibsWithTags`,
|
|
264
|
+
`projectWithoutTagsCannotHaveDependencies`); npm/circular/lazy-load rules
|
|
265
|
+
need import-site details that graph edges do not carry. A consumer who needs
|
|
266
|
+
the complete verdict should run `check`.
|