@ecoma-io/archkeep 0.25.0 → 0.27.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 (121) hide show
  1. package/cli.mjs +167 -506
  2. package/commands.mjs +7 -1
  3. package/package.json +1 -1
  4. package/src/architecture-intent/judge.mjs +1 -1
  5. package/src/architecture-intent/model.mjs +34 -0
  6. package/src/canonical.mjs +2 -1
  7. package/src/commands/adr-for-workspace.mjs +63 -0
  8. package/src/commands/analyze-capability.mjs +13 -0
  9. package/src/commands/change-intent.mjs +11 -0
  10. package/src/commands/change.mjs +251 -35
  11. package/src/commands/check-capability.mjs +13 -0
  12. package/src/commands/check.mjs +11 -3
  13. package/src/commands/compare-capability.mjs +20 -0
  14. package/src/commands/completeness.mjs +12 -0
  15. package/src/commands/context-command.mjs +31 -0
  16. package/src/commands/coverage-verdict.mjs +15 -2
  17. package/src/commands/debt.mjs +30 -0
  18. package/src/commands/decisions.mjs +59 -10
  19. package/src/commands/delta.mjs +266 -50
  20. package/src/commands/diff.mjs +45 -0
  21. package/src/commands/discover.mjs +37 -0
  22. package/src/commands/drift.mjs +45 -0
  23. package/src/commands/evolution.mjs +32 -2
  24. package/src/commands/explain-capability.mjs +11 -0
  25. package/src/commands/explain.mjs +24 -0
  26. package/src/commands/fitness.mjs +119 -10
  27. package/src/commands/govern-capability.mjs +16 -0
  28. package/src/commands/graph.mjs +39 -0
  29. package/src/commands/health.mjs +34 -0
  30. package/src/commands/history.mjs +85 -2
  31. package/src/commands/impact.mjs +23 -0
  32. package/src/commands/inspect-capability.mjs +16 -0
  33. package/src/commands/plan-context-command.mjs +35 -0
  34. package/src/commands/policy.mjs +42 -4
  35. package/src/commands/provenance-command.mjs +15 -24
  36. package/src/commands/report.mjs +38 -0
  37. package/src/commands/rules-capability.mjs +16 -0
  38. package/src/commands/rules.mjs +3 -1
  39. package/src/commands/scenario.mjs +23 -0
  40. package/src/commands/trajectory.mjs +35 -0
  41. package/src/commands/waivers.mjs +30 -0
  42. package/src/corpus/goldens/.gitkeep +0 -0
  43. package/src/corpus/goldens/adr.json +62 -0
  44. package/src/corpus/goldens/adr.text +6 -0
  45. package/src/corpus/goldens/change.json +99 -0
  46. package/src/corpus/goldens/change.text +6 -0
  47. package/src/corpus/goldens/check.json +90 -0
  48. package/src/corpus/goldens/check.sarif +479 -0
  49. package/src/corpus/goldens/check.text +9 -0
  50. package/src/corpus/goldens/context.json +47 -0
  51. package/src/corpus/goldens/context.text +6 -0
  52. package/src/corpus/goldens/debt.json +59 -0
  53. package/src/corpus/goldens/debt.text +7 -0
  54. package/src/corpus/goldens/decisions.json +74 -0
  55. package/src/corpus/goldens/decisions.text +7 -0
  56. package/src/corpus/goldens/delta.json +94 -0
  57. package/src/corpus/goldens/delta.sarif +472 -0
  58. package/src/corpus/goldens/delta.text +5 -0
  59. package/src/corpus/goldens/diff.json +58 -0
  60. package/src/corpus/goldens/diff.text +4 -0
  61. package/src/corpus/goldens/discover.json +77 -0
  62. package/src/corpus/goldens/discover.text +10 -0
  63. package/src/corpus/goldens/drift.json +43 -0
  64. package/src/corpus/goldens/drift.text +3 -0
  65. package/src/corpus/goldens/evolution.json +330 -0
  66. package/src/corpus/goldens/evolution.text +32 -0
  67. package/src/corpus/goldens/explain.json +60 -0
  68. package/src/corpus/goldens/explain.text +7 -0
  69. package/src/corpus/goldens/fitness.json +45 -0
  70. package/src/corpus/goldens/fitness.text +3 -0
  71. package/src/corpus/goldens/graph.json +78 -0
  72. package/src/corpus/goldens/graph.text +10 -0
  73. package/src/corpus/goldens/health.json +72 -0
  74. package/src/corpus/goldens/health.text +10 -0
  75. package/src/corpus/goldens/help.text +104 -0
  76. package/src/corpus/goldens/history.json +43 -0
  77. package/src/corpus/goldens/history.text +4 -0
  78. package/src/corpus/goldens/impact.json +327 -0
  79. package/src/corpus/goldens/impact.text +10 -0
  80. package/src/corpus/goldens/provenance.json +453 -0
  81. package/src/corpus/goldens/provenance.text +14 -0
  82. package/src/corpus/goldens/reconcile.json +168 -0
  83. package/src/corpus/goldens/reconcile.text +3 -0
  84. package/src/corpus/goldens/report.json +168 -0
  85. package/src/corpus/goldens/report.text +42 -0
  86. package/src/corpus/goldens/rules verify.json +0 -0
  87. package/src/corpus/goldens/rules verify.text +0 -0
  88. package/src/corpus/goldens/scenario.json +441 -0
  89. package/src/corpus/goldens/scenario.text +28 -0
  90. package/src/corpus/goldens/trajectory.json +83 -0
  91. package/src/corpus/goldens/trajectory.text +10 -0
  92. package/src/corpus/goldens/usage-error.text +105 -0
  93. package/src/corpus/goldens/waivers.json +40 -0
  94. package/src/corpus/goldens/waivers.text +1 -0
  95. package/src/corpus/goldens-lsp/initialize.result.json +15 -0
  96. package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
  97. package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
  98. package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
  99. package/src/corpus/goldens-lsp/publish-violation.json +22 -0
  100. package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
  101. package/src/go-work.mjs +7 -23
  102. package/src/governance/debt-ledger.mjs +5 -2
  103. package/src/governance/evolution-event.mjs +14 -0
  104. package/src/governance/evolution-store.mjs +59 -31
  105. package/src/governance/provenance-record.mjs +0 -152
  106. package/src/governance/verdict.mjs +26 -3
  107. package/src/intent/intent-manifest.json +14 -14
  108. package/src/lsp/server.mjs +5 -8
  109. package/src/lsp/workspace-index.mjs +55 -220
  110. package/src/options.mjs +10 -0
  111. package/src/providers/native/discover.mjs +13 -12
  112. package/src/providers/native/model.mjs +8 -3
  113. package/src/providers/nx-static.mjs +231 -0
  114. package/src/report/json.mjs +3 -2
  115. package/src/report/sarif.mjs +8 -3
  116. package/src/rules/edge-constraints.mjs +8 -0
  117. package/src/rules/index.mjs +24 -10
  118. package/src/rules/messages.mjs +64 -14
  119. package/src/rules/specifiers.mjs +21 -20
  120. package/src/tsconfig-paths.mjs +7 -14
  121. package/src/verdict.mjs +127 -16
package/src/verdict.mjs CHANGED
@@ -2,6 +2,22 @@
2
2
  * The process's exit-code contract, and the one function that turns a run's
3
3
  * counts into the verdict every format agrees on.
4
4
  *
5
+ * Layer: the check lane — the process's exit codes, the coverage clauses a
6
+ * run that could not look needs, and `verdictFor`, the per-lint verdict the
7
+ * `check` command words its envelope and its exit from. Its counterpart,
8
+ * `./governance/verdict.mjs`, owns the verdict vocabulary and decision
9
+ * construction (`VERDICTS`, `isVerdict`, `verdictForStatus`, `fitnessVerdict`,
10
+ * `buildDecision`); the `buildDecision` import below is that layering made
11
+ * one-directional — the check lane calls governance, and governance never
12
+ * imports this file. Two near-collision pairs sit across that boundary, and
13
+ * the name decides which file a reader wants: `verdictFor` here folds a
14
+ * run's counts into status + exitCode, while governance's `verdictForStatus`
15
+ * looks one status up in its frozen map; `EXIT_FOR_STATUS` here is derived
16
+ * from this module's `EXIT`, while governance's `VERDICT_FOR_STATUS` is an
17
+ * independently frozen map. Exit-code, coverage and check-lane concerns live
18
+ * here; verdict vocabulary and decision construction live in
19
+ * `./governance/verdict.mjs`.
20
+ *
5
21
  * Both sit here rather than in `../cli.mjs` because two callers need them and
6
22
  * only one of the two is the CLI shell: `./commands/check.mjs` words its own
7
23
  * `--format json` envelope's `status` and `exitCode` from `verdictFor`, and
@@ -17,6 +33,7 @@
17
33
  */
18
34
 
19
35
  import { buildDecision } from "./governance/verdict.mjs";
36
+ import { describe, isPlainObject } from "./values.mjs";
20
37
 
21
38
  export const EXIT = Object.freeze({
22
39
  ok: 0,
@@ -88,6 +105,79 @@ export function coverageComplete({ unchecked, blindSpotCount, analyzed }) {
88
105
  return unchecked === 0 && blindSpotCount === 0 && analyzed > 0;
89
106
  }
90
107
 
108
+ /**
109
+ * The count keys `verdictFor` folds, spelled as a roster because the
110
+ * destructure inside that function cannot be introspected. Every key is a
111
+ * non-negative integer or `undefined` (optional keys only); anything else —
112
+ * and any key not on this roster — is refused by the input latch below rather
113
+ * than destructured, because a misspelled key drops out of the destructure
114
+ * as `undefined` and `undefined > 0` is `false` in every lane: the missing
115
+ * count reads as zero, and a failing run can pass. The roster and the
116
+ * destructure are one list in two spellings; a key added to either without
117
+ * the other is a no-verdict on every `check` run, not a silent default.
118
+ */
119
+ const REQUIRED_COUNT_KEYS = Object.freeze([
120
+ "violations",
121
+ "declaredEdgeFindings",
122
+ "goWorkDrift",
123
+ "tsconfigPathsDead",
124
+ "intentFindings",
125
+ "intentUnresolved",
126
+ "unchecked",
127
+ "analyzed",
128
+ "blindSpots",
129
+ ]);
130
+ const OPTIONAL_COUNT_KEYS = Object.freeze([
131
+ "intentUnresolvedDecisionRefs",
132
+ "fitnessFail",
133
+ "fitnessUnknown",
134
+ "customRuleFail",
135
+ "customRuleUnknown",
136
+ ]);
137
+ const KNOWN_COUNT_KEYS = new Set([...REQUIRED_COUNT_KEYS, ...OPTIONAL_COUNT_KEYS]);
138
+
139
+ /**
140
+ * Whether a value is a count `verdictFor` can fold — a non-negative integer.
141
+ *
142
+ * @param {unknown} value
143
+ * @returns {boolean}
144
+ */
145
+ function isCount(value) {
146
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
147
+ }
148
+
149
+ /**
150
+ * The input latch for `verdictFor`: names the first way `counts` is not a
151
+ * shape the fold can read, or `null` when it is. A returned problem becomes
152
+ * a no-verdict verdict, not a throw — the fold's contract is the same triple
153
+ * every verdict-bearing caller already consumes, and a malformed internal
154
+ * count is a fact about the run the caller reports the same way it reports a
155
+ * blind spot.
156
+ *
157
+ * @param {object} counts
158
+ * @returns {string|null}
159
+ */
160
+ function countInputProblem(counts) {
161
+ if (!isPlainObject(counts)) {
162
+ return `the verdict fold refuses counts of ${describe(counts)} — the counts must be an object for any lane of the fold to read. This is a bug in archkeep, not a fact about the workspace.`;
163
+ }
164
+ const unknownKey = Object.keys(counts).find((key) => !KNOWN_COUNT_KEYS.has(key));
165
+ if (unknownKey !== undefined) {
166
+ return `the verdict fold refuses the unknown count key "${unknownKey}" — expected one of ${[...KNOWN_COUNT_KEYS].join(", ")}. A misspelled count key would read as 0 in every lane below and can flip a failing run into a pass. This is a bug in archkeep, not a fact about the workspace.`;
167
+ }
168
+ for (const key of REQUIRED_COUNT_KEYS) {
169
+ if (!isCount(counts[key])) {
170
+ return `the verdict fold refuses counts where the required key "${key}" is ${describe(counts[key])} — every count must be a non-negative integer, or a missing one reads as 0 in every lane below. This is a bug in archkeep, not a fact about the workspace.`;
171
+ }
172
+ }
173
+ for (const key of OPTIONAL_COUNT_KEYS) {
174
+ if (counts[key] !== undefined && !isCount(counts[key])) {
175
+ return `the verdict fold refuses counts where the optional key "${key}" is ${describe(counts[key])} — every count must be a non-negative integer, or a malformed one reads as 0 in every lane below. This is a bug in archkeep, not a fact about the workspace.`;
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
91
181
  /**
92
182
  * The one place that turns a run's counts into the verdict every format
93
183
  * agrees on. `runCheck` uses it for the process's exit code; `check` uses the
@@ -103,6 +193,12 @@ export function coverageComplete({ unchecked, blindSpotCount, analyzed }) {
103
193
  * verify — is the case that must not read `ok`, because `ok` is read as
104
194
  * "checked, and fine".
105
195
  *
196
+ * The input latch runs before any lane: counts the fold cannot read are
197
+ * refused as a no-verdict whose single reason names the malformed input
198
+ * (`countInputProblem` above), never folded past as silent zeros. The refusal
199
+ * carries `coverageComplete: false` because the coverage counts are among the
200
+ * unread input — a coverage claim over them would be a second unread field.
201
+ *
106
202
  * The `decision` is the canonical 4-state verb of the same verdict
107
203
  * (`./governance/verdict.mjs`), built from the same counts so the envelope's
108
204
  * `status` and its `decision.verdict` cannot disagree: `ok`→`pass`,
@@ -118,22 +214,37 @@ export function coverageComplete({ unchecked, blindSpotCount, analyzed }) {
118
214
  * clean one. `decision.reason` joins it with the intent/fitness/custom
119
215
  * clauses where the lane is no-verdict.
120
216
  */
121
- export function verdictFor({
122
- violations,
123
- declaredEdgeFindings,
124
- goWorkDrift,
125
- tsconfigPathsDead,
126
- intentFindings,
127
- intentUnresolved,
128
- intentUnresolvedDecisionRefs = 0,
129
- unchecked,
130
- analyzed,
131
- blindSpots,
132
- fitnessFail = 0,
133
- fitnessUnknown = 0,
134
- customRuleFail = 0,
135
- customRuleUnknown = 0,
136
- }) {
217
+ export function verdictFor(counts) {
218
+ const problem = countInputProblem(counts);
219
+ if (problem !== null) {
220
+ return {
221
+ status: "no-verdict",
222
+ exitCode: EXIT_FOR_STATUS["no-verdict"],
223
+ reasons: [problem],
224
+ decision: buildDecision({
225
+ status: "no-verdict",
226
+ coverageComplete: false,
227
+ findings: 0,
228
+ reason: problem,
229
+ }),
230
+ };
231
+ }
232
+ const {
233
+ violations,
234
+ declaredEdgeFindings,
235
+ goWorkDrift,
236
+ tsconfigPathsDead,
237
+ intentFindings,
238
+ intentUnresolved,
239
+ intentUnresolvedDecisionRefs = 0,
240
+ unchecked,
241
+ analyzed,
242
+ blindSpots,
243
+ fitnessFail = 0,
244
+ fitnessUnknown = 0,
245
+ customRuleFail = 0,
246
+ customRuleUnknown = 0,
247
+ } = counts;
137
248
  const coverageReasons = coverageIncompleteReasons({ unchecked, blindSpots, analyzed });
138
249
  if (
139
250
  violations > 0 ||