@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,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranked candidate model edits for `reconcile --propose` — the pure proposal
|
|
3
|
+
* half.
|
|
4
|
+
*
|
|
5
|
+
* `reconcile-score.mjs` decides the divergence states. This module converts
|
|
6
|
+
* each divergent element into a candidate edit of the canonical intent —
|
|
7
|
+
* READ-ONLY: the candidate is a description of a change an intentional human
|
|
8
|
+
* or agent might apply, never an application. Every candidate carries the
|
|
9
|
+
* evidence that supports it (the scored element it repairs) and, through its
|
|
10
|
+
* `proposed: true` / `notAuthoritative` marker, states plainly that the intent
|
|
11
|
+
* file has not been touched.
|
|
12
|
+
*
|
|
13
|
+
* ## The edit vocabulary
|
|
14
|
+
*
|
|
15
|
+
* A candidate is one of four kinds:
|
|
16
|
+
*
|
|
17
|
+
* - `add` — the observed architecture carries an element the declared model
|
|
18
|
+
* does not admit: a project outside the declared existence model, a
|
|
19
|
+
* dependency outside the `dependencies.allowed` allowlist. The edit admits
|
|
20
|
+
* the observed element into the model.
|
|
21
|
+
* - `removal` — the model states an element the observed architecture no
|
|
22
|
+
* longer builds: a required project that does not exist, a forbidden project
|
|
23
|
+
* that does, a forbidden dependency or tag rule the architecture builds. The
|
|
24
|
+
* edit drops the stale statement.
|
|
25
|
+
* - `tag-change` — a required project lacks a required tag. The edit updates
|
|
26
|
+
* the project's required tags to what reality actually carries.
|
|
27
|
+
* - `boundary-change` — a boundary `allowed`/`forbidden` row does not match
|
|
28
|
+
* the dependencies reality builds: the observed architecture builds a
|
|
29
|
+
* dependency a forbidden row bans, or never builds one an allowed row
|
|
30
|
+
* permits. The edit changes the boundary row.
|
|
31
|
+
*
|
|
32
|
+
* `match` and `unknown` states never produce a candidate: a `match` has
|
|
33
|
+
* nothing to repair, and an `unknown` has no verdict to propose from.
|
|
34
|
+
*
|
|
35
|
+
* ## One candidate per divergence, never two
|
|
36
|
+
*
|
|
37
|
+
* Divergence appears on two sides — the observed element (a forbidden edge, a
|
|
38
|
+
* missing required row's absence) and the intent row that states it. Both must
|
|
39
|
+
* never propose the same edit. So the two sides feed disjoint classification
|
|
40
|
+
* sets: observed elements propose `add` (`intentUnknownProject`,
|
|
41
|
+
* `dependencyNotAllowed` — the two divergences with no row of their own to
|
|
42
|
+
* edit), and intent rows propose the edit that changes the row
|
|
43
|
+
* (`removal`, `tag-change`, `boundary-change`). The witness edge of a
|
|
44
|
+
* forbidden boundary row is scored but never proposes — its row proposes for
|
|
45
|
+
* it.
|
|
46
|
+
*
|
|
47
|
+
* ## Rank, determinism
|
|
48
|
+
*
|
|
49
|
+
* Candidates are ranked by the scored element's severity (all `unexpected` at
|
|
50
|
+
* 4 before all `absent` at 3), then by a deterministic total order — plane,
|
|
51
|
+
* then name — so two runs over an unchanged tree and intent produce the same
|
|
52
|
+
* ranked list, byte for byte. Plain string comparison everywhere, never
|
|
53
|
+
* `localeCompare`.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A candidate model edit.
|
|
58
|
+
*
|
|
59
|
+
* @typedef {object} CandidateEdit
|
|
60
|
+
* @property {string} kind `"add"` | `"removal"` | `"tag-change"` | `"boundary-change"`.
|
|
61
|
+
* @property {string} plane The scored element's plane.
|
|
62
|
+
* @property {string} name The scored element's name.
|
|
63
|
+
* @property {string} state The scored element's state (`"absent"` or `"unexpected"`).
|
|
64
|
+
* @property {number} severity
|
|
65
|
+
* @property {string} evidence The classification that supports the proposal.
|
|
66
|
+
* @property {object|null} intentRow The scored element's `intentRow`, when the
|
|
67
|
+
* element came from the intent's own rows.
|
|
68
|
+
* @property {object} edit A description of the concrete edit:
|
|
69
|
+
* `{action, section, key, value?, reason}`.
|
|
70
|
+
* @property {boolean} proposed Always `true` — the candidate is a proposal,
|
|
71
|
+
* never an applied edit.
|
|
72
|
+
* @property {boolean} notAuthoritative Always `true` — the intent file has not
|
|
73
|
+
* been modified.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/** Observed-element divergences with no intent row of their own — `add`. */
|
|
77
|
+
const OBSERVED_ELEMENT_KIND = {
|
|
78
|
+
intentUnknownProject: "add",
|
|
79
|
+
dependencyNotAllowed: "add",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Intent-row divergences — the candidate edits the row itself. */
|
|
83
|
+
const INTENT_ROW_KIND = {
|
|
84
|
+
projectMissing: "removal",
|
|
85
|
+
projectPresent: "removal",
|
|
86
|
+
dependencyForbidden: "removal",
|
|
87
|
+
tagDependencyForbidden: "removal",
|
|
88
|
+
projectTagMissing: "tag-change",
|
|
89
|
+
intentForbiddenEdge: "boundary-change",
|
|
90
|
+
intentAllowedMissing: "boundary-change",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The concrete edit a candidate describes — the section, the row identity,
|
|
95
|
+
* and the action an operator would take by hand.
|
|
96
|
+
*
|
|
97
|
+
* @param {import("./reconcile-score.mjs").ScoredElement} element
|
|
98
|
+
* @returns {{action: string, section: string, key: string, value?: object, reason: string}}
|
|
99
|
+
*/
|
|
100
|
+
function editFor(element) {
|
|
101
|
+
const section = element.intentRow ? sectionForRow(element) : sectionForPlane(element.plane);
|
|
102
|
+
const base = { section, key: element.name };
|
|
103
|
+
|
|
104
|
+
switch (element.classification) {
|
|
105
|
+
case "intentUnknownProject":
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
action: "add",
|
|
109
|
+
reason:
|
|
110
|
+
"the observed architecture carries a project the declared model does not admit — add it to the model",
|
|
111
|
+
};
|
|
112
|
+
case "dependencyNotAllowed":
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
action: "add",
|
|
116
|
+
reason:
|
|
117
|
+
"the observed architecture builds a dependency outside dependencies.allowed — add it to the allowlist",
|
|
118
|
+
};
|
|
119
|
+
case "projectMissing":
|
|
120
|
+
return {
|
|
121
|
+
...base,
|
|
122
|
+
action: "remove",
|
|
123
|
+
reason:
|
|
124
|
+
"the intent requires this project, but the observed architecture does not build it — remove the required row, or build the project",
|
|
125
|
+
};
|
|
126
|
+
case "projectPresent":
|
|
127
|
+
return {
|
|
128
|
+
...base,
|
|
129
|
+
action: "remove",
|
|
130
|
+
reason:
|
|
131
|
+
"the intent forbids this project, but the observed architecture carries it — remove the forbidden row, or remove the project",
|
|
132
|
+
};
|
|
133
|
+
case "dependencyForbidden":
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
action: "remove",
|
|
137
|
+
reason:
|
|
138
|
+
"the intent forbids this dependency, but the observed architecture builds it — remove the forbidden row",
|
|
139
|
+
};
|
|
140
|
+
case "tagDependencyForbidden":
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
action: "remove",
|
|
144
|
+
reason:
|
|
145
|
+
"the intent forbids this tag dependency, but the observed architecture builds it — remove the tag rule",
|
|
146
|
+
};
|
|
147
|
+
case "projectTagMissing":
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
action: "update required tags",
|
|
151
|
+
reason:
|
|
152
|
+
"a required project lacks a required tag — align the required tags with the project's actual tags",
|
|
153
|
+
};
|
|
154
|
+
case "intentForbiddenEdge": {
|
|
155
|
+
const [from, to] = element.name.split(" → ");
|
|
156
|
+
return {
|
|
157
|
+
...base,
|
|
158
|
+
action: "change boundary row",
|
|
159
|
+
value: { from, to },
|
|
160
|
+
reason:
|
|
161
|
+
"the observed architecture builds a dependency this boundary row forbids — relax the row or change the boundary",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case "intentAllowedMissing": {
|
|
165
|
+
const [from, to] = element.name.split(" → ");
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
action: "change boundary row",
|
|
169
|
+
value: { from, to },
|
|
170
|
+
reason:
|
|
171
|
+
"the intent allows a dependency the architecture never builds — build it, or change the boundary row",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
// Every real classification is handled above; a classification that
|
|
176
|
+
// reaches the edit builder without an edit mapping is a programming
|
|
177
|
+
// error, and inventing one here would propose an edit nothing in the
|
|
178
|
+
// scoring model stands behind.
|
|
179
|
+
throw new Error(`reconcile: classification ${element.classification} has no candidate edit`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The intent section a row's edit would touch — derived from the row's `kind`,
|
|
185
|
+
* so the proposal names where the operator edits.
|
|
186
|
+
*
|
|
187
|
+
* @param {import("./reconcile-score.mjs").ScoredElement} element
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
function sectionForRow(element) {
|
|
191
|
+
switch (element.intentRow.kind) {
|
|
192
|
+
case "required":
|
|
193
|
+
return "projects.required";
|
|
194
|
+
case "forbidden":
|
|
195
|
+
return element.plane === "project" ? "projects.forbidden" : "dependencies.forbidden";
|
|
196
|
+
case "tag-forbidden":
|
|
197
|
+
return "forbiddenTags";
|
|
198
|
+
case "allowed":
|
|
199
|
+
return "dependencies.allowed";
|
|
200
|
+
default:
|
|
201
|
+
return element.plane;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The section an observed (row-less) element's edit would touch.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} plane
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
function sectionForPlane(plane) {
|
|
212
|
+
switch (plane) {
|
|
213
|
+
case "project":
|
|
214
|
+
return "projects";
|
|
215
|
+
case "edge":
|
|
216
|
+
return "dependencies";
|
|
217
|
+
case "tag":
|
|
218
|
+
return "forbiddenTags";
|
|
219
|
+
case "boundary":
|
|
220
|
+
return "boundaries";
|
|
221
|
+
default:
|
|
222
|
+
return plane;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Builds one candidate from a scored element, using the kind table that side
|
|
228
|
+
* (observed element or intent row) owns.
|
|
229
|
+
*
|
|
230
|
+
* @param {import("./reconcile-score.mjs").ScoredElement} element
|
|
231
|
+
* @param {Record<string, string>} kindByClassification
|
|
232
|
+
* @returns {CandidateEdit|null} `null` when the state has nothing to propose.
|
|
233
|
+
*/
|
|
234
|
+
function candidateFromElement(element, kindByClassification) {
|
|
235
|
+
if (element.state === "match" || element.state === "unknown") return null;
|
|
236
|
+
const kind = kindByClassification[element.classification];
|
|
237
|
+
if (kind === undefined) return null;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
kind,
|
|
241
|
+
plane: element.plane,
|
|
242
|
+
name: element.name,
|
|
243
|
+
state: element.state,
|
|
244
|
+
severity: element.severity,
|
|
245
|
+
evidence: element.classification,
|
|
246
|
+
intentRow: element.intentRow,
|
|
247
|
+
edit: editFor(element),
|
|
248
|
+
proposed: true,
|
|
249
|
+
notAuthoritative: true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Builds the ranked candidate list from scored divergence.
|
|
255
|
+
*
|
|
256
|
+
* Two disjoint sources, so a divergence is never proposed twice: observed
|
|
257
|
+
* elements with no intent row of their own (`intentUnknownProject`,
|
|
258
|
+
* `dependencyNotAllowed`, read from `scores.projects`/`scores.edges`) propose
|
|
259
|
+
* `add`, and intent rows propose the edit that changes the row itself
|
|
260
|
+
* (`removal`, `tag-change`, `boundary-change`, read from `scores.tags` — the
|
|
261
|
+
* per-required-tag elements `scoreProject` emits — and `scores.intentRows`).
|
|
262
|
+
* Candidates are ranked by severity (higher first — every `unexpected` before
|
|
263
|
+
* every `absent`), then by plane, then name — byte-identical across runs over
|
|
264
|
+
* an unchanged tree and intent.
|
|
265
|
+
*
|
|
266
|
+
* @param {object} scores From `reconcileScores`.
|
|
267
|
+
* @returns {CandidateEdit[]}
|
|
268
|
+
*/
|
|
269
|
+
export function buildRankedCandidates(scores) {
|
|
270
|
+
const candidates = [];
|
|
271
|
+
|
|
272
|
+
for (const element of scores.projects) {
|
|
273
|
+
const candidate = candidateFromElement(element, OBSERVED_ELEMENT_KIND);
|
|
274
|
+
if (candidate !== null) candidates.push(candidate);
|
|
275
|
+
}
|
|
276
|
+
for (const element of scores.edges) {
|
|
277
|
+
const candidate = candidateFromElement(element, OBSERVED_ELEMENT_KIND);
|
|
278
|
+
if (candidate !== null) candidates.push(candidate);
|
|
279
|
+
}
|
|
280
|
+
// `scores.tags` carries `projectTagMissing` elements (a required project's
|
|
281
|
+
// missing required tag) — an intent-row-owned divergence like `removal` and
|
|
282
|
+
// `boundary-change`, so it reads the same INTENT_ROW_KIND table. Omitting
|
|
283
|
+
// this loop left `projectTagMissing → "tag-change"` dead: `--propose` could
|
|
284
|
+
// never surface the one candidate kind that repairs a missing required tag.
|
|
285
|
+
for (const element of scores.tags) {
|
|
286
|
+
const candidate = candidateFromElement(element, INTENT_ROW_KIND);
|
|
287
|
+
if (candidate !== null) candidates.push(candidate);
|
|
288
|
+
}
|
|
289
|
+
for (const element of scores.intentRows) {
|
|
290
|
+
const candidate = candidateFromElement(element, INTENT_ROW_KIND);
|
|
291
|
+
if (candidate !== null) candidates.push(candidate);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
candidates.sort(
|
|
295
|
+
(a, b) =>
|
|
296
|
+
b.severity - a.severity ||
|
|
297
|
+
(a.plane < b.plane ? -1 : a.plane > b.plane ? 1 : 0) ||
|
|
298
|
+
(a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
|
|
299
|
+
);
|
|
300
|
+
return candidates;
|
|
301
|
+
}
|