@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,248 @@
1
+ /**
2
+ * The `adr` command: the workspace's recorded architecture decisions, and the
3
+ * rules/fitnesses each makes enforceable.
4
+ *
5
+ * `adr` reads the ADR registry — `docs/adr/NNN-slug.md` files at the workspace
6
+ * root (`../../governance/adr-registry.mjs` owns the format and the index).
7
+ * With no arguments it dumps the whole registry: every record, its status, its
8
+ * supersession chain, and which rule/fitness ids it binds. Given an id it shows
9
+ * that one record and — the reverse lookup — which ADRs bind each of its
10
+ * bindings.
11
+ *
12
+ * It is descriptive, exactly like `graph`/`drift`/`history`: it never exits 1,
13
+ * because a description of what is recorded is never a finding. Only `check`
14
+ * exits 1. What it DOES refuse, loudly (exit 3, never clean):
15
+ *
16
+ * - an unreadable registry — a `docs/adr/` that exists but holds a malformed
17
+ * record, a duplicate id, or a file that will not parse. "Could not read the
18
+ * registry" must never read as "no ADRs";
19
+ * - a reference into the registry's OWN name space that names nothing: the id
20
+ * a caller asked about, and any record's `supersedes` target. Both resolve
21
+ * against the index, so a supersession chain is never rendered as fact
22
+ * unless its far end is a record. The invariant (`../../../../AGENTS.md`):
23
+ * a reference that does not resolve is `unknown`, never `pass`. A
24
+ * `bindings` entry is deliberately NOT on this list — it names an id in the
25
+ * rule/fitness name space, which this command holds no authority over;
26
+ * "What it cannot assert" below owns that limit and how a binding is
27
+ * surfaced instead.
28
+ *
29
+ * It does not need a project graph, Nx, or a boundary config: the registry is
30
+ * self-contained in the tree. `resolveCommandContext`'s heavy preamble is
31
+ * skipped, so `adr` runs on a tree with no Nx at all — the same posture
32
+ * `history` has when given no `--capture`.
33
+ *
34
+ * ## What it cannot assert
35
+ *
36
+ * It reports what the registry records; it does not verify that a bound
37
+ * rule/fitness exists anywhere else in the workspace (that is the decisionRef
38
+ * validator's question at load time). It CANNOT, and the reason is worth
39
+ * stating because it is easy to write a check that only looks like one: this
40
+ * command loads no boundary config, so the only id set in reach is
41
+ * `knownFitness`, and `boundFitnessIds` derives that from the records' own
42
+ * `bindings`. Testing a binding against it is self-resolution — vacuous for a
43
+ * bare id, and for a `rule:`/`fitness:`-prefixed one an artifact of
44
+ * `resolveDecisionRef` stripping the prefix off one side only, which would
45
+ * refuse `rule:no-such-rule` and the equally valid `rule:no-direct-dep`
46
+ * alike. So no binding is refused here, and — the honest consequence — the
47
+ * `(unknown)` marker `../report/adr-text.mjs` renders cannot fire on any run
48
+ * driven from THIS command: every binding is in the set by construction. The
49
+ * marker is real and both text faces apply it identically (one `bindingsLine`
50
+ * serves the dump and the single-record report, so they cannot disagree about
51
+ * which bindings carry it); what is missing is an id set that did not come
52
+ * from the bindings, which only a caller holding the workspace's declared
53
+ * rule/fitness ids can supply. Until one does, a binding is surfaced and
54
+ * never adjudicated: carried verbatim in the text and in the envelope's
55
+ * `bindings` beside `knownFitness`, at exit 0. Naming a limit is not a
56
+ * verdict; leaving it unnamed would be the silent direction
57
+ * (`../../../../AGENTS.md`).
58
+ */
59
+ import { jsonEnvelope, renderJson } from "../report/json.mjs";
60
+ import {
61
+ formatAdrDump,
62
+ formatAdrMissing,
63
+ formatAdrRecord,
64
+ formatAdrReverse,
65
+ } from "../report/adr-text.mjs";
66
+ import { ADR_DIR, stripAdrPrefix } from "../governance/adr-registry.mjs";
67
+ import { adrsBinding, boundFitnessIds, loadAdrRegistry } from "../governance/adr-registry.mjs";
68
+
69
+ /**
70
+ * The other half of the id name space the positional argument answers
71
+ * (`docs/usage/adr.md` and `docs/reference/adr.md`, "The id name space"): a
72
+ * `rule:`/`fitness:`-prefixed ref names a rule or fitness id, and the reverse
73
+ * lookup for one no ADR binds is a legitimate, ok fact — most fitness ids
74
+ * are never bound by any ADR, and that is not the same thing as being
75
+ * unresolved. Every other spelling is read as an attempted ADR reference —
76
+ * bare `NNN-slug`, the `adr:`-prefixed spelling `../governance/row-schema.mjs`'s
77
+ * own decisionRef docs recommend, or any other near-miss (wrong case, a
78
+ * truncation, a path-traversal shape) — so a miss there reports unresolved
79
+ * instead of silently falling into this pattern's empty-but-clean case.
80
+ */
81
+ const FITNESS_REF_PATTERN = /^(?:rule|fitness):/u;
82
+
83
+ /**
84
+ * The result of reading one registry: the records, the index, and the known
85
+ * rule/fitness id set derived from the records' own bindings.
86
+ *
87
+ * @typedef {object} AdrContext
88
+ * @property {object[]} records
89
+ * @property {Map<string, object>} byId
90
+ * @property {Set<string>} knownFitness The ids every record's `bindings`
91
+ * mention — the names `resolveDecisionRef` answers as `fitness`.
92
+ */
93
+
94
+ /**
95
+ * Reads the registry at `root` and derives the known-fitness set. Throws on an
96
+ * unreadable registry — the caller (cli.mjs) maps that to exit 3.
97
+ *
98
+ * @param {string} root
99
+ * @param {{loadAdrRegistryOverride?: typeof loadAdrRegistry, tracked?: string[],
100
+ * lstatSync?: (path: string) => {isSymbolicLink: () => boolean},
101
+ * realpathSync?: (path: string) => string}} [io] `tracked`, `lstatSync` and
102
+ * `realpathSync` are forwarded to `loadAdrRegistry` unchanged — see its own
103
+ * header for what they guard against.
104
+ * @returns {AdrContext}
105
+ */
106
+ export function readAdrContext(root, io = {}) {
107
+ const registry = (io.loadAdrRegistryOverride ?? loadAdrRegistry)(root, {
108
+ tracked: io.tracked,
109
+ lstatSync: io.lstatSync,
110
+ realpathSync: io.realpathSync,
111
+ });
112
+ return {
113
+ records: registry.records,
114
+ byId: registry.byId,
115
+ knownFitness: boundFitnessIds(registry.records),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * The verdict for one `adr` run: the payload for both renderers, the status,
121
+ * and the coverage that decides exit 0 against 3.
122
+ *
123
+ * @param {string} root
124
+ * @param {{id?: string}} options
125
+ * @param {{loadAdrRegistryOverride?: typeof loadAdrRegistry, tracked?: string[],
126
+ * lstatSync?: (path: string) => {isSymbolicLink: () => boolean},
127
+ * realpathSync?: (path: string) => string}} [io] Forwarded to
128
+ * `readAdrContext` unchanged.
129
+ * @returns {{status: "ok"|"no-verdict", result: object, coverage: object,
130
+ * report: {text: string, json: string}}}
131
+ * @throws {Error} on an unreadable registry (exit-3 class).
132
+ */
133
+ export function adrCommand(root, options, io = {}) {
134
+ const ctx = readAdrContext(root, io);
135
+
136
+ const { records, byId, knownFitness } = ctx;
137
+
138
+ // An id the caller asked about that the registry does not know is a named
139
+ // unknown, not a clean result — the invariant. Two cases, told apart by the
140
+ // id's shape: a `rule:x`/`fitness:x` ref (`FITNESS_REF_PATTERN`, above) is a
141
+ // reverse lookup, and an unenforced one is a fact about the registry, ok.
142
+ // Everything else is read as an attempted ADR reference — bare `NNN-slug`,
143
+ // or `adr:`-prefixed (`stripAdrPrefix` strips it before the lookup below,
144
+ // the same normalisation `resolveDecisionRef` applies) — and one that does
145
+ // not resolve is unresolved, exit 3. Classifying by "is this fitness-shaped"
146
+ // rather than "does this match the ADR pattern" is what catches a near-miss
147
+ // ADR spelling — that `adr:` prefix, a case mismatch, a truncation, a
148
+ // path-traversal shape, or anything else that is neither a real record nor
149
+ // a fitness/rule reference: every one of those used to fall through to the
150
+ // reverse-lookup branch below and read as a clean, unenforced-but-known
151
+ // fact instead of a reference the registry could not resolve at all.
152
+ const requestedId = options.id;
153
+ const isFitnessRef = requestedId !== undefined && FITNESS_REF_PATTERN.test(requestedId);
154
+ const resolvedAdrId = requestedId === undefined ? undefined : stripAdrPrefix(requestedId);
155
+ const unresolved = [];
156
+ if (requestedId !== undefined && !isFitnessRef && !byId.has(resolvedAdrId)) {
157
+ unresolved.push({ ref: requestedId, why: `${requestedId} is not an ADR in ${ADR_DIR}` });
158
+ }
159
+
160
+ // Every record's `supersedes` target, resolved against the registry index.
161
+ // `validateRecord` (`../governance/adr-registry.mjs`) checks the SHAPE of a
162
+ // supersedes entry — that it looks like an ADR id — and nothing more, so a
163
+ // `supersedes: ["0000-does-not-exist"]` loaded clean and was rendered below
164
+ // as a supersession chain, in `result.supersedes` and in the record's own
165
+ // text block, under `status: "ok"` and `coverage.complete: true`. A chain
166
+ // whose far end is not a record is a claim about a decision this workspace
167
+ // never recorded — precisely the "unresolvable decisionRef — a binding, a
168
+ // supersedes target, or a row's decisionRef that names nothing" this
169
+ // module's header promises to refuse, and printing it as fact is the silent
170
+ // direction the invariant (`../../../../AGENTS.md`) forbids. `stripAdrPrefix`
171
+ // is applied for the same reason the requested-id lookup above applies it:
172
+ // the `adr:`-prefixed spelling this tool's own docs recommend must not be
173
+ // the one spelling that fails to resolve against a record that exists.
174
+ for (const record of records) {
175
+ for (const ref of record.supersedes) {
176
+ if (byId.has(stripAdrPrefix(ref))) continue;
177
+ unresolved.push({
178
+ ref,
179
+ why: `${record.id} supersedes ${ref}, which is not an ADR in ${ADR_DIR}`,
180
+ });
181
+ }
182
+ }
183
+
184
+ const result = {
185
+ adrs: records.map((record) => record.id),
186
+ registry: {
187
+ dir: ADR_DIR,
188
+ count: records.length,
189
+ },
190
+ statuses: records.map((record) => ({ id: record.id, status: record.status })),
191
+ bindings: records.flatMap((record) =>
192
+ record.bindings.map((binding) => ({ adr: record.id, binding })),
193
+ ),
194
+ supersedes: records.flatMap((record) =>
195
+ record.supersedes.map((ref) => ({ adr: record.id, supersedes: ref })),
196
+ ),
197
+ unresolved,
198
+ knownFitness: [...knownFitness].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
199
+ };
200
+
201
+ const text =
202
+ requestedId === undefined
203
+ ? formatAdrDump({ records, knownFitness })
204
+ : byId.has(resolvedAdrId)
205
+ ? formatAdrRecord(byId.get(resolvedAdrId), knownFitness)
206
+ : isFitnessRef
207
+ ? formatAdrReverse({ fitnessId: requestedId, adrIds: adrsBinding(records, requestedId) })
208
+ : formatAdrMissing({ adrId: requestedId });
209
+
210
+ const coverage = {
211
+ complete: unresolved.length === 0,
212
+ // `adr` reads no source files and no graph — the units it counted are the
213
+ // records it read. `complete` is what decides the exit code.
214
+ projects: 0,
215
+ analyzedFiles: records.length,
216
+ imports: 0,
217
+ notAnalyzed: unresolved.map(({ ref, why }) => ({ file: `${ADR_DIR}/${ref}.md`, reason: why })),
218
+ blindSpots: [],
219
+ notes: [],
220
+ };
221
+
222
+ const status = unresolved.length === 0 ? "ok" : "no-verdict";
223
+ const exitCode = status === "ok" ? 0 : 3;
224
+
225
+ const envelope = jsonEnvelope({
226
+ command: "adr",
227
+ context: {
228
+ root,
229
+ provider: "native",
230
+ marker: ADR_DIR,
231
+ provenance: null,
232
+ },
233
+ status,
234
+ exitCode,
235
+ coverage,
236
+ result,
237
+ });
238
+
239
+ return {
240
+ status,
241
+ result,
242
+ coverage,
243
+ report: {
244
+ text: `${text}\n`,
245
+ json: renderJson(envelope),
246
+ },
247
+ };
248
+ }