@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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/analysis/csharp.mjs +3 -1
  3. package/src/analysis/dotnet/csproj.mjs +5 -1
  4. package/src/analysis/dotnet/namespaces.mjs +1 -0
  5. package/src/analysis/go.mjs +6 -0
  6. package/src/analysis/java.mjs +2 -0
  7. package/src/analysis/jvm/gradle.mjs +3 -1
  8. package/src/analysis/jvm/maven.mjs +6 -1
  9. package/src/analysis/jvm/packages.mjs +1 -0
  10. package/src/analysis/jvm/resolve.mjs +4 -2
  11. package/src/analysis/kotlin.mjs +2 -0
  12. package/src/analysis/markdown.mjs +1 -0
  13. package/src/analysis/python.mjs +8 -0
  14. package/src/analysis/rust.mjs +5 -1
  15. package/src/analysis/source-util.mjs +1 -1
  16. package/src/analysis/typescript.mjs +2 -0
  17. package/src/architecture-intent/model.mjs +11 -7
  18. package/src/architecture-intent/selectors.mjs +2 -1
  19. package/src/commands/change-intent.mjs +10 -9
  20. package/src/commands/change.mjs +17 -1
  21. package/src/commands/check.mjs +11 -9
  22. package/src/commands/completeness.mjs +7 -6
  23. package/src/commands/coverage-acceptance.mjs +46 -0
  24. package/src/commands/custom-rules.mjs +1 -0
  25. package/src/commands/delta-classify.mjs +3 -0
  26. package/src/commands/delta-snapshot.mjs +2 -1
  27. package/src/commands/delta.mjs +35 -21
  28. package/src/commands/drift.mjs +1 -1
  29. package/src/commands/evaluation-primitives.mjs +4 -4
  30. package/src/commands/evolution.mjs +2 -0
  31. package/src/commands/explain.mjs +2 -0
  32. package/src/commands/graph.mjs +39 -17
  33. package/src/commands/history.mjs +2 -0
  34. package/src/commands/plan-context-command.mjs +4 -1
  35. package/src/commands/policy.mjs +5 -2
  36. package/src/commands/scenario-evaluation.mjs +1 -1
  37. package/src/commands/trajectory.mjs +2 -1
  38. package/src/config.mjs +1 -1
  39. package/src/custom-rules/host.mjs +3 -3
  40. package/src/eslint-config.mjs +1 -0
  41. package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
  42. package/src/go-work.mjs +1 -1
  43. package/src/governance/adr-registry.mjs +4 -1
  44. package/src/governance/debt-ledger.mjs +1 -1
  45. package/src/governance/decision-fitness.mjs +2 -0
  46. package/src/governance/decision-graph.mjs +1 -0
  47. package/src/governance/discovery-proposal.mjs +8 -2
  48. package/src/governance/evolution-event.mjs +42 -0
  49. package/src/governance/fitness-registry.mjs +2 -0
  50. package/src/governance/preset-fingerprints.json +14 -14
  51. package/src/governance/profile-registry.mjs +22 -3
  52. package/src/governance/provenance-record.mjs +4 -1
  53. package/src/governance/reconcile-score.mjs +4 -0
  54. package/src/governance/row-schema.mjs +1 -0
  55. package/src/governance/verdict.mjs +1 -0
  56. package/src/governance/waiver.mjs +1 -0
  57. package/src/intent/intent-manifest.json +3 -3
  58. package/src/intent/mask-non-code.mjs +1 -0
  59. package/src/lsp/diagnostics.mjs +3 -2
  60. package/src/lsp/protocol.mjs +2 -1
  61. package/src/lsp/server.mjs +3 -0
  62. package/src/lsp/workspace-index.mjs +3 -1
  63. package/src/providers/native/differential.fixtures.mjs +29 -11
  64. package/src/providers/native/index.mjs +2 -1
  65. package/src/providers/native/model.mjs +4 -0
  66. package/src/report/envelope-shape.mjs +2 -0
  67. package/src/report/sarif.mjs +21 -8
  68. package/src/report/snapshot-text.mjs +3 -3
  69. package/src/report/text.mjs +10 -2
  70. package/src/rules/match.mjs +7 -5
  71. package/src/rules/specifiers.mjs +2 -0
  72. package/src/rules/tags.mjs +3 -2
  73. package/src/rules/topology.mjs +6 -1
  74. 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
- export function tokenizeGoWorkLine(line, lineNumber) {
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
- export const ADR_FILE_PATTERN = /^(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/u;
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
- export function entryId(kind, source) {
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
- export function sameComponent(components, source, target) {
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": "28e4dc572cf278e69f05c01bed020cda425ebd7b11bfe04804f21d2f1bbdcb69",
3
- "clean-architecture#clean-architecture-pure-core": "85e7d5a36759169190dab1d871acc1d2c733b145306ef33f7b161e641c9f78e4",
4
- "ddd-bounded-contexts#ddd-bounded-contexts": "8ec7feb0e6dbf64373e1aaf5bfc9d53d6299de3c11fb38955a5a3857fac04f09",
5
- "ddd-bounded-contexts#ddd-bounded-contexts-isolated": "8ac4183559010abf01e20ae50bffd7611b78417b4af531af6c7a15fbcdc443da",
6
- "ddd-bounded-contexts#ddd-bounded-contexts-partitioned": "982f90d0718c2bdd6874411964ef9baa379ee5f9c849404cb9cde2ab7aaccc19",
7
- "hexagonal#hexagonal": "08e57312c449a79ce32c4248931e7d9488441fa47f8638fb780fd02602442003",
8
- "hexagonal#hexagonal-pure-domain": "3a43406883c6e182592ff1f47347c6f9a04f04690300ea5b9260877dea2f134f",
9
- "layered#layered-relaxed": "ad2912594d26a270b6139c6561454ef01e2bf6e2214d2d8ec57afcc8fee11467",
10
- "layered#layered-strict": "26af569cb6c352a223e09e18e3e7c9817d5c1cb53575be46ffad950bd2f84be2",
11
- "modular-monolith#modular-monolith": "4b3517e4d4ae1357e675b947d7e4845260fa6d75122f40d8e75ad202d910606a",
12
- "modular-monolith#modular-monolith-sealed-kernel": "124a7bafd43abbd87e5218146219d57af6010e9374d80f0c3c1d2ac13a8e2677",
13
- "modular-monolith#modular-monolith-sealed-modules": "772d74fd8bbdb85ea39c5e38bdb8193a4f069ace8b458bff96943d5a243f5d25",
14
- "vertical-slice#vertical-slice": "5a4a17b041bb57046ba0f42a28032afb933c1f28ee3ecaa1262c706786c14dc7",
15
- "vertical-slice#vertical-slice-sealed-kernel": "efbcd5617f3c96f43b02f622cd79c4b27e49af907329ec9dcd9ee77549eb49fa"
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
- export const PROFILE_REGISTRY_SCHEMA_VERSION = 1;
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 policyFrom(effective, `${sourceLabel} (profile "${profileName}")`);
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}`));
@@ -94,6 +94,7 @@ export const GOVERNANCE_ROW_KEYS = Object.freeze([
94
94
  * @returns {string[]}
95
95
  */
96
96
  export function governanceBlockViolations(raw, at) {
97
+ // used by its own test
97
98
  if (!isPlainObject(raw)) return [];
98
99
  const violations = [];
99
100
 
@@ -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",
@@ -55,6 +55,7 @@ export function isWaiver(row) {
55
55
  * @returns {number}
56
56
  */
57
57
  export function expiresAtMs(row) {
58
+ // used by its own test
58
59
  return Date.parse(String(row.expiresAt));
59
60
  }
60
61
 
@@ -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": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
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": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
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": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
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
@@ -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
@@ -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
  }
@@ -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
- export const parseProjectJson = parseNxJson;
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 —