@ecoma-io/archkeep 0.22.2 → 0.24.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 (63) hide show
  1. package/package.json +40 -13
  2. package/src/analysis/jvm/packages.mjs +0 -17
  3. package/src/analysis/manifest-util.mjs +14 -5
  4. package/src/analysis/markdown.mjs +340 -0
  5. package/src/analysis/source-util.mjs +5 -4
  6. package/src/analysis/typescript.mjs +146 -0
  7. package/src/architecture-intent/judge.mjs +1 -1
  8. package/src/architecture-intent/model.mjs +3 -12
  9. package/src/commands/README.md +16 -7
  10. package/src/commands/change-intent.mjs +2 -11
  11. package/src/commands/check.mjs +234 -12
  12. package/src/commands/completeness.mjs +0 -32
  13. package/src/commands/context-command.mjs +13 -21
  14. package/src/commands/context.mjs +46 -47
  15. package/src/commands/coverage-verdict.mjs +12 -2
  16. package/src/commands/delta-snapshot.mjs +1 -5
  17. package/src/commands/diff.mjs +1 -1
  18. package/src/commands/discover.mjs +7 -3
  19. package/src/commands/evaluation-primitives.mjs +6 -2
  20. package/src/commands/explain.mjs +17 -20
  21. package/src/commands/graph.mjs +7 -0
  22. package/src/commands/health.mjs +4 -0
  23. package/src/commands/impact-reachability.mjs +104 -0
  24. package/src/commands/impact.mjs +9 -71
  25. package/src/commands/plan-context-command.mjs +5 -1
  26. package/src/commands/policy.mjs +4 -4
  27. package/src/commands/provenance.mjs +8 -2
  28. package/src/commands/scenario-evaluation.mjs +1 -1
  29. package/src/config.mjs +171 -17
  30. package/src/custom-rules/evidence.mjs +1 -1
  31. package/src/custom-rules/host.mjs +2 -2
  32. package/src/custom-rules/values.mjs +8 -3
  33. package/src/errors.mjs +24 -2
  34. package/src/eslint-config.mjs +2 -5
  35. package/src/fixtures/evolution-lifecycle/workspace.mjs +0 -5
  36. package/src/governance/adr-registry.mjs +33 -17
  37. package/src/governance/decision-graph.mjs +1 -1
  38. package/src/governance/evolution-store.mjs +36 -18
  39. package/src/governance/fitness-registry.mjs +1 -14
  40. package/src/governance/profile-registry.mjs +20 -23
  41. package/src/governance/provenance-record.mjs +1 -11
  42. package/src/governance/reconcile-score.mjs +0 -3
  43. package/src/governance/row-schema.mjs +1 -14
  44. package/src/governance/verdict.mjs +168 -4
  45. package/src/intent/intent-manifest.json +16 -16
  46. package/src/lsp/diagnose.mjs +2 -2
  47. package/src/lsp/server.mjs +1 -1
  48. package/src/lsp/workspace-index.mjs +3 -3
  49. package/src/options.mjs +1 -1
  50. package/src/providers/model-gate.mjs +59 -0
  51. package/src/providers/moon.mjs +6 -6
  52. package/src/providers/native/model.mjs +2 -16
  53. package/src/report/README.md +13 -7
  54. package/src/report/evidence.mjs +11 -168
  55. package/src/report/json.mjs +10 -7
  56. package/src/report/sarif.mjs +29 -4
  57. package/src/report/text.mjs +39 -0
  58. package/src/rules/README.md +18 -9
  59. package/src/{commands → rules}/edge-constraints.mjs +18 -12
  60. package/src/rules/index.mjs +30 -0
  61. package/src/values.mjs +49 -0
  62. package/src/verdict.mjs +58 -7
  63. package/src/workspace.mjs +29 -0
package/src/verdict.mjs CHANGED
@@ -8,9 +8,15 @@
8
8
  * `../cli.mjs`'s `runCheck` takes the process's exit code from the same call.
9
9
  * `../cli.mjs` re-exports `EXIT` under its own name, so every importer that
10
10
  * already reads it from there keeps working.
11
+ *
12
+ * `EXIT` is the one place a status→exit-code number is written. The
13
+ * status-keyed view of it, `EXIT_FOR_STATUS`, is derived from `EXIT` and is
14
+ * what the envelope's consistency check (`./report/json.mjs`) asserts against,
15
+ * so a consumer-facing status and the process's own exit code can never drift
16
+ * into two encodings of one contract.
11
17
  */
12
18
 
13
- import { buildDecision } from "./report/evidence.mjs";
19
+ import { buildDecision } from "./governance/verdict.mjs";
14
20
 
15
21
  export const EXIT = Object.freeze({
16
22
  ok: 0,
@@ -18,6 +24,20 @@ export const EXIT = Object.freeze({
18
24
  usage: 2,
19
25
  error: 3,
20
26
  });
27
+
28
+ /**
29
+ * The envelope `status`→`exitCode` view of `EXIT` — the one mapping
30
+ * `jsonEnvelope` asserts every command's envelope against. Derived from
31
+ * `EXIT` rather than restated, so the numbers are written exactly once;
32
+ * `usage` has no status because a usage error never reaches an envelope.
33
+ *
34
+ * @type {Readonly<Record<"ok"|"findings"|"no-verdict", 0|1|3>>}
35
+ */
36
+ export const EXIT_FOR_STATUS = Object.freeze({
37
+ ok: EXIT.ok,
38
+ findings: EXIT.violations,
39
+ "no-verdict": EXIT.error,
40
+ });
21
41
  /**
22
42
  * The coverage clauses of a no-verdict reason, spelled once — the strings
23
43
  * `verdictFor` joins into `decision.reason` and `check`'s text report renders
@@ -41,6 +61,33 @@ export function coverageIncompleteReasons({ unchecked, blindSpots, analyzed }) {
41
61
  analyzed === 0 ? "no file in scope could be analyzed — coverage incomplete" : null,
42
62
  ].filter(Boolean);
43
63
  }
64
+
65
+ /**
66
+ * The one completeness predicate — the three coverage axes conjoined, the
67
+ * boolean twin of `coverageIncompleteReasons` directly above, which words the
68
+ * same axes as clauses. `coverageVerdict` (`./commands/coverage-verdict.mjs`)
69
+ * reads its `complete` from here, `verdictFor` reads the decision's
70
+ * `coverageComplete` from here, and `check`'s coverage block reads its
71
+ * `complete` from here — three faces of one claim, so the envelope's
72
+ * `coverage.complete` and its `decision.coverageComplete` cannot disagree
73
+ * about a run neither re-derives from the other.
74
+ *
75
+ * The counts come from the caller because a command's coverage universe is
76
+ * its own: `check`'s is wider than `commandContext.analysis` (the go.work and
77
+ * tsconfig whole-file failures it pushes, the accepted `coverage.unowned`
78
+ * files it withdraws), and each face feeds the counts it is a claim about. A
79
+ * caller that reads plain `commandContext.analysis` should call
80
+ * `coverageVerdict` instead — this predicate is the law's last step, not the
81
+ * place failure classes get decided (`./analysis/source-util.mjs`'s
82
+ * classifiers own that line).
83
+ *
84
+ * @param {{unchecked: number, blindSpotCount: number, analyzed: number}} counts
85
+ * @returns {boolean} Whether the run judged everything in its scope.
86
+ */
87
+ export function coverageComplete({ unchecked, blindSpotCount, analyzed }) {
88
+ return unchecked === 0 && blindSpotCount === 0 && analyzed > 0;
89
+ }
90
+
44
91
  /**
45
92
  * The one place that turns a run's counts into the verdict every format
46
93
  * agrees on. `runCheck` uses it for the process's exit code; `check` uses the
@@ -57,7 +104,7 @@ export function coverageIncompleteReasons({ unchecked, blindSpots, analyzed }) {
57
104
  * "checked, and fine".
58
105
  *
59
106
  * The `decision` is the canonical 4-state verb of the same verdict
60
- * (`./report/evidence.mjs`), built from the same counts so the envelope's
107
+ * (`./governance/verdict.mjs`), built from the same counts so the envelope's
61
108
  * `status` and its `decision.verdict` cannot disagree: `ok`→`pass`,
62
109
  * `findings`→`fail`, `no-verdict`→`unknown`. `buildDecision` throws on any
63
110
  * invariant the counts violate (a `pass` over incomplete coverage, a `fail`
@@ -103,11 +150,15 @@ export function verdictFor({
103
150
  ) {
104
151
  return {
105
152
  status: "findings",
106
- exitCode: EXIT.violations,
153
+ exitCode: EXIT_FOR_STATUS.findings,
107
154
  reasons: coverageReasons,
108
155
  decision: buildDecision({
109
156
  status: "findings",
110
- coverageComplete: unchecked === 0 && blindSpots === 0 && analyzed > 0,
157
+ // The one completeness predicate, not a restatement: the decision's
158
+ // `coverageComplete` and the envelope's `coverage.complete` are the
159
+ // same claim about the same counts (`check` feeds both from one
160
+ // object), so they read it from one expression.
161
+ coverageComplete: coverageComplete({ unchecked, blindSpotCount: blindSpots, analyzed }),
111
162
  findings:
112
163
  violations +
113
164
  declaredEdgeFindings +
@@ -164,11 +215,11 @@ export function verdictFor({
164
215
  ].filter(Boolean);
165
216
  return {
166
217
  status: "no-verdict",
167
- exitCode: EXIT.error,
218
+ exitCode: EXIT_FOR_STATUS["no-verdict"],
168
219
  reasons,
169
220
  decision: buildDecision({
170
221
  status: "no-verdict",
171
- coverageComplete: unchecked === 0 && blindSpots === 0 && analyzed > 0,
222
+ coverageComplete: coverageComplete({ unchecked, blindSpotCount: blindSpots, analyzed }),
172
223
  findings: 0,
173
224
  reason: reasons.join("; "),
174
225
  }),
@@ -176,7 +227,7 @@ export function verdictFor({
176
227
  }
177
228
  return {
178
229
  status: "ok",
179
- exitCode: EXIT.ok,
230
+ exitCode: EXIT_FOR_STATUS.ok,
180
231
  reasons: [],
181
232
  decision: buildDecision({
182
233
  status: "ok",
package/src/workspace.mjs CHANGED
@@ -177,6 +177,35 @@ export function listTrackedFiles(workspaceRoot, { run = runProcess } = {}) {
177
177
  return out.split("\0").filter((path) => path !== "");
178
178
  }
179
179
 
180
+ /**
181
+ * Every file present in the worktree that git does NOT track, workspace-
182
+ * relative — the complement of `listTrackedFiles` above, and the answer to
183
+ * the only question that pair can ask: what exists in the tree the tracked
184
+ * universe was cut from, but never entered it (#675).
185
+ *
186
+ * `--exclude-standard` is what keeps this answer a git answer and not a walk
187
+ * of our own: ignored files — build outputs, dependency installs, anything a
188
+ * `.gitignore`, `.git/info/exclude` or `core.excludesFile` names — are not
189
+ * part of the workspace's population, exactly as for the tracked list. The
190
+ * header's argument for `git ls-files` over a tree walk applies here with one
191
+ * more clause: walking the tree for the untracked half would need those ignore
192
+ * rules reimplemented, and the copy would drift from `.gitignore` the first
193
+ * time a build directory was added.
194
+ *
195
+ * The order is git's worktree-traversal order, not a sorted order — every
196
+ * consumer of this list sorts before it renders (a file list in a report must
197
+ * not vary with git's traversal), the same discipline
198
+ * `../commands/check.mjs`'s `sortViolations` states for the tracked list.
199
+ *
200
+ * @param {string} workspaceRoot
201
+ * @param {{ run?: typeof runProcess }} [io]
202
+ * @returns {string[]}
203
+ */
204
+ export function listUntrackedFiles(workspaceRoot, { run = runProcess } = {}) {
205
+ const out = run("git", ["ls-files", "--others", "--exclude-standard", "-z"], workspaceRoot);
206
+ return out.split("\0").filter((path) => path !== "");
207
+ }
208
+
180
209
  /**
181
210
  * The `Workspace` the analysis contract defines — `{ root, projects, filesOf,
182
211
  * readFile, tsConfig }` — plus the per-project file index it is built from,