@ecoma-io/archkeep 0.24.0 → 0.24.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/package.json +1 -1
- package/src/analysis/csharp.mjs +3 -1
- package/src/analysis/dotnet/csproj.mjs +5 -1
- package/src/analysis/dotnet/namespaces.mjs +1 -0
- package/src/analysis/go.mjs +6 -0
- package/src/analysis/java.mjs +2 -0
- package/src/analysis/jvm/gradle.mjs +3 -1
- package/src/analysis/jvm/maven.mjs +6 -1
- package/src/analysis/jvm/packages.mjs +1 -0
- package/src/analysis/jvm/resolve.mjs +4 -2
- package/src/analysis/kotlin.mjs +2 -0
- package/src/analysis/markdown.mjs +1 -0
- package/src/analysis/python.mjs +8 -0
- package/src/analysis/rust.mjs +5 -1
- package/src/analysis/source-util.mjs +1 -1
- package/src/analysis/typescript.mjs +2 -0
- package/src/architecture-intent/model.mjs +11 -7
- package/src/architecture-intent/selectors.mjs +2 -1
- package/src/commands/change-intent.mjs +10 -9
- package/src/commands/change.mjs +17 -1
- package/src/commands/check.mjs +11 -9
- package/src/commands/completeness.mjs +7 -6
- package/src/commands/coverage-acceptance.mjs +46 -0
- package/src/commands/custom-rules.mjs +1 -0
- package/src/commands/delta-classify.mjs +3 -0
- package/src/commands/delta-snapshot.mjs +2 -1
- package/src/commands/delta.mjs +35 -21
- package/src/commands/drift.mjs +1 -1
- package/src/commands/evaluation-primitives.mjs +4 -4
- package/src/commands/evolution.mjs +2 -0
- package/src/commands/explain.mjs +2 -0
- package/src/commands/graph.mjs +39 -17
- package/src/commands/history.mjs +2 -0
- package/src/commands/plan-context-command.mjs +4 -1
- package/src/commands/policy.mjs +5 -2
- package/src/commands/scenario-evaluation.mjs +1 -1
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +1 -1
- package/src/custom-rules/host.mjs +3 -3
- package/src/eslint-config.mjs +1 -0
- package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
- package/src/go-work.mjs +1 -1
- package/src/governance/adr-registry.mjs +4 -1
- package/src/governance/debt-ledger.mjs +1 -1
- package/src/governance/decision-fitness.mjs +2 -0
- package/src/governance/decision-graph.mjs +1 -0
- package/src/governance/discovery-proposal.mjs +8 -2
- package/src/governance/evolution-event.mjs +42 -0
- package/src/governance/fitness-registry.mjs +2 -0
- package/src/governance/preset-fingerprints.json +14 -14
- package/src/governance/profile-registry.mjs +22 -3
- package/src/governance/provenance-record.mjs +4 -1
- package/src/governance/reconcile-score.mjs +4 -0
- package/src/governance/row-schema.mjs +1 -0
- package/src/governance/verdict.mjs +1 -0
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +3 -3
- package/src/intent/mask-non-code.mjs +1 -0
- package/src/lsp/diagnostics.mjs +3 -2
- package/src/lsp/protocol.mjs +2 -1
- package/src/lsp/server.mjs +3 -0
- package/src/lsp/workspace-index.mjs +3 -1
- package/src/providers/native/differential.fixtures.mjs +29 -11
- package/src/providers/native/index.mjs +2 -1
- package/src/providers/native/model.mjs +4 -0
- package/src/report/envelope-shape.mjs +2 -0
- package/src/report/sarif.mjs +21 -8
- package/src/report/snapshot-text.mjs +3 -3
- package/src/report/text.mjs +10 -2
- package/src/rules/match.mjs +7 -5
- package/src/rules/specifiers.mjs +2 -0
- package/src/rules/tags.mjs +3 -2
- package/src/rules/topology.mjs +6 -1
- package/src/workspace.mjs +1 -0
|
@@ -36,6 +36,7 @@ const IDENTITY = ["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpg
|
|
|
36
36
|
* than blocking the worker thread forever.
|
|
37
37
|
*/
|
|
38
38
|
export function git(cwd, ...args) {
|
|
39
|
+
// used by its own test
|
|
39
40
|
return execFileSync("git", args, {
|
|
40
41
|
cwd,
|
|
41
42
|
env: environmentForTree(),
|
|
@@ -47,12 +48,14 @@ export function git(cwd, ...args) {
|
|
|
47
48
|
|
|
48
49
|
/** Writes `text` to `root/relativePath`, creating parent directories. */
|
|
49
50
|
export function writeIn(root, relativePath, text) {
|
|
51
|
+
// used by its own test
|
|
50
52
|
mkdirSync(join(root, relativePath, ".."), { recursive: true });
|
|
51
53
|
writeFileSync(join(root, relativePath), text);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** Stages every change and commits with the fixture identity; returns the SHA. */
|
|
55
57
|
export function commit(root, message) {
|
|
58
|
+
// used by its own test
|
|
56
59
|
git(root, ...IDENTITY, "add", "-A");
|
|
57
60
|
git(root, ...IDENTITY, "commit", "-q", "-m", message);
|
|
58
61
|
return git(root, "rev-parse", "HEAD").trim();
|
|
@@ -109,12 +112,12 @@ const OPTIONS = `export const moduleBoundaryOptions = {
|
|
|
109
112
|
};
|
|
110
113
|
`;
|
|
111
114
|
|
|
112
|
-
export const ALPHA_CLEAN = `package alpha
|
|
115
|
+
export const ALPHA_CLEAN = `package alpha // used by its own test
|
|
113
116
|
|
|
114
117
|
func Name() string { return "alpha" }
|
|
115
118
|
`;
|
|
116
119
|
|
|
117
|
-
export const ALPHA_REACHING = `package alpha
|
|
120
|
+
export const ALPHA_REACHING = `package alpha // used by its own test
|
|
118
121
|
|
|
119
122
|
import (
|
|
120
123
|
"example.com/beta"
|
|
@@ -123,7 +126,7 @@ import (
|
|
|
123
126
|
func Name() string { return "alpha" + beta.Suffix() }
|
|
124
127
|
`;
|
|
125
128
|
|
|
126
|
-
export const BETA = `package beta
|
|
129
|
+
export const BETA = `package beta // used by its own test
|
|
127
130
|
|
|
128
131
|
func Suffix() string { return "-beta" }
|
|
129
132
|
`;
|
|
@@ -136,6 +139,7 @@ func Suffix() string { return "-beta" }
|
|
|
136
139
|
* @param {{rows?: string, fitness?: string}} [law]
|
|
137
140
|
*/
|
|
138
141
|
export function writeLaw(root, { rows = "", fitness } = {}) {
|
|
142
|
+
// used by its own test
|
|
139
143
|
writeIn(
|
|
140
144
|
root,
|
|
141
145
|
"module-boundaries.config.mjs",
|
|
@@ -149,7 +153,7 @@ export function writeLaw(root, { rows = "", fitness } = {}) {
|
|
|
149
153
|
* evolution CLI integration fixtures use, so an allowed alpha→beta edge never
|
|
150
154
|
* trips a boundary rule.
|
|
151
155
|
*/
|
|
152
|
-
export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags: ["layer:b"] },`;
|
|
156
|
+
export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags: ["layer:b"] },`; // used by its own test
|
|
153
157
|
|
|
154
158
|
/**
|
|
155
159
|
* Writes `architecture-intent.json` at `root`. `sections` carries the top-level
|
|
@@ -157,6 +161,7 @@ export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags:
|
|
|
157
161
|
* `dependencies`, …); `version` defaults to "1".
|
|
158
162
|
*/
|
|
159
163
|
export function writeIntent(root, sections) {
|
|
164
|
+
// used by its own test
|
|
160
165
|
writeIn(root, "architecture-intent.json", `${JSON.stringify(sections, null, 2)}\n`);
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -165,6 +170,7 @@ export function writeIntent(root, sections) {
|
|
|
165
170
|
* `record` is the frontmatter map (`{id, status, supersedes?, bindings?}`).
|
|
166
171
|
*/
|
|
167
172
|
export function writeAdr(root, filename, record) {
|
|
173
|
+
// used by its own test
|
|
168
174
|
const lines = ["---", `id: ${record.id}`, `status: ${record.status}`];
|
|
169
175
|
if (record.supersedes?.length) {
|
|
170
176
|
lines.push("supersedes:");
|
|
@@ -184,6 +190,7 @@ export function writeAdr(root, filename, record) {
|
|
|
184
190
|
* (`../cli.mjs`), never a shell-out to a binary named `archkeep`.
|
|
185
191
|
*/
|
|
186
192
|
export async function runEvolution(cwd, argv) {
|
|
193
|
+
// used by its own test
|
|
187
194
|
const out = [];
|
|
188
195
|
const err = [];
|
|
189
196
|
const exitCode = await runCli(argv, {
|
|
@@ -201,6 +208,7 @@ export async function runEvolution(cwd, argv) {
|
|
|
201
208
|
* @param {{head?: string, eventOut?: string, format?: string}} [options]
|
|
202
209
|
*/
|
|
203
210
|
export function evolutionArgs(base, { head, eventOut, format = "json" } = {}) {
|
|
211
|
+
// used by its own test
|
|
204
212
|
const args = ["evolution", "--base", base];
|
|
205
213
|
if (head) args.push("--head", head);
|
|
206
214
|
if (eventOut) args.push("--event-out", eventOut);
|
|
@@ -212,6 +220,7 @@ export function evolutionArgs(base, { head, eventOut, format = "json" } = {}) {
|
|
|
212
220
|
* Parses the `--format json` envelope out of a successful evolution run.
|
|
213
221
|
*/
|
|
214
222
|
export function parseEnvelope(run) {
|
|
223
|
+
// used by its own test
|
|
215
224
|
if (run.exitCode !== EXIT.ok) throw new Error(`evolution exited ${run.exitCode}: ${run.err}`);
|
|
216
225
|
return JSON.parse(run.out);
|
|
217
226
|
}
|
|
@@ -226,6 +235,7 @@ export function readEvents(dir) {
|
|
|
226
235
|
|
|
227
236
|
/** The event store's file names in `dir`, in filename order. */
|
|
228
237
|
export function eventFiles(dir) {
|
|
238
|
+
// used by its own test
|
|
229
239
|
return readdirSync(dir)
|
|
230
240
|
.filter((name) => name.endsWith(".json") && !name.endsWith(".json.tmp"))
|
|
231
241
|
.sort();
|
|
@@ -233,5 +243,6 @@ export function eventFiles(dir) {
|
|
|
233
243
|
|
|
234
244
|
/** Removes a throwaway workspace. */
|
|
235
245
|
export function dispose(root) {
|
|
246
|
+
// used by its own test
|
|
236
247
|
rmSync(root, { recursive: true, force: true });
|
|
237
248
|
}
|
package/src/go-work.mjs
CHANGED
|
@@ -113,7 +113,7 @@ const IDENTIFIER_BOUNDARY = new Set([" ", "\t", "\r", '"', "`", "(", ")"]);
|
|
|
113
113
|
* @throws {Error} on an unterminated string — a string left open would
|
|
114
114
|
* otherwise swallow the rest of the line silently.
|
|
115
115
|
*/
|
|
116
|
-
|
|
116
|
+
function tokenizeGoWorkLine(line, lineNumber) {
|
|
117
117
|
const tokens = [];
|
|
118
118
|
let at = 0;
|
|
119
119
|
while (at < line.length) {
|
|
@@ -124,7 +124,7 @@ export function hasAuthority(status) {
|
|
|
124
124
|
* format outgrows 999 records without breaking; the slug is dash-separated
|
|
125
125
|
* lowercase words.
|
|
126
126
|
*/
|
|
127
|
-
|
|
127
|
+
const ADR_FILE_PATTERN = /^(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/u;
|
|
128
128
|
|
|
129
129
|
/** An ADR id — `NNN-slug` — must match the filename it lives in. */
|
|
130
130
|
export const ADR_ID_PATTERN = /^\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
@@ -236,6 +236,7 @@ function parseProseFields(body) {
|
|
|
236
236
|
* that already appears earlier in the same block.
|
|
237
237
|
*/
|
|
238
238
|
export function parseFrontmatterFields(text, at) {
|
|
239
|
+
// used by its own test
|
|
239
240
|
/** @type {Record<string, string|string[]|undefined>} */
|
|
240
241
|
const fields = {};
|
|
241
242
|
let currentList = null;
|
|
@@ -307,6 +308,7 @@ function toList(value) {
|
|
|
307
308
|
* @throws {Error} naming every violation at once.
|
|
308
309
|
*/
|
|
309
310
|
export function validateRecord({ id, frontmatter, body = "" }) {
|
|
311
|
+
// used by its own test
|
|
310
312
|
const fields = frontmatter === null ? {} : parseFrontmatterFields(frontmatter, id);
|
|
311
313
|
const violations = [];
|
|
312
314
|
|
|
@@ -400,6 +402,7 @@ export function validateRecord({ id, frontmatter, body = "" }) {
|
|
|
400
402
|
* @throws {Error} naming every lineage violation at once.
|
|
401
403
|
*/
|
|
402
404
|
export function validateLineage(records) {
|
|
405
|
+
// used by its own test
|
|
403
406
|
const byId = new Map(records.map((record) => [record.id, record]));
|
|
404
407
|
/** @type {Map<string, string[]>} */
|
|
405
408
|
const supersededBy = new Map(records.map((record) => [record.id, []]));
|
|
@@ -143,7 +143,7 @@ function owningProjectForPath(path, byName) {
|
|
|
143
143
|
* @param {string} source The entry's `source` (its keying field).
|
|
144
144
|
* @returns {string} The stable hex id.
|
|
145
145
|
*/
|
|
146
|
-
|
|
146
|
+
function entryId(kind, source) {
|
|
147
147
|
const semanticKind = kind === "expired-waiver" ? "waiver" : kind;
|
|
148
148
|
return createHash("sha256")
|
|
149
149
|
.update(canonicalizeJson({ kind: semanticKind, source }))
|
|
@@ -61,6 +61,7 @@ import { hasAuthority } from "./adr-registry.mjs";
|
|
|
61
61
|
|
|
62
62
|
/** The closed set of per-decision fitness levels `computeDecisionFitness` emits. */
|
|
63
63
|
export const DECISION_FITNESS_LEVELS = Object.freeze([
|
|
64
|
+
// used by its own test
|
|
64
65
|
"enforced",
|
|
65
66
|
"partially-enforced",
|
|
66
67
|
"violated",
|
|
@@ -70,6 +71,7 @@ export const DECISION_FITNESS_LEVELS = Object.freeze([
|
|
|
70
71
|
|
|
71
72
|
/** Whether a level names a red (never-healthy) direction. */
|
|
72
73
|
export function isRedDirection(level) {
|
|
74
|
+
// used by its own test
|
|
73
75
|
return level === "violated" || level === "unverifiable";
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -382,6 +382,7 @@ export function forwardDecision(decisionId, ctx) {
|
|
|
382
382
|
* @returns {GraphWalk}
|
|
383
383
|
*/
|
|
384
384
|
export function reverseRow(rowRef, ctx) {
|
|
385
|
+
// used by its own test
|
|
385
386
|
const g = newWalk();
|
|
386
387
|
|
|
387
388
|
const row = ctx.rows.find((candidate) => candidate.id === rowRef);
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
* The uncertainty marker vocabulary — three values, the bound the "bounded
|
|
62
62
|
* uncertainty markers" test asserts.
|
|
63
63
|
*/
|
|
64
|
-
export const CONFIDENCE = Object.freeze(["high", "medium", "low"]);
|
|
64
|
+
export const CONFIDENCE = Object.freeze(["high", "medium", "low"]); // used by its own test
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* The component model: every project's root's first path segment (`""` at the
|
|
@@ -73,6 +73,7 @@ export const CONFIDENCE = Object.freeze(["high", "medium", "low"]);
|
|
|
73
73
|
* @returns {Map<string, {name: string, root: string, tags: string[]}[]>}
|
|
74
74
|
*/
|
|
75
75
|
export function componentsByDirectory(projects) {
|
|
76
|
+
// used by its own test
|
|
76
77
|
const buckets = new Map();
|
|
77
78
|
for (const project of projects) {
|
|
78
79
|
const component = project.root === "" ? "" : project.root.split("/")[0];
|
|
@@ -101,6 +102,7 @@ export function componentsByDirectory(projects) {
|
|
|
101
102
|
* @returns {{tag: string, component: string, members: string[]}[]} Sorted.
|
|
102
103
|
*/
|
|
103
104
|
export function dominantTags(components) {
|
|
105
|
+
// used by its own test
|
|
104
106
|
const tags = [];
|
|
105
107
|
for (const [component, members] of components) {
|
|
106
108
|
if (members.length < 2) continue;
|
|
@@ -136,6 +138,7 @@ export function dominantTags(components) {
|
|
|
136
138
|
* @returns {{axis: string, values: string[]}[]} Sorted by axis.
|
|
137
139
|
*/
|
|
138
140
|
export function tagAxes(projects) {
|
|
141
|
+
// used by its own test
|
|
139
142
|
const byAxis = new Map();
|
|
140
143
|
for (const project of projects) {
|
|
141
144
|
for (const tag of project.tags) {
|
|
@@ -169,7 +172,7 @@ export function tagAxes(projects) {
|
|
|
169
172
|
* @param {string} target
|
|
170
173
|
* @returns {boolean}
|
|
171
174
|
*/
|
|
172
|
-
|
|
175
|
+
function sameComponent(components, source, target) {
|
|
173
176
|
for (const members of components.values()) {
|
|
174
177
|
if (members.some((m) => m.name === source) && members.some((m) => m.name === target)) {
|
|
175
178
|
return true;
|
|
@@ -198,6 +201,7 @@ export function sameComponent(components, source, target) {
|
|
|
198
201
|
* component?: string, evidence: object[], confidence: string}[]} Sorted.
|
|
199
202
|
*/
|
|
200
203
|
export function boundaryAssertions({ projects, edges }) {
|
|
204
|
+
// used by its own test
|
|
201
205
|
const components = componentsByDirectory(projects);
|
|
202
206
|
const projectNames = new Set(projects.map((p) => p.name));
|
|
203
207
|
/** @type {{kind: "edge"|"component", source: string|undefined, target: string|undefined,
|
|
@@ -256,6 +260,7 @@ export function boundaryAssertions({ projects, edges }) {
|
|
|
256
260
|
* evidence: object[], confidence: string}[]} Sorted.
|
|
257
261
|
*/
|
|
258
262
|
export function tagVocabulary(projects) {
|
|
263
|
+
// used by its own test
|
|
259
264
|
const components = componentsByDirectory(projects);
|
|
260
265
|
/** @type {{kind: "observed"|"suggested", tag: string|undefined, axis: string|undefined,
|
|
261
266
|
* component: string|undefined, members: string[]|undefined, values: string[]|undefined,
|
|
@@ -308,6 +313,7 @@ export function tagVocabulary(projects) {
|
|
|
308
313
|
* component?: string, evidence: object[], confidence: string}[]} Sorted.
|
|
309
314
|
*/
|
|
310
315
|
export function candidateRules(assertions) {
|
|
316
|
+
// used by its own test
|
|
311
317
|
/** @type {{kind: "noDependency"|"boundary", source: string|undefined, target: string|undefined,
|
|
312
318
|
* component: string|undefined, evidence: object[], confidence: "medium"}[]} */
|
|
313
319
|
const rules = [];
|
|
@@ -80,6 +80,48 @@ export function eventId(event) {
|
|
|
80
80
|
return createHash("sha256").update(eventDedupeKey(event)).digest("hex");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The refusal law every command that writes an evolution event holds: the
|
|
85
|
+
* write happens only from a reproducible identity — a committed, clean head
|
|
86
|
+
* and a clean base. A commitless head has no revision to name (the event
|
|
87
|
+
* would serialize its `head` as `{}`, and every run over that workspace
|
|
88
|
+
* would collide on one event id); a dirty tree names a commit its evidence
|
|
89
|
+
* does not back, so two distinct uncommitted states collapse onto one event
|
|
90
|
+
* id — a later transition is silently lost or aliased. That is the silent
|
|
91
|
+
* direction this repository refuses, so the write is refused loudly instead.
|
|
92
|
+
* The same run without `--event-out` stays a byte-identical in-memory run —
|
|
93
|
+
* the gate is the event write, never the verdict.
|
|
94
|
+
*
|
|
95
|
+
* The messages are the `delta` command's original wording, parameterized by
|
|
96
|
+
* the writing command's label. Consumers match on these strings (a refusal
|
|
97
|
+
* is part of a run's observable contract), so the wording is frozen here —
|
|
98
|
+
* one home, one copy, no per-command drift.
|
|
99
|
+
*
|
|
100
|
+
* @param {{label: "delta"|"change", headCommit: string|undefined,
|
|
101
|
+
* baseDirty: boolean, headDirty: boolean}} input `label` names the writing
|
|
102
|
+
* command in the refusal message; `headCommit` is the head revision the
|
|
103
|
+
* event would carry (`undefined` when provenance could not resolve one);
|
|
104
|
+
* `baseDirty`/`headDirty` are the two sides' provenance dirty bits.
|
|
105
|
+
* @returns {void} Throws on every state that cannot produce a reproducible
|
|
106
|
+
* event identity.
|
|
107
|
+
*/
|
|
108
|
+
export function assertReproducibleEventIdentity({ label, headCommit, baseDirty, headDirty }) {
|
|
109
|
+
if (typeof headCommit !== "string") {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`archkeep: refusing to write a ${label} event without a committed head — a commitless ` +
|
|
112
|
+
"head has no reproducible event identity, and every distinct head state would " +
|
|
113
|
+
"collide on one event id. Commit the head, or capture without --event-out.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (baseDirty === true || headDirty === true) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`archkeep: refusing to write a ${label} event from a dirty working tree — the event ` +
|
|
119
|
+
"would name a commit whose evidence is uncommitted, and distinct uncommitted " +
|
|
120
|
+
"states would collide on one event id. Commit both sides first.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
/**
|
|
84
126
|
* The digest of a normalized change-intent's DECLARATIVE parts only:
|
|
85
127
|
* `{version, base, projects, edges, constraints}`. The prose `summary` is
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
|
|
66
66
|
/** The condition types the registry can evaluate. */
|
|
67
67
|
export const CONDITION_TYPES = Object.freeze([
|
|
68
|
+
// used by its own test
|
|
68
69
|
"cycle-free",
|
|
69
70
|
"layer-dependency",
|
|
70
71
|
"tag-conformance",
|
|
@@ -327,6 +328,7 @@ function tagAxisIsolationViolations(condition, at) {
|
|
|
327
328
|
* @returns {object} A verdict record from `fitnessVerdict`.
|
|
328
329
|
*/
|
|
329
330
|
export function judgeFitnessRow(row, graph, analysis, intent, suppressions) {
|
|
331
|
+
// used by its own test
|
|
330
332
|
const names = resolveMembers(row.match, graph.nodes);
|
|
331
333
|
if (names.length === 0) {
|
|
332
334
|
return fitnessVerdict({
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"clean-architecture#clean-architecture": "
|
|
3
|
-
"clean-architecture#clean-architecture-pure-core": "
|
|
4
|
-
"ddd-bounded-contexts#ddd-bounded-contexts": "
|
|
5
|
-
"ddd-bounded-contexts#ddd-bounded-contexts-isolated": "
|
|
6
|
-
"ddd-bounded-contexts#ddd-bounded-contexts-partitioned": "
|
|
7
|
-
"hexagonal#hexagonal": "
|
|
8
|
-
"hexagonal#hexagonal-pure-domain": "
|
|
9
|
-
"layered#layered-relaxed": "
|
|
10
|
-
"layered#layered-strict": "
|
|
11
|
-
"modular-monolith#modular-monolith": "
|
|
12
|
-
"modular-monolith#modular-monolith-sealed-kernel": "
|
|
13
|
-
"modular-monolith#modular-monolith-sealed-modules": "
|
|
14
|
-
"vertical-slice#vertical-slice": "
|
|
15
|
-
"vertical-slice#vertical-slice-sealed-kernel": "
|
|
2
|
+
"clean-architecture#clean-architecture": "5f32ab7e98b5e49ed832bbb4dd97d6757abdf607ca85dbe70a3fc9515493fcb4",
|
|
3
|
+
"clean-architecture#clean-architecture-pure-core": "9728201a3f57fc7db38747e58cd277fdff54277a34d3997b6ae67827dc547404",
|
|
4
|
+
"ddd-bounded-contexts#ddd-bounded-contexts": "6d9f733c915e3ab33932d5150d0651a4603a7869625a7ec55afc7bb3a0328871",
|
|
5
|
+
"ddd-bounded-contexts#ddd-bounded-contexts-isolated": "6dd7e4360b8d8fb0359148af1360bc8986526b2be51c6b7a647f7fb414e5a0a3",
|
|
6
|
+
"ddd-bounded-contexts#ddd-bounded-contexts-partitioned": "93b15523dda77be5aca06806d0235eefa32a504e411d4fcf6962a7c1bec9e719",
|
|
7
|
+
"hexagonal#hexagonal": "86321ee12d3e304567c988a5fb04ef17d040763c45f021a9b1f64c3852ea7c12",
|
|
8
|
+
"hexagonal#hexagonal-pure-domain": "2aa8efe7d71a4c731aee61c46961d6bba8d239af68909a71595488374c5de80a",
|
|
9
|
+
"layered#layered-relaxed": "ffc0f268d37b88c97eb8c783283adbf9779850750b3e21c780c7851d936fb6b4",
|
|
10
|
+
"layered#layered-strict": "4c90afb7cfe1e273fedce2a85e2403a1f354a4a038f1c872ad103f1c2f1d7df1",
|
|
11
|
+
"modular-monolith#modular-monolith": "634ef3e706d06780bfd72a57c1a4a3c0be2d4163b0728fc734f4bd4e5da8cbe1",
|
|
12
|
+
"modular-monolith#modular-monolith-sealed-kernel": "372cabc2d618aea8a0aa8b284e8be93a062dd5ebe22f64af52bc1fa785826a97",
|
|
13
|
+
"modular-monolith#modular-monolith-sealed-modules": "0ff0b39328d50fb3622dc84fb4d7b26fd567a5110437536aaa586e29ad4b8c92",
|
|
14
|
+
"vertical-slice#vertical-slice": "4df057e4ec1f92dae8aada8203d18869c6e283764f66a9f96f1c425304436f34",
|
|
15
|
+
"vertical-slice#vertical-slice-sealed-kernel": "1d1fbaf268a7afc851e6e4a126bd586dd0ca9547cf9d654e44dcf0dd99537f78"
|
|
16
16
|
}
|
|
@@ -74,7 +74,7 @@ import { describe, isPlainObject } from "../values.mjs";
|
|
|
74
74
|
const REGISTRY_KEYS = ["profiles", "version", "$schema"];
|
|
75
75
|
|
|
76
76
|
/** A version a reader that predates it must refuse, per `docs/reference/profiles.md`. */
|
|
77
|
-
|
|
77
|
+
const PROFILE_REGISTRY_SCHEMA_VERSION = 1;
|
|
78
78
|
|
|
79
79
|
/** The registry's schema version: stated, or schema 1 when absent. */
|
|
80
80
|
function registrySchemaVersion(raw) {
|
|
@@ -112,6 +112,7 @@ const NAME_PATTERN = /^[a-zA-Z0-9_-]+$/u;
|
|
|
112
112
|
|
|
113
113
|
/** A profile's declared block, kept ONLY for this command's own data. */
|
|
114
114
|
export function listNames(registry) {
|
|
115
|
+
// used by its own test
|
|
115
116
|
return registry.profiles.map((profile) => profile.name);
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -126,6 +127,7 @@ export function listNames(registry) {
|
|
|
126
127
|
* @returns {string[]}
|
|
127
128
|
*/
|
|
128
129
|
export function profileRegistryViolations(raw) {
|
|
130
|
+
// used by its own test
|
|
129
131
|
if (!isPlainObject(raw)) {
|
|
130
132
|
return [`profiles: expected a JSON object, got ${describe(raw)}`];
|
|
131
133
|
}
|
|
@@ -201,6 +203,7 @@ export function profileRegistryViolations(raw) {
|
|
|
201
203
|
* @returns {string[]}
|
|
202
204
|
*/
|
|
203
205
|
export function profileReferenceViolations(profiles) {
|
|
206
|
+
// used by its own test
|
|
204
207
|
const violations = [];
|
|
205
208
|
const byName = new Map(profiles.map((profile) => [profile.name, profile]));
|
|
206
209
|
for (const profile of profiles) {
|
|
@@ -250,6 +253,7 @@ export function profileReferenceViolations(profiles) {
|
|
|
250
253
|
* silently resolved as "no profile".
|
|
251
254
|
*/
|
|
252
255
|
export function resolveProfile(profiles, name, seen = new Set()) {
|
|
256
|
+
// used by its own test
|
|
253
257
|
const profile = profiles.find((candidate) => candidate.name === name);
|
|
254
258
|
if (profile === undefined) {
|
|
255
259
|
throw new Error(
|
|
@@ -302,6 +306,7 @@ export function resolveProfile(profiles, name, seen = new Set()) {
|
|
|
302
306
|
* profile-registry or reference-graph defect.
|
|
303
307
|
*/
|
|
304
308
|
export function loadProfileRegistry(path, { readFile = defaultProfileIo.readFile } = {}) {
|
|
309
|
+
// used by its own test
|
|
305
310
|
const text = readFile(path);
|
|
306
311
|
if (text === null) {
|
|
307
312
|
throw new Error(`archkeep: cannot read profiles file ${path}`);
|
|
@@ -336,13 +341,27 @@ export function loadProfileRegistry(path, { readFile = defaultProfileIo.readFile
|
|
|
336
341
|
* @param {string} profileName The profile to resolve.
|
|
337
342
|
* @param {string} sourceLabel What failed, named in the thrown message.
|
|
338
343
|
* @param {{readFile?: (path: string) => string|null}} [io]
|
|
339
|
-
* @returns {{depConstraints: object[], options: object, suppressions: object[], fitness?: object[]
|
|
344
|
+
* @returns {{depConstraints: object[], options: object, suppressions: object[], fitness?: object[],
|
|
345
|
+
* profile: string}} `profile` is the selection the policy was resolved by,
|
|
346
|
+
* carried on the policy itself so the law's identity travels with it —
|
|
347
|
+
* `../commands/graph.mjs`'s `computePolicyFingerprint` reads it, and the
|
|
348
|
+
* fingerprint is the one policy fact that travels between captures
|
|
349
|
+
* (`delta`, `diff`, `history`).
|
|
340
350
|
* @throws {Error} when the registry or the named profile is defective.
|
|
341
351
|
*/
|
|
342
352
|
export function profilePolicy(registryPath, profileName, sourceLabel, io = {}) {
|
|
343
353
|
const registry = loadProfileRegistry(registryPath, io);
|
|
344
354
|
const effective = resolveProfile(registry.profiles, profileName);
|
|
345
|
-
return
|
|
355
|
+
return {
|
|
356
|
+
...policyFrom(effective, `${sourceLabel} (profile "${profileName}")`),
|
|
357
|
+
// The selection rides on the policy it produced: two profiles whose
|
|
358
|
+
// blocks converge resolve to the same fields, so without this key a
|
|
359
|
+
// switch between them resolves to the same fingerprint and a `delta`
|
|
360
|
+
// across the switch classifies as no law change. The NAME, never
|
|
361
|
+
// `registryPath` — a path is machine-local, and a fingerprint that
|
|
362
|
+
// differs between a laptop and CI reports a change nobody made.
|
|
363
|
+
profile: profileName,
|
|
364
|
+
};
|
|
346
365
|
}
|
|
347
366
|
|
|
348
367
|
/**
|
|
@@ -52,7 +52,7 @@ import { clockViolations } from "./clock.mjs";
|
|
|
52
52
|
import { describe, isPlainObject } from "../values.mjs";
|
|
53
53
|
|
|
54
54
|
/** The only keys a validated `origin` may carry. */
|
|
55
|
-
export const ORIGIN_KEYS = Object.freeze(["by", "tool", "on"]);
|
|
55
|
+
export const ORIGIN_KEYS = Object.freeze(["by", "tool", "on"]); // used by its own test
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* @typedef {object} OriginRecord
|
|
@@ -135,6 +135,7 @@ export function originViolations(raw, io = {}) {
|
|
|
135
135
|
* @throws {Error} naming every violation at once, prefixed by `at`.
|
|
136
136
|
*/
|
|
137
137
|
export function validateOrigin(raw, io = {}, at = "origin") {
|
|
138
|
+
// used by its own test
|
|
138
139
|
const violations = originViolations(raw, io).map((message) =>
|
|
139
140
|
message.startsWith("origin.")
|
|
140
141
|
? `${at}.${message.slice("origin.".length)}`
|
|
@@ -181,6 +182,7 @@ export function recordOrigin({ by, tool, clock }) {
|
|
|
181
182
|
* change, supersession, and bindings change is recorded as one of these.
|
|
182
183
|
*/
|
|
183
184
|
export const DECISION_LIFECYCLE_KINDS = Object.freeze([
|
|
185
|
+
// used by its own test
|
|
184
186
|
"status-transition",
|
|
185
187
|
"supersession",
|
|
186
188
|
"bindings-change",
|
|
@@ -234,6 +236,7 @@ export const DECISION_LIFECYCLE_KINDS = Object.freeze([
|
|
|
234
236
|
* the registry's `ADR_STATUSES`, a no-op event, or an invalid origin/clock.
|
|
235
237
|
*/
|
|
236
238
|
export function recordDecisionLifecycle({
|
|
239
|
+
// used by its own test
|
|
237
240
|
kind,
|
|
238
241
|
decisionId,
|
|
239
242
|
from = null,
|
|
@@ -70,6 +70,7 @@ function boundaryKey(from, to) {
|
|
|
70
70
|
|
|
71
71
|
/** The severity a state earns — the sort key a ranked proposal list uses. */
|
|
72
72
|
export const SEVERITY_ORDER = Object.freeze({
|
|
73
|
+
// used by its own test
|
|
73
74
|
unexpected: 4,
|
|
74
75
|
absent: 3,
|
|
75
76
|
match: 0,
|
|
@@ -133,6 +134,7 @@ function intentKeys(intent) {
|
|
|
133
134
|
* @returns {{project: ScoredElement, tags: ScoredElement[]}}
|
|
134
135
|
*/
|
|
135
136
|
export function scoreProject(project, keys, requiredTagsByProject) {
|
|
137
|
+
// used by its own test
|
|
136
138
|
const tags = project.data?.tags ?? project.tags ?? [];
|
|
137
139
|
const requiredTags = requiredTagsByProject.get(project.name) ?? [];
|
|
138
140
|
const element = { plane: "project", name: project.name };
|
|
@@ -209,6 +211,7 @@ export function scoreProject(project, keys, requiredTagsByProject) {
|
|
|
209
211
|
* @returns {ScoredElement}
|
|
210
212
|
*/
|
|
211
213
|
export function scoreEdge(edge, keys, intentForbiddenPairs, tagForbiddenPairs) {
|
|
214
|
+
// used by its own test
|
|
212
215
|
const key = `${edge.source} → ${edge.target}`;
|
|
213
216
|
const element = { plane: "edge", name: key, intentRow: null };
|
|
214
217
|
|
|
@@ -279,6 +282,7 @@ export function scoreEdge(edge, keys, intentForbiddenPairs, tagForbiddenPairs) {
|
|
|
279
282
|
* @returns {ScoredElement[]}
|
|
280
283
|
*/
|
|
281
284
|
export function scoreIntentRows(intent, judgeVerdict, observed, tagsByProject) {
|
|
285
|
+
// used by its own test
|
|
282
286
|
const rows = [];
|
|
283
287
|
const observedNames = new Set(observed.projects.map((p) => p.name));
|
|
284
288
|
const observedEdgeKeys = new Set(observed.edges.map((e) => `${e.source} → ${e.target}`));
|
|
@@ -48,6 +48,7 @@ export const VERDICTS = Object.freeze(["pass", "fail", "unknown", "not_applicabl
|
|
|
48
48
|
|
|
49
49
|
/** The single mapping from an envelope status to a verdict. */
|
|
50
50
|
export const VERDICT_FOR_STATUS = Object.freeze({
|
|
51
|
+
// used by its own test
|
|
51
52
|
ok: "pass",
|
|
52
53
|
findings: "fail",
|
|
53
54
|
"no-verdict": "unknown",
|
|
@@ -104,13 +104,13 @@
|
|
|
104
104
|
"type": "source-evidence",
|
|
105
105
|
"path": "src/commands/graph.mjs",
|
|
106
106
|
"assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped; SCHEMA_VERSION = 2",
|
|
107
|
-
"sha256": "
|
|
107
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"type": "source-evidence",
|
|
111
111
|
"path": "src/commands/graph.mjs",
|
|
112
112
|
"assertion": "computePolicyFingerprint produces SHA-256 of canonicalized policy",
|
|
113
|
-
"sha256": "
|
|
113
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
114
114
|
}
|
|
115
115
|
],
|
|
116
116
|
"status": "proven"
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"type": "source-evidence",
|
|
287
287
|
"path": "src/commands/graph.mjs",
|
|
288
288
|
"assertion": "Plain string comparison throughout; never localeCompare",
|
|
289
|
-
"sha256": "
|
|
289
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
290
290
|
}
|
|
291
291
|
],
|
|
292
292
|
"status": "proven"
|
|
@@ -536,6 +536,7 @@ function scanTemplate(src, i) {
|
|
|
536
536
|
* same line structure.
|
|
537
537
|
*/
|
|
538
538
|
export function maskNonCode(src) {
|
|
539
|
+
// used by its own test
|
|
539
540
|
/**
|
|
540
541
|
* Chunks of the result, joined once at the end. Code is copied through in
|
|
541
542
|
* RUNS rather than a token at a time — `plainFrom` is where the current
|
package/src/lsp/diagnostics.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { DIAGNOSTIC_SEVERITY, SERVER_INFO } from "./protocol.mjs";
|
|
|
38
38
|
* prints beside the message to say which tool spoke. Taken from the server's
|
|
39
39
|
* own identity so the two can never disagree.
|
|
40
40
|
*/
|
|
41
|
-
export const DIAGNOSTIC_SOURCE = SERVER_INFO.name;
|
|
41
|
+
export const DIAGNOSTIC_SOURCE = SERVER_INFO.name; // used by its own test
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The `code` on a diagnostic that reports the ABSENCE of a verdict rather than
|
|
@@ -46,7 +46,7 @@ export const DIAGNOSTIC_SOURCE = SERVER_INFO.name;
|
|
|
46
46
|
* against `MESSAGE_IDS` at load so a future upstream id cannot silently collide
|
|
47
47
|
* with it.
|
|
48
48
|
*/
|
|
49
|
-
export const ANALYSIS_FAILURE_CODE = "analysisFailure";
|
|
49
|
+
export const ANALYSIS_FAILURE_CODE = "analysisFailure"; // used by its own test
|
|
50
50
|
|
|
51
51
|
if (MESSAGE_IDS.includes(ANALYSIS_FAILURE_CODE)) {
|
|
52
52
|
throw new Error(
|
|
@@ -100,6 +100,7 @@ const QUOTES = new Set(['"', "'", "`"]);
|
|
|
100
100
|
* @returns {{start: {line: number, character: number}, end: {line: number, character: number}}}
|
|
101
101
|
*/
|
|
102
102
|
export function rangeAt(at, lines) {
|
|
103
|
+
// used by its own test
|
|
103
104
|
// A failure about the file as a whole carries no position (`contract.md`
|
|
104
105
|
// fixes it as an explicit `null`). It gets the first line, whole: a
|
|
105
106
|
// zero-width range at the origin renders as an invisible caret in most
|
package/src/lsp/protocol.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export const DIAGNOSTIC_SEVERITY = Object.freeze({
|
|
|
95
95
|
* more is not a supported conversation, and saying so beats waiting for bytes
|
|
96
96
|
* that will never arrive.
|
|
97
97
|
*/
|
|
98
|
-
export const MAX_CONTENT_LENGTH = 64 * 1024 * 1024;
|
|
98
|
+
export const MAX_CONTENT_LENGTH = 64 * 1024 * 1024; // used by its own test
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* LSP `MessageType`, as `window/showMessage` reports it.
|
|
@@ -211,5 +211,6 @@ export function uriToPath(uri) {
|
|
|
211
211
|
* @returns {string}
|
|
212
212
|
*/
|
|
213
213
|
export function pathToUri(path) {
|
|
214
|
+
// used by its own test
|
|
214
215
|
return pathToFileURL(path).href;
|
|
215
216
|
}
|
package/src/lsp/server.mjs
CHANGED
|
@@ -292,6 +292,7 @@ const POLYGLOT_GRAPH_MANIFESTS = Object.freeze([
|
|
|
292
292
|
* @returns {readonly string[]}
|
|
293
293
|
*/
|
|
294
294
|
export function watchedFilesFor(options, { unresolved = false } = {}) {
|
|
295
|
+
// used by its own test
|
|
295
296
|
return Object.freeze([
|
|
296
297
|
...(typeof options.boundaryConfig === "string" ? [options.boundaryConfig] : []),
|
|
297
298
|
...(unresolved
|
|
@@ -376,6 +377,7 @@ function markersAt(root) {
|
|
|
376
377
|
* convention chain can find while carrying files that need one.
|
|
377
378
|
*/
|
|
378
379
|
export function readWorkspaceOptions(root) {
|
|
380
|
+
// used by its own test
|
|
379
381
|
const { hasNx, hasNative } = markersAt(root);
|
|
380
382
|
if (hasNx && hasNative) {
|
|
381
383
|
throw new Error(
|
|
@@ -450,6 +452,7 @@ const WATCHER_REGISTRATION_ID = "archkeep/watched-files";
|
|
|
450
452
|
* text than the one on screen.
|
|
451
453
|
*/
|
|
452
454
|
export const SERVER_CAPABILITIES = Object.freeze({
|
|
455
|
+
// used by its own test
|
|
453
456
|
textDocumentSync: Object.freeze({
|
|
454
457
|
openClose: true,
|
|
455
458
|
change: TEXT_DOCUMENT_SYNC_KIND.full,
|
|
@@ -156,7 +156,7 @@ export { PROJECT_CONFIG_FILE, nodeTypeOf, buildDependencies };
|
|
|
156
156
|
* @returns {object} Whatever the JSON describes.
|
|
157
157
|
* @throws {Error} when neither parser can read it.
|
|
158
158
|
*/
|
|
159
|
-
|
|
159
|
+
const parseProjectJson = parseNxJson;
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Every file git considers part of the working tree, workspace-relative and
|
|
@@ -205,6 +205,7 @@ const directoryOf = (file) => {
|
|
|
205
205
|
* @returns {{projects: {name: string, root: string, config: object}[], skipped: {file: string, reason: string}[]}}
|
|
206
206
|
*/
|
|
207
207
|
export function discoverProjects({ files, readFile }) {
|
|
208
|
+
// used by its own test
|
|
208
209
|
const projects = [];
|
|
209
210
|
const skipped = [];
|
|
210
211
|
for (const file of files) {
|
|
@@ -263,6 +264,7 @@ export function discoverProjects({ files, readFile }) {
|
|
|
263
264
|
* publishes the collision through `indexGaps`.
|
|
264
265
|
*/
|
|
265
266
|
export function buildNodes(projects) {
|
|
267
|
+
// used by its own test
|
|
266
268
|
// Null-prototype for the same reason `../providers/native/graph.mjs` and
|
|
267
269
|
// `../providers/moon.mjs` use them: every key here is a project NAME, and
|
|
268
270
|
// project names come from a `project.json`'s own `name` field —
|