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