@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.
- package/cli.mjs +4 -3
- package/package.json +2 -1
- package/src/architecture-intent/model.mjs +2 -2
- package/src/commands/README.md +7 -4
- package/src/commands/analyze-capability.mjs +2 -2
- package/src/commands/change-intent.mjs +2 -2
- package/src/commands/change.mjs +9 -5
- package/src/commands/check-capability.mjs +2 -2
- package/src/commands/check.mjs +3 -5
- package/src/commands/compare-capability.mjs +2 -2
- package/src/commands/completeness.mjs +2 -2
- package/src/commands/delta-snapshot.mjs +17 -0
- package/src/commands/delta.mjs +9 -5
- package/src/commands/diff.mjs +2 -2
- package/src/commands/evolution.mjs +10 -0
- package/src/commands/explain-capability.mjs +2 -2
- package/src/commands/fitness.mjs +2 -2
- package/src/commands/govern-capability.mjs +2 -2
- package/src/commands/inspect-capability.mjs +2 -2
- package/src/commands/rules-capability.mjs +2 -2
- package/src/commands/scenario-evaluation.mjs +97 -3
- package/src/corpus/goldens/adr.json +2 -2
- package/src/corpus/goldens/change.json +6 -5
- package/src/corpus/goldens/change.text +2 -2
- package/src/corpus/goldens/check.json +2 -2
- package/src/corpus/goldens/context.json +2 -2
- package/src/corpus/goldens/debt.json +4 -4
- package/src/corpus/goldens/debt.text +2 -2
- package/src/corpus/goldens/decisions.json +2 -2
- package/src/corpus/goldens/delta.json +4 -4
- package/src/corpus/goldens/delta.text +1 -1
- package/src/corpus/goldens/diff.json +4 -4
- package/src/corpus/goldens/diff.text +1 -1
- package/src/corpus/goldens/discover.json +2 -2
- package/src/corpus/goldens/drift.json +2 -2
- package/src/corpus/goldens/evolution.json +4 -2
- package/src/corpus/goldens/explain.json +2 -2
- package/src/corpus/goldens/fitness.json +2 -2
- package/src/corpus/goldens/graph.json +2 -2
- package/src/corpus/goldens/health.json +2 -2
- package/src/corpus/goldens/history.json +3 -3
- package/src/corpus/goldens/history.text +1 -1
- package/src/corpus/goldens/impact.json +2 -2
- package/src/corpus/goldens/provenance.json +2 -2
- package/src/corpus/goldens/reconcile.json +2 -2
- package/src/corpus/goldens/report.json +2 -2
- package/src/corpus/goldens/report.text +1 -1
- package/src/corpus/goldens/scenario.json +2 -2
- package/src/corpus/goldens/trajectory.json +3 -3
- package/src/corpus/goldens/trajectory.text +1 -1
- package/src/corpus/goldens/waivers.json +2 -2
- package/src/go-work.mjs +4 -5
- package/src/governance/debt-ledger.mjs +5 -2
- package/src/governance/evolution-event.mjs +1 -2
- package/src/governance/reconcile-score.mjs +59 -9
- package/src/intent/intent-manifest.json +8 -8
- package/src/providers/nx-static.mjs +4 -4
- package/src/report/text.mjs +1 -1
- package/src/rules/edge-constraints.mjs +4 -5
- package/src/rules/index.mjs +8 -10
- package/src/tsconfig-paths.mjs +4 -5
- 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`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
package/src/report/text.mjs
CHANGED
|
@@ -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}
|
|
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
|
|
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
|
|
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`).
|
|
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`).
|
package/src/rules/index.mjs
CHANGED
|
@@ -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 (
|
|
429
|
-
*
|
|
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
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* markdown fold is the
|
|
439
|
-
*
|
|
440
|
-
*
|
|
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 {
|
package/src/tsconfig-paths.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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`).
|
|
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
|
-
}
|