@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,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`.