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