@ecoma-io/archkeep 0.26.0 → 0.27.1

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 (62) hide show
  1. package/cli.mjs +4 -3
  2. package/package.json +2 -1
  3. package/src/architecture-intent/model.mjs +2 -2
  4. package/src/commands/README.md +7 -4
  5. package/src/commands/analyze-capability.mjs +2 -2
  6. package/src/commands/change-intent.mjs +2 -2
  7. package/src/commands/change.mjs +9 -5
  8. package/src/commands/check-capability.mjs +2 -2
  9. package/src/commands/check.mjs +3 -5
  10. package/src/commands/compare-capability.mjs +2 -2
  11. package/src/commands/completeness.mjs +2 -2
  12. package/src/commands/delta-snapshot.mjs +17 -0
  13. package/src/commands/delta.mjs +9 -5
  14. package/src/commands/diff.mjs +2 -2
  15. package/src/commands/evolution.mjs +10 -0
  16. package/src/commands/explain-capability.mjs +2 -2
  17. package/src/commands/fitness.mjs +2 -2
  18. package/src/commands/govern-capability.mjs +2 -2
  19. package/src/commands/inspect-capability.mjs +2 -2
  20. package/src/commands/rules-capability.mjs +2 -2
  21. package/src/commands/scenario-evaluation.mjs +97 -3
  22. package/src/corpus/goldens/adr.json +2 -2
  23. package/src/corpus/goldens/change.json +6 -5
  24. package/src/corpus/goldens/change.text +2 -2
  25. package/src/corpus/goldens/check.json +2 -2
  26. package/src/corpus/goldens/context.json +2 -2
  27. package/src/corpus/goldens/debt.json +4 -4
  28. package/src/corpus/goldens/debt.text +2 -2
  29. package/src/corpus/goldens/decisions.json +2 -2
  30. package/src/corpus/goldens/delta.json +4 -4
  31. package/src/corpus/goldens/delta.text +1 -1
  32. package/src/corpus/goldens/diff.json +4 -4
  33. package/src/corpus/goldens/diff.text +1 -1
  34. package/src/corpus/goldens/discover.json +2 -2
  35. package/src/corpus/goldens/drift.json +2 -2
  36. package/src/corpus/goldens/evolution.json +4 -2
  37. package/src/corpus/goldens/explain.json +2 -2
  38. package/src/corpus/goldens/fitness.json +2 -2
  39. package/src/corpus/goldens/graph.json +2 -2
  40. package/src/corpus/goldens/health.json +2 -2
  41. package/src/corpus/goldens/history.json +3 -3
  42. package/src/corpus/goldens/history.text +1 -1
  43. package/src/corpus/goldens/impact.json +2 -2
  44. package/src/corpus/goldens/provenance.json +2 -2
  45. package/src/corpus/goldens/reconcile.json +2 -2
  46. package/src/corpus/goldens/report.json +2 -2
  47. package/src/corpus/goldens/report.text +1 -1
  48. package/src/corpus/goldens/scenario.json +2 -2
  49. package/src/corpus/goldens/trajectory.json +3 -3
  50. package/src/corpus/goldens/trajectory.text +1 -1
  51. package/src/corpus/goldens/waivers.json +2 -2
  52. package/src/go-work.mjs +4 -5
  53. package/src/governance/debt-ledger.mjs +5 -2
  54. package/src/governance/evolution-event.mjs +1 -2
  55. package/src/governance/reconcile-score.mjs +59 -9
  56. package/src/intent/intent-manifest.json +8 -8
  57. package/src/providers/nx-static.mjs +4 -4
  58. package/src/report/text.mjs +1 -1
  59. package/src/rules/edge-constraints.mjs +4 -5
  60. package/src/rules/index.mjs +8 -10
  61. package/src/tsconfig-paths.mjs +4 -5
  62. package/src/fixtures/evolution-lifecycle/workspace.mjs +0 -248
@@ -2,10 +2,10 @@
2
2
  * The Nx provider's static acquisition — the graph built from the tree's own
3
3
  * tracked `project.json` files, with no Nx process asked.
4
4
  *
5
- * This is the acquisition `../lsp/workspace-index.mjs` composed inline until
6
- * the Phase 7 collapse: a language server is spawned by an editor, in a
7
- * directory, with nothing else — no `nx` binary to resolve, and a spawn per
8
- * index build would put `nx graph` on every file save. So beside
5
+ * This is the acquisition `../lsp/workspace-index.mjs` composes: a language
6
+ * server is spawned by an editor, in a directory, with nothing else — no `nx`
7
+ * binary to resolve, and a spawn per index build would put `nx graph` on
8
+ * every file save. So beside
9
9
  * `./nx.mjs`'s `readProjectGraph` (which asks Nx itself and is what
10
10
  * `../../cli.mjs`'s `check` runs) this module builds the same `{nodes,
11
11
  * dependencies}`-shaped starting point — here just the nodes; the caller
@@ -774,7 +774,7 @@ function formatUntrackedFilesGap(gap) {
774
774
  `⚠ ${count} project-owned file${count === 1 ? "" : "s"} ${count === 1 ? "is" : "are"} ` +
775
775
  `not tracked by git — never read by this run, so no boundary verdict here covers ${them}\n` +
776
776
  `${lines.join("\n")}\n` +
777
- `${DETAIL}git add ${them} so the next run reads ${them}, or let git ignore ${them}`
777
+ `${DETAIL}git add ${them} so the next run reads ${them}`
778
778
  );
779
779
  }
780
780
 
@@ -66,14 +66,13 @@ import {
66
66
  * table — it does NOT mean the edge is free of all boundary violations (see
67
67
  * this module's header for what is not checked).
68
68
  *
69
- * One of the FOUR finding families — PD-13 (2026-09-06) outcome (c): no
69
+ * One of the FOUR finding families: no
70
70
  * Finding supertype exists, and the relationship pin lives on `./index.mjs`'s
71
- * `violationOf` header. These verdicts are NOT `Violation` records: the
71
+ * `violationOf` header (stated in `../../../../docs/concepts/evidence.md`'s
72
+ * "Four finding families"). These verdicts are NOT `Violation` records: the
72
73
  * markdown-pairing fold in `../commands/check.mjs` reshapes each into the
73
74
  * exact record `violationOf` builds, and the families fold into the one
74
- * verdict lane as count keys into `verdictFor` (`../verdict.mjs`). The
75
- * canonical statement is the "Finding — the unowned concept" section of
76
- * `../../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
75
+ * verdict lane as count keys into `verdictFor` (`../verdict.mjs`).
77
76
  *
78
77
  * @param {{source: string, target: string}} edge The graph edge to judge.
79
78
  * @param {object} nodes The project graph's `nodes` map (carries `data.tags`).
@@ -425,20 +425,18 @@ export function exemptResolvedFile(site, exemptedFiles) {
425
425
  }
426
426
 
427
427
  /**
428
- * Builds one `Violation` — the rules lane's canonical violation record (PD-13
429
- * (2026-09-06) in `../../../../docs/architecture/refactor/DECISIONS.md`: the
430
- * Violation concept ruled outcome (a), this constructor its one home).
428
+ * Builds one `Violation` — the rules lane's canonical violation record (the
429
+ * relationship pin below, stated in `docs/concepts/evidence.md`).
431
430
  *
432
431
  * This is also one of the FOUR finding families — `violationOf` here,
433
432
  * `judgeEdge` (`./edge-constraints.mjs`), `compareGoWork` (`../go-work.mjs`)
434
433
  * and `judgeTsconfigPaths` (`../tsconfig-paths.mjs`) — and no Finding
435
- * supertype exists (PD-13 outcome (c)): what binds the four is the
436
- * relationship pin. Each family folds into the one verdict lane as count
437
- * keys into `verdictFor` (`../verdict.mjs`), and `../commands/check.mjs`'s
438
- * markdown fold is the documented seam where `judgeEdge`'s verdicts are
439
- * reshaped into the exact record this function builds. The canonical
440
- * statement is the "Finding — the unowned concept" section of
441
- * `../../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
434
+ * supertype exists: what binds the four is the relationship pin, stated in
435
+ * `../../../../docs/concepts/evidence.md`'s "Four finding families". Each
436
+ * family folds into the one verdict lane as count keys into `verdictFor`
437
+ * (`../verdict.mjs`), and `../commands/check.mjs`'s markdown fold is the
438
+ * documented seam where `judgeEdge`'s verdicts are reshaped into the exact
439
+ * record this function builds.
442
440
  */
443
441
  function violationOf(site, sourceProject, targetProject, messageId, data = {}, constraint = null) {
444
442
  return {
@@ -114,13 +114,12 @@ function probeDirectory(target, base, root) {
114
114
  * need no filesystem: the table and its base come from the resolver's own
115
115
  * parsed context, and existence arrives as a predicate.
116
116
  *
117
- * One of the FOUR finding families — PD-13 (2026-09-06) outcome (c): no
117
+ * One of the FOUR finding families: no
118
118
  * Finding supertype exists, and the relationship pin lives on
119
- * `./rules/index.mjs`'s `violationOf` header. These findings stay this
119
+ * `./rules/index.mjs`'s `violationOf` header (stated in
120
+ * `../../../docs/concepts/evidence.md`'s "Four finding families"). These findings stay this
120
121
  * family's own shape and fold into the one verdict lane as count keys into
121
- * `verdictFor` (`./verdict.mjs`). The canonical statement is the
122
- * "Finding — the unowned concept" section of
123
- * `../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
122
+ * `verdictFor` (`./verdict.mjs`).
124
123
  *
125
124
  * @param {{ paths: Record<string, unknown>,
126
125
  * base: string,
@@ -1,248 +0,0 @@
1
- /**
2
- * Fixture scaffolding for the Wave 3 W8 evolution-lifecycle conformance suite
3
- * (`../evolution-lifecycle.integration.test.mjs`). This module is the ONE home
4
- * for the real-git workspace builders that suite uses — a throwaway native Go
5
- * workspace per case, materialized through real `git`, driven through the real
6
- * `archkeep evolution` entry point.
7
- *
8
- * It deliberately reuses the native-workspace recipe already proven by
9
- * `../commands/evolution.cli.integration.test.mjs` (an `archkeep.json` model,
10
- * a `module-boundaries.config.mjs` law, and Go sources) rather than inventing
11
- * a second convention, and threads the same environment guard (`../process.mjs`).
12
- *
13
- * Nothing here decides a verdict. It builds trees and drives the CLI; the
14
- * assertions live in the suite. Keeping the builders here (and only here) is
15
- * what the W8 task boundary requires: fixture scaffolding lives in
16
- * `./fixtures/evolution-lifecycle/`, nowhere else.
17
- */
18
-
19
- import { execFileSync } from "node:child_process";
20
- import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
21
- import { tmpdir } from "node:os";
22
- import { join } from "node:path";
23
-
24
- import { EXIT, runCli } from "../../../cli.mjs";
25
- import { SPAWN_BUDGET_MS, SPAWN_TEST_BUDGET_MS } from "../../../spawn-budget.mjs";
26
- import { environmentForTree } from "../../workspace.mjs";
27
-
28
- export { SPAWN_TEST_BUDGET_MS, EXIT };
29
-
30
- /** Identity flags keeping every fixture commit independent of the machine. */
31
- const IDENTITY = ["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false"];
32
-
33
- /**
34
- * Runs git in `cwd` through the same environment guard production uses, with
35
- * the single-spawn budget on every child so a wedged git fails the test rather
36
- * than blocking the worker thread forever.
37
- */
38
- export function git(cwd, ...args) {
39
- // used by its own test
40
- return execFileSync("git", args, {
41
- cwd,
42
- env: environmentForTree(),
43
- encoding: "utf8",
44
- timeout: SPAWN_BUDGET_MS,
45
- killSignal: "SIGKILL",
46
- });
47
- }
48
-
49
- /** Writes `text` to `root/relativePath`, creating parent directories. */
50
- export function writeIn(root, relativePath, text) {
51
- // used by its own test
52
- mkdirSync(join(root, relativePath, ".."), { recursive: true });
53
- writeFileSync(join(root, relativePath), text);
54
- }
55
-
56
- /** Stages every change and commits with the fixture identity; returns the SHA. */
57
- export function commit(root, message) {
58
- // used by its own test
59
- git(root, ...IDENTITY, "add", "-A");
60
- git(root, ...IDENTITY, "commit", "-q", "-m", message);
61
- return git(root, "rev-parse", "HEAD").trim();
62
- }
63
-
64
- /**
65
- * Opens a brand-new throwaway native git workspace (never the repository's own
66
- * tree). `archkeep.json` declares two Go projects on two layers, exactly the
67
- * MODEL `../commands/evolution.cli.integration.test.mjs` uses, so a case can
68
- * lay an edge between them and the native provider draws it.
69
- *
70
- * @returns {{root: string}}
71
- */
72
- export function createWorkspace() {
73
- const root = mkdtempSync(join(tmpdir(), "archkeep-lifecycle-"));
74
- git(root, "init", "-q", "-b", "main");
75
- writeIn(root, "archkeep.json", `${MODEL()}\n`);
76
- writeIn(root, "libs/alpha/go.mod", "module example.com/alpha\n\ngo 1.22\n");
77
- writeIn(root, "libs/beta/go.mod", "module example.com/beta\n\ngo 1.22\n");
78
- return { root };
79
- }
80
-
81
- /**
82
- * The native workspace model: two Go projects on two layers (alpha is
83
- * `layer:a`, beta is `layer:b`), the law file exempted from coverage.
84
- */
85
- const MODEL = () =>
86
- JSON.stringify(
87
- {
88
- projects: {
89
- declared: [
90
- { root: "libs/alpha", name: "alpha", tags: ["layer:a"] },
91
- { root: "libs/beta", name: "beta", tags: ["layer:b"] },
92
- ],
93
- },
94
- coverage: {
95
- exempt: [{ path: "module-boundaries.config.mjs", reason: "the workspace's own law" }],
96
- },
97
- },
98
- null,
99
- 2,
100
- );
101
-
102
- /** The eight options a valid boundary law must carry, per `policyFrom`. */
103
- const OPTIONS = `export const moduleBoundaryOptions = {
104
- allow: [],
105
- buildTargets: ["build"],
106
- enforceBuildableLibDependency: false,
107
- allowCircularSelfDependency: false,
108
- checkDynamicDependenciesExceptions: [],
109
- ignoredCircularDependencies: [],
110
- banTransitiveDependencies: false,
111
- checkNestedExternalImports: false,
112
- };
113
- `;
114
-
115
- export const ALPHA_CLEAN = `package alpha // used by its own test
116
-
117
- func Name() string { return "alpha" }
118
- `;
119
-
120
- export const ALPHA_REACHING = `package alpha // used by its own test
121
-
122
- import (
123
- "example.com/beta"
124
- )
125
-
126
- func Name() string { return "alpha" + beta.Suffix() }
127
- `;
128
-
129
- export const BETA = `package beta // used by its own test
130
-
131
- func Suffix() string { return "-beta" }
132
- `;
133
-
134
- /**
135
- * Writes a `module-boundaries.config.mjs` law at `root` with the given
136
- * `depConstraints` rows and optional `fitness` array.
137
- *
138
- * @param {string} root
139
- * @param {{rows?: string, fitness?: string}} [law]
140
- */
141
- export function writeLaw(root, { rows = "", fitness } = {}) {
142
- // used by its own test
143
- writeIn(
144
- root,
145
- "module-boundaries.config.mjs",
146
- `export const depConstraints = [\n${rows}\n];\n${OPTIONS}` +
147
- (fitness === undefined ? "" : `\nexport const fitness = ${fitness};\n`),
148
- );
149
- }
150
-
151
- /**
152
- * A single permitted layer rule (a may reach b). The same ONE_ROW the
153
- * evolution CLI integration fixtures use, so an allowed alpha→beta edge never
154
- * trips a boundary rule.
155
- */
156
- export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags: ["layer:b"] },`; // used by its own test
157
-
158
- /**
159
- * Writes `architecture-intent.json` at `root`. `sections` carries the top-level
160
- * keys directly (`version`, `boundaries`, `allowed`, `forbidden`,
161
- * `dependencies`, …); `version` defaults to "1".
162
- */
163
- export function writeIntent(root, sections) {
164
- // used by its own test
165
- writeIn(root, "architecture-intent.json", `${JSON.stringify(sections, null, 2)}\n`);
166
- }
167
-
168
- /**
169
- * Writes one ADR record under `docs/adr/`, the shape `adr-registry.mjs` reads.
170
- * `record` is the frontmatter map (`{id, status, supersedes?, bindings?}`).
171
- */
172
- export function writeAdr(root, filename, record) {
173
- // used by its own test
174
- const lines = ["---", `id: ${record.id}`, `status: ${record.status}`];
175
- if (record.supersedes?.length) {
176
- lines.push("supersedes:");
177
- for (const target of record.supersedes) lines.push(` - ${target}`);
178
- }
179
- if (record.bindings?.length) {
180
- lines.push("bindings:");
181
- for (const binding of record.bindings) lines.push(` - ${binding}`);
182
- }
183
- lines.push("---", "", `# ${record.id}`, "");
184
- writeIn(root, join("docs/adr", filename), `${lines.join("\n")}\n`);
185
- }
186
-
187
- /**
188
- * Drives the CLI in-process over `cwd`, capturing streams. Returns the exit
189
- * code and joined `out`/`err`. `runCli` is the real entry point
190
- * (`../cli.mjs`), never a shell-out to a binary named `archkeep`.
191
- */
192
- export async function runEvolution(cwd, argv) {
193
- // used by its own test
194
- const out = [];
195
- const err = [];
196
- const exitCode = await runCli(argv, {
197
- out: (text) => out.push(text),
198
- err: (text) => err.push(text),
199
- cwd,
200
- });
201
- return { exitCode, out: out.join("\n"), err: err.join("\n") };
202
- }
203
-
204
- /**
205
- * Invokes `evolution --base <base> [--head <head>] [--event-out <dir>] [--format json]`.
206
- *
207
- * @param {string} base The base revision (full SHA).
208
- * @param {{head?: string, eventOut?: string, format?: string}} [options]
209
- */
210
- export function evolutionArgs(base, { head, eventOut, format = "json" } = {}) {
211
- // used by its own test
212
- const args = ["evolution", "--base", base];
213
- if (head) args.push("--head", head);
214
- if (eventOut) args.push("--event-out", eventOut);
215
- if (format) args.push("--format", format);
216
- return args;
217
- }
218
-
219
- /**
220
- * Parses the `--format json` envelope out of a successful evolution run.
221
- */
222
- export function parseEnvelope(run) {
223
- // used by its own test
224
- if (run.exitCode !== EXIT.ok) throw new Error(`evolution exited ${run.exitCode}: ${run.err}`);
225
- return JSON.parse(run.out);
226
- }
227
-
228
- /** The parsed event files in `dir`, in filename order. */
229
- export function readEvents(dir) {
230
- return readdirSync(dir)
231
- .filter((name) => name.endsWith(".json") && !name.endsWith(".json.tmp"))
232
- .sort()
233
- .map((name) => JSON.parse(readFileSync(join(dir, name), "utf8")));
234
- }
235
-
236
- /** The event store's file names in `dir`, in filename order. */
237
- export function eventFiles(dir) {
238
- // used by its own test
239
- return readdirSync(dir)
240
- .filter((name) => name.endsWith(".json") && !name.endsWith(".json.tmp"))
241
- .sort();
242
- }
243
-
244
- /** Removes a throwaway workspace. */
245
- export function dispose(root) {
246
- // used by its own test
247
- rmSync(root, { recursive: true, force: true });
248
- }