@ecoma-io/archkeep 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) 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 +86 -23
  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 +27 -5
  27. package/src/commands/delta.mjs +73 -39
  28. package/src/commands/diff.mjs +3 -9
  29. package/src/commands/drift.mjs +1 -1
  30. package/src/commands/evaluation-primitives.mjs +4 -4
  31. package/src/commands/evolution.mjs +2 -0
  32. package/src/commands/explain.mjs +2 -0
  33. package/src/commands/graph.mjs +39 -17
  34. package/src/commands/history.mjs +36 -0
  35. package/src/commands/plan-context-command.mjs +4 -1
  36. package/src/commands/policy.mjs +5 -2
  37. package/src/commands/scenario-evaluation.mjs +1 -1
  38. package/src/commands/snapshot-meta.mjs +33 -0
  39. package/src/commands/trajectory.mjs +2 -1
  40. package/src/config.mjs +1 -1
  41. package/src/custom-rules/host.mjs +3 -3
  42. package/src/custom-rules/values.mjs +1 -4
  43. package/src/eslint-config.mjs +1 -0
  44. package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
  45. package/src/go-work.mjs +1 -1
  46. package/src/governance/adr-registry.mjs +4 -1
  47. package/src/governance/debt-ledger.mjs +1 -1
  48. package/src/governance/decision-fitness.mjs +2 -0
  49. package/src/governance/decision-graph.mjs +1 -0
  50. package/src/governance/discovery-proposal.mjs +8 -2
  51. package/src/governance/evolution-event.mjs +42 -0
  52. package/src/governance/fitness-registry.mjs +16 -1
  53. package/src/governance/preset-fingerprints.json +14 -14
  54. package/src/governance/profile-registry.mjs +22 -3
  55. package/src/governance/provenance-record.mjs +4 -1
  56. package/src/governance/reconcile-score.mjs +4 -0
  57. package/src/governance/row-schema.mjs +1 -0
  58. package/src/governance/verdict.mjs +37 -4
  59. package/src/governance/waiver.mjs +1 -0
  60. package/src/intent/intent-manifest.json +18 -12
  61. package/src/intent/mask-non-code.mjs +1 -0
  62. package/src/lsp/diagnostics.mjs +3 -2
  63. package/src/lsp/protocol.mjs +2 -1
  64. package/src/lsp/server.mjs +3 -0
  65. package/src/lsp/workspace-index.mjs +3 -1
  66. package/src/providers/native/differential.fixtures.mjs +29 -11
  67. package/src/providers/native/index.mjs +2 -1
  68. package/src/providers/native/model.mjs +4 -0
  69. package/src/report/envelope-shape.mjs +2 -0
  70. package/src/report/json.mjs +4 -2
  71. package/src/report/sarif.mjs +21 -8
  72. package/src/report/snapshot-text.mjs +3 -3
  73. package/src/report/text.mjs +10 -2
  74. package/src/rules/match.mjs +7 -5
  75. package/src/rules/specifiers.mjs +2 -0
  76. package/src/rules/tags.mjs +3 -2
  77. package/src/rules/topology.mjs +6 -1
  78. package/src/values.mjs +11 -0
  79. package/src/workspace.mjs +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecoma-io/archkeep",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Architecture authority for human and agentic software development — deterministic, evidence-backed enforcement of declared architecture.",
5
5
  "keywords": [
6
6
  "architecture",
@@ -188,7 +188,7 @@ function withoutGenericArguments(rhs) {
188
188
  * @returns {{ specifier: string, importableName: string|null, specifierStartInBody: number }|null} `null`
189
189
  * when the body is a using STATEMENT's shape, not a directive's.
190
190
  */
191
- export function classifyUsingBody(body) {
191
+ function classifyUsingBody(body) {
192
192
  const trimmed = body.trim();
193
193
  if (trimmed === "") return null;
194
194
  const staticForm = STATIC_FORM.exec(trimmed);
@@ -236,6 +236,7 @@ export function classifyUsingBody(body) {
236
236
  * @returns {{ specifier: string, importableName: string|null, offset: number }[]}
237
237
  */
238
238
  export function parseCSharpDirectiveSites(csharpText) {
239
+ // used by its own test
239
240
  const source = maskCSharpComments(csharpText);
240
241
  const sites = [];
241
242
  for (const match of source.matchAll(CS_USING_BODY)) {
@@ -305,6 +306,7 @@ export function parseCSharpDirectiveSites(csharpText) {
305
306
  * each naming its line. Empty when the directives read fully.
306
307
  */
307
308
  export function csharpDirectiveMalformations(csharpText) {
309
+ // used by its own test
308
310
  const source = maskCSharpComments(csharpText);
309
311
  /** @type {string[]} */
310
312
  const reasons = [];
@@ -122,6 +122,7 @@ function collectElements(node, name) {
122
122
  * { project?: undefined, reason: string }}
123
123
  */
124
124
  export function parseCsproj(text) {
125
+ // used by its own test
125
126
  const { parser, validate, error } = xmlParser();
126
127
  if (parser === null) {
127
128
  return { reason: `${XML_PARSER} is unavailable (${error})` };
@@ -153,6 +154,7 @@ export function parseCsproj(text) {
153
154
  * @returns {{ paths: string[], problems: string[] }}
154
155
  */
155
156
  export function projectReferenceFacts(project, csprojDir) {
157
+ // used by its own test
156
158
  const paths = [];
157
159
  const problems = [];
158
160
  for (const ref of collectElements(project, "ProjectReference")) {
@@ -184,6 +186,7 @@ export function projectReferenceFacts(project, csprojDir) {
184
186
  * @returns {string[]}
185
187
  */
186
188
  export function usingNamespacesOf(project) {
189
+ // used by its own test
187
190
  const namespaces = [];
188
191
  for (const item of collectElements(project, "Using")) {
189
192
  const include = textOf(item["@_Include"] ?? item.Include);
@@ -215,6 +218,7 @@ export function usingNamespacesOf(project) {
215
218
  * { entry?: undefined, problems?: undefined, reason: string }}
216
219
  */
217
220
  export function csprojEntryOf(projectName, csprojPath, text) {
221
+ // used by its own test
218
222
  const parsed = parseCsproj(text);
219
223
  if (parsed.reason !== undefined) return { reason: parsed.reason };
220
224
  // A manifest at the workspace root has no separator: `lastIndexOf` answers
@@ -251,7 +255,7 @@ export function csprojEntryOf(projectName, csprojPath, text) {
251
255
  * usingEdges: { source: string, target: string, sourceFile: string, type: string }[],
252
256
  * failures: { sourceFile: string, line: null, column: null, reason: string }[] }}
253
257
  */
254
- export const csprojModelOf = perWorkspace(({ projects, filesOf, readFile }) => {
258
+ const csprojModelOf = perWorkspace(({ projects, filesOf, readFile }) => {
255
259
  const entries = [];
256
260
  const failures = [];
257
261
  const identity = new Map();
@@ -62,6 +62,7 @@ import { maskCSharpComments } from "./mask.mjs";
62
62
  * which declares no name the index can carry.
63
63
  */
64
64
  export function parseCSharpNamespaceDeclarations(maskedText) {
65
+ // used by its own test
65
66
  const CS_NAMESPACE_DECLARATION =
66
67
  /(?:^\uFEFF?|\n)[ \t]*namespace[ \t]+([\p{L}_][\p{L}\p{Nd}_]*(?:[ \t]*\.[ \t]*[\p{L}_][\p{L}\p{Nd}_]*)*)[ \t]*(?:;|(?=[{;\r\n]|$))/gu;
67
68
  const declarations = [];
@@ -132,6 +132,7 @@ import {
132
132
  * character shifts nothing any record points at.
133
133
  */
134
134
  export function parseGoModulePath(goModText) {
135
+ // used by its own test
135
136
  const match = goModText.replace(/^\uFEFF/, "").match(/^module\s+(\S+)/m);
136
137
  if (!match) return null;
137
138
  const token = match[1];
@@ -170,6 +171,7 @@ const LEXICAL_START = /\/\/|\/\*|["'`]/g;
170
171
  * @returns {string} Same length as `goText`.
171
172
  */
172
173
  export function maskGoComments(goText) {
174
+ // used by its own test
173
175
  const scan = new RegExp(LEXICAL_START.source, "g");
174
176
  let masked = "";
175
177
  let copied = 0;
@@ -298,6 +300,7 @@ const GO_ALIAS_AT = /[\p{L}_.][\p{L}\p{Nd}_.]*\s+/uy;
298
300
  * seen on. Empty when the imports read fully.
299
301
  */
300
302
  export function goImportMalformations(goText) {
303
+ // used by its own test
301
304
  const source = blankGoStringContents(maskGoComments(goText));
302
305
  /** @type {string[]} */
303
306
  const reasons = [];
@@ -417,6 +420,7 @@ const GO_IMPORT_ALIAS = "[\\p{L}_.][\\p{L}\\p{Nd}_.]*";
417
420
  * @returns {{ specifier: string, offset: number }[]}
418
421
  */
419
422
  export function parseGoImportSites(goText) {
423
+ // used by its own test
420
424
  const source = maskGoComments(goText);
421
425
  const sites = [];
422
426
  // import "p" | import alias "p" | import _ "p" | import . "p" — the path
@@ -457,6 +461,7 @@ export function parseGoImportSites(goText) {
457
461
 
458
462
  /** Every import path in a .go file (single-form and block-form), deduped. */
459
463
  export function parseGoImports(goText) {
464
+ // used by its own test
460
465
  return [...new Set(parseGoImportSites(goText).map((site) => site.specifier))];
461
466
  }
462
467
 
@@ -604,6 +609,7 @@ const isUnderModule = (importPath, modulePath) =>
604
609
  * no module claims `importPath`.
605
610
  */
606
611
  export function resolveGoModule(importPath, modules) {
612
+ // used by its own test
607
613
  let matched = "";
608
614
  let project = null;
609
615
  for (const { modulePath, project: target } of modules) {
@@ -69,6 +69,7 @@ import {
69
69
  * @returns {{ specifier: string, importableName: string, offset: number }[]}
70
70
  */
71
71
  export function parseJavaImportSites(javaText) {
72
+ // used by its own test
72
73
  const source = maskJavaComments(javaText);
73
74
  // Anchored to a line head — through a leading UTF-8 BOM, matched rather than
74
75
  // stripped so offsets keep indexing the bytes on disk, the same anchor
@@ -132,6 +133,7 @@ export function parseJavaImportSites(javaText) {
132
133
  * read fully.
133
134
  */
134
135
  export function javaImportMalformations(javaText) {
136
+ // used by its own test
135
137
  const source = maskJavaComments(javaText);
136
138
  const JAVA_IMPORT_HEAD = /(?:^\uFEFF?|[\n;])[ \t]*(?:import[ \t]+)/gu;
137
139
  /** @type {string[]} */
@@ -90,6 +90,7 @@ import { fileFailure, perWorkspace, refuseUnreadTree } from "../source-util.mjs"
90
90
  * { rootProjectName?: undefined, includedProjects?: undefined, reason: string }}
91
91
  */
92
92
  export function parseGradleSettings(text) {
93
+ // used by its own test
93
94
  // First, remove block comments to avoid matching include statements inside them
94
95
  let processedText = text.replace(/\/\*[\s\S]*?\*\//g, "");
95
96
 
@@ -187,6 +188,7 @@ export function parseGradleSettings(text) {
187
188
  * { projectDependencies?: undefined, reason: string }}
188
189
  */
189
190
  export function parseGradleBuild(text) {
191
+ // used by its own test
190
192
  const lines = text.split(/\r?\n/);
191
193
  const projectDependencies = [];
192
194
  let inString = false;
@@ -480,7 +482,7 @@ function buildGradleModel(workspace) {
480
482
  return { entries, pathToDirectory, failures };
481
483
  }
482
484
 
483
- export const gradleModelOf = perWorkspace(buildGradleModel);
485
+ const gradleModelOf = perWorkspace(buildGradleModel);
484
486
 
485
487
  /**
486
488
  * Manifest-edge resolver: one edge per declared project dependency whose
@@ -128,6 +128,7 @@ const textOf = (value) => {
128
128
  * { project?: undefined, reason: string }}
129
129
  */
130
130
  export function parsePomProject(text) {
131
+ // used by its own test
131
132
  const { parser, validate, error } = xmlParser();
132
133
  if (parser === null) {
133
134
  return { reason: `${XML_PARSER} is unavailable (${error})` };
@@ -179,6 +180,7 @@ export function parsePomProject(text) {
179
180
  * @returns {{ entry: PomEntry, reason?: undefined } | { entry?: undefined, reason: string }}
180
181
  */
181
182
  export function pomEntryOf(projectName, pomPath, text) {
183
+ // used by its own test
182
184
  const parsed = parsePomProject(text);
183
185
  if (parsed.reason !== undefined) return { reason: parsed.reason };
184
186
  const project = parsed.project;
@@ -252,6 +254,7 @@ export function pomEntryOf(projectName, pomPath, text) {
252
254
  * @returns {string|null}
253
255
  */
254
256
  export function parentPomPath(pomPath, relativePath) {
257
+ // used by its own test
255
258
  const dir = pomPath.includes("/") ? pomPath.slice(0, pomPath.lastIndexOf("/")) : "";
256
259
  const direct = resolveWithinWorkspace(dir, relativePath);
257
260
  if (direct === null) return null;
@@ -270,6 +273,7 @@ export function parentPomPath(pomPath, relativePath) {
270
273
  * @returns {Record<string, string>}
271
274
  */
272
275
  export function mavenConfigProperties(readFile, candidatePaths) {
276
+ // used by its own test
273
277
  const props = /** @type {Record<string, string>} */ ({});
274
278
  for (const path of candidatePaths) {
275
279
  const text = readFile(path);
@@ -300,6 +304,7 @@ export function mavenConfigProperties(readFile, candidatePaths) {
300
304
  * @returns {{ value: string, resolved: boolean }}
301
305
  */
302
306
  export function interpolateCoordinate(value, props, builtins) {
307
+ // used by its own test
303
308
  let resolved = true;
304
309
  const out = value.replace(/\$\{([^}]+)\}/g, (_, key) => {
305
310
  if (Object.hasOwn(builtins, key)) return builtins[key];
@@ -557,7 +562,7 @@ function buildMavenModel(workspace) {
557
562
  return { entries, identityHolders, failures };
558
563
  }
559
564
 
560
- export const mavenModelOf = perWorkspace(buildMavenModel);
565
+ export const mavenModelOf = perWorkspace(buildMavenModel); // used by its own test
561
566
 
562
567
  /**
563
568
  * Manifest-edge resolver: one edge per declared dependency whose coordinates
@@ -81,6 +81,7 @@ import { maskJavaComments, maskKotlinComments } from "./mask.mjs";
81
81
  * package file, which declares no name the index can carry.
82
82
  */
83
83
  export function parseJvmPackageDeclaration(maskedText) {
84
+ // used by its own test
84
85
  // An identifier segment starts with a letter (Unicode, via \p{L}), `_`, or
85
86
  // `$`, continues with those plus digits; segments join on optional spaces
86
87
  // around the dot, because `com . example` is legal if absurd. The match is
@@ -40,7 +40,7 @@ import { resolveJvmPackagePrefix } from "./packages.mjs";
40
40
  * their own explicit imports, which is why an explicit `import java.util.List`
41
41
  * classifies as ordinary external rather than by this table.
42
42
  */
43
- export const JAVA_DEFAULT_IMPORT_ROOTS = Object.freeze(["java.lang"]);
43
+ export const JAVA_DEFAULT_IMPORT_ROOTS = Object.freeze(["java.lang"]); // used by its own test
44
44
 
45
45
  /**
46
46
  * Kotlin's default imports (kotlinlang.org, "Default imports", page dated
@@ -50,6 +50,7 @@ export const JAVA_DEFAULT_IMPORT_ROOTS = Object.freeze(["java.lang"]);
50
50
  * testable classification of names the compiler brings in unasked.
51
51
  */
52
52
  export const KOTLIN_DEFAULT_IMPORT_ROOTS = Object.freeze([
53
+ // used by its own test
53
54
  "kotlin",
54
55
  "kotlin.annotation",
55
56
  "kotlin.collections",
@@ -70,7 +71,7 @@ const DEFAULT_IMPORT_ROOTS_BY_LANGUAGE = {
70
71
  };
71
72
 
72
73
  /** The default-import roots a language's files carry, or `[]` when unknown. */
73
- export const defaultImportRootsFor = (language) => DEFAULT_IMPORT_ROOTS_BY_LANGUAGE[language] ?? [];
74
+ export const defaultImportRootsFor = (language) => DEFAULT_IMPORT_ROOTS_BY_LANGUAGE[language] ?? []; // used by its own test
74
75
 
75
76
  /**
76
77
  * True when `specifier` falls under one of `roots`: equal to a root or a dot-
@@ -83,6 +84,7 @@ export const defaultImportRootsFor = (language) => DEFAULT_IMPORT_ROOTS_BY_LANGU
83
84
  * @returns {boolean}
84
85
  */
85
86
  export const underAnyRoot = (specifier, roots) =>
87
+ // used by its own test
86
88
  roots.some((root) => specifier === root || specifier.startsWith(`${root}.`));
87
89
 
88
90
  /**
@@ -69,6 +69,7 @@ const KOTLIN_IMPORT = new RegExp(
69
69
  * @returns {{ specifier: string, importableName: string, offset: number }[]}
70
70
  */
71
71
  export function parseKotlinImportSites(kotlinText) {
72
+ // used by its own test
72
73
  // A shebang needs no handling of its own: `#!…` cannot anchor an import
73
74
  // match (`import` must follow a line head, `;`, or newline), and masking
74
75
  // runs before anything reads the text anyway.
@@ -112,6 +113,7 @@ const importableNameOf = (name) => (name.endsWith(".*") ? name.slice(0, -2) : na
112
113
  * @returns {string[]} Reasons, at most one per malformation kind.
113
114
  */
114
115
  export function kotlinImportMalformations(kotlinText) {
116
+ // used by its own test
115
117
  const source = maskKotlinComments(kotlinText);
116
118
  const KOTLIN_IMPORT_HEAD = /(?:^\uFEFF?|[\n;])[ \t]*(?:import[ \t]+)/gu;
117
119
  /** @type {string[]} */
@@ -259,6 +259,7 @@ export function foldMarkdownTrack({ tracked, owned, readFile, workspace, markdow
259
259
  * @returns {string[]}
260
260
  */
261
261
  export function markdownIncludedFiles({ include, tracked }) {
262
+ // used by its own test
262
263
  return tracked.filter(
263
264
  (file) =>
264
265
  file.endsWith(MARKDOWN_EXTENSION) && include.some((pattern) => matchesInclude(file, pattern)),
@@ -227,17 +227,20 @@ import {
227
227
 
228
228
  /** PEP 503 name normalization: case-insensitive, runs of `-_.` collapse to `-`. */
229
229
  export function normalizePackageName(name) {
230
+ // used by its own test
230
231
  return name.toLowerCase().replace(/[-_.]+/g, "-");
231
232
  }
232
233
 
233
234
  /** The package name a PEP 508 requirement string refers to, or null. */
234
235
  export function parseRequirementName(requirement) {
236
+ // used by its own test
235
237
  const match = requirement.trim().match(/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?/);
236
238
  return match ? normalizePackageName(match[0]) : null;
237
239
  }
238
240
 
239
241
  /** Every dependency name a pyproject manifest declares, deduped. */
240
242
  export function collectDeclaredDependencies(manifest) {
243
+ // used by its own test
241
244
  const names = new Set();
242
245
  const groups = [
243
246
  manifest.project?.dependencies ?? [],
@@ -630,6 +633,7 @@ const READ_BUILD_BACKENDS = new Set([
630
633
  * @returns {{ directories: string[], unmodelled: string[] }}
631
634
  */
632
635
  export function pythonPackageLayout(manifestText) {
636
+ // used by its own test
633
637
  if (manifestText === null) return { directories: [], unmodelled: [] };
634
638
  const manifest = parseManifest(manifestText);
635
639
  if (manifest === null) {
@@ -730,6 +734,7 @@ function ownPackageOf(file, projectRoot, directories) {
730
734
  * @returns {Map<string, { file: string|null, namespace: boolean }>}
731
735
  */
732
736
  export function pythonModuleIndex(projectRoot, files, directories = []) {
737
+ // used by its own test
733
738
  const index = new Map();
734
739
  for (const file of files) {
735
740
  if (!file.endsWith(".py")) continue;
@@ -755,6 +760,7 @@ export function pythonModuleIndex(projectRoot, files, directories = []) {
755
760
  * @returns {string[]}
756
761
  */
757
762
  export function pythonImportRoots(projectRoot, files, directories = []) {
763
+ // used by its own test
758
764
  return [...pythonModuleIndex(projectRoot, files, directories).keys()]
759
765
  .filter((name) => !name.includes("."))
760
766
  .sort();
@@ -1059,6 +1065,7 @@ function joinContinuedStatement(physicalLines, lineOffsets, index) {
1059
1065
  * continuation?: boolean }[]}
1060
1066
  */
1061
1067
  export function parsePythonImportSites(pythonText) {
1068
+ // used by its own test
1062
1069
  const sites = [];
1063
1070
  // Byte tolerance (`contract.md`): the lines a CRLF file splits into still
1064
1071
  // carry their `\r`, and a BOM-prefixed file's first line starts with
@@ -1166,6 +1173,7 @@ export function parsePythonImportSites(pythonText) {
1166
1173
  * @returns {string[]} Reasons, at most one per malformation kind.
1167
1174
  */
1168
1175
  export function pythonImportMalformations(pythonText) {
1176
+ // used by its own test
1169
1177
  /** @type {string[]} */
1170
1178
  const reasons = [];
1171
1179
  // A bare `import` or `from` at the end of the file, or on a line with
@@ -238,6 +238,7 @@ export function resolveRustDependencies(projects, filesOf, readFile) {
238
238
 
239
239
  /** Cargo's identifier spelling of a crate name: `-` and `.` become `_`. */
240
240
  export function crateIdentifier(name) {
241
+ // used by its own test
241
242
  return name.replace(/[-.]/g, "_");
242
243
  }
243
244
 
@@ -250,6 +251,7 @@ export function crateIdentifier(name) {
250
251
  * @returns {string|null}
251
252
  */
252
253
  export function crateImportName(manifest) {
254
+ // used by its own test
253
255
  const declared = manifest?.lib?.name ?? manifest?.package?.name;
254
256
  return typeof declared === "string" && declared !== "" ? crateIdentifier(declared) : null;
255
257
  }
@@ -471,7 +473,7 @@ function isOwnProjectPath(root, owner, byCrate) {
471
473
  * @param {string} path The `use` path, from the first non-space to the `;`.
472
474
  * @returns {{text: string, offset: number}[] | null}
473
475
  */
474
- export function braceGroupArms(path) {
476
+ function braceGroupArms(path) {
475
477
  const open = path.indexOf("{");
476
478
  if (open === -1 || path.slice(0, open).trim() !== "") return null;
477
479
  /** @type {{text: string, offset: number}[]} */
@@ -520,6 +522,7 @@ export function braceGroupArms(path) {
520
522
  * with a brace group and names none.
521
523
  */
522
524
  export function useRootSegment(path) {
525
+ // used by its own test
523
526
  const match = /^\s*(?:::\s*)?([A-Za-z_]\w*)/.exec(path);
524
527
  return match ? match[1] : null;
525
528
  }
@@ -560,6 +563,7 @@ export function useRootSegment(path) {
560
563
  * produces from reading as a clean file.
561
564
  */
562
565
  export function parseRustUseSites(rustText, knownCrates = new Set(), options = {}) {
566
+ // used by its own test
563
567
  const { returnMetrics = false } = options;
564
568
  /** The offset of the first `use` opener no `;` terminates, `null` before one is seen. */
565
569
  let unterminatedUseAt = null;
@@ -160,7 +160,7 @@ function ownershipIndexOf(projects) {
160
160
  * one per equality probe.
161
161
  */
162
162
  let rootComparisons = 0;
163
- export const ownershipRootComparisons = () => rootComparisons;
163
+ export const ownershipRootComparisons = () => rootComparisons; // used by its own test
164
164
 
165
165
  /**
166
166
  * The first index in `roots` (sorted ascending) whose value is at or after
@@ -318,6 +318,7 @@ const SCRIPT_KIND_BY_LANG = Object.freeze({
318
318
  * @returns {{ path: boolean, relative: boolean, namesOnly: boolean }}
319
319
  */
320
320
  export function specifierSpelling(specifier) {
321
+ // used by its own test
321
322
  const relative =
322
323
  specifier === "." ||
323
324
  specifier === ".." ||
@@ -340,6 +341,7 @@ export function specifierSpelling(specifier) {
340
341
  * @returns {string|null}
341
342
  */
342
343
  export function packageNameOf(specifier) {
344
+ // used by its own test
343
345
  if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/")) return null;
344
346
  const segments = specifier.split("/");
345
347
  if (specifier.startsWith("@"))
@@ -51,7 +51,7 @@ import { GOVERNANCE_ROW_KEYS, rowSchemaViolations } from "../governance/row-sche
51
51
  export const INTENT_FILE = "architecture-intent.json";
52
52
 
53
53
  /** The one supported `version`. A different value is a load error. */
54
- export const INTENT_VERSION = "1";
54
+ export const INTENT_VERSION = "1"; // used by its own test
55
55
 
56
56
  /**
57
57
  * The only keys a valid intent file may carry at the top level.
@@ -66,6 +66,7 @@ export const INTENT_VERSION = "1";
66
66
  * engine judges them.
67
67
  */
68
68
  export const TOP_LEVEL_KEYS = Object.freeze([
69
+ // used by its own test
69
70
  "version",
70
71
  "boundaries",
71
72
  "allowed",
@@ -76,17 +77,17 @@ export const TOP_LEVEL_KEYS = Object.freeze([
76
77
  ]);
77
78
 
78
79
  /** The sub-keys a `projects` section may carry. */
79
- export const PROJECT_SECTION_KEYS = Object.freeze(["required", "forbidden"]);
80
+ const PROJECT_SECTION_KEYS = Object.freeze(["required", "forbidden"]);
80
81
  /** The sub-keys a `dependencies` section may carry. */
81
- export const DEPENDENCY_SECTION_KEYS = Object.freeze(["allowed", "forbidden"]);
82
+ const DEPENDENCY_SECTION_KEYS = Object.freeze(["allowed", "forbidden"]);
82
83
  /** The keys a `projects.required[]` row may carry. */
83
- export const REQUIRED_PROJECT_KEYS = Object.freeze(["name", "tags", "decisionRef"]);
84
+ const REQUIRED_PROJECT_KEYS = Object.freeze(["name", "tags", "decisionRef"]);
84
85
  /** The keys a `projects.forbidden[]` row may carry. */
85
- export const FORBIDDEN_PROJECT_KEYS = Object.freeze(["name", "decisionRef"]);
86
+ const FORBIDDEN_PROJECT_KEYS = Object.freeze(["name", "decisionRef"]);
86
87
  /** The keys a `dependencies.allowed[]` / `dependencies.forbidden[]` row may carry. */
87
- export const DEPENDENCY_ROW_KEYS = Object.freeze(["source", "target", "decisionRef"]);
88
+ const DEPENDENCY_ROW_KEYS = Object.freeze(["source", "target", "decisionRef"]);
88
89
  /** The keys a `forbiddenTags[]` row may carry. */
89
- export const TAG_ROW_KEYS = Object.freeze(["from", "to", "decisionRef"]);
90
+ const TAG_ROW_KEYS = Object.freeze(["from", "to", "decisionRef"]);
90
91
 
91
92
  /**
92
93
  * The shared governance-block check for any intent row (Contract 2): when the
@@ -157,6 +158,7 @@ function isSingleProjectSelector(selector) {
157
158
  * resolve against.
158
159
  */
159
160
  export function boundaryNames(intent) {
161
+ // used by its own test
160
162
  return (intent.boundaries ?? []).map((b) => b.name);
161
163
  }
162
164
 
@@ -168,6 +170,7 @@ export function boundaryNames(intent) {
168
170
  * @returns {string[]}
169
171
  */
170
172
  export function findIntentViolations(raw) {
173
+ // used by its own test
171
174
  const violations = [];
172
175
 
173
176
  if (!isPlainObject(raw)) {
@@ -603,6 +606,7 @@ export function findIntentViolations(raw) {
603
606
  * @returns {object} The normalized model: `{version, boundaries: {name, match, members?}[], allowed, forbidden, projects?, dependencies?, forbiddenTags?}`.
604
607
  */
605
608
  export function normalizeIntent(intent) {
609
+ // used by its own test
606
610
  return {
607
611
  version: intent.version,
608
612
  boundaries: intent.boundaries.map((b) => ({ name: b.name, match: [...b.match] })),
@@ -30,7 +30,7 @@
30
30
  */
31
31
 
32
32
  /** The three selector labels. `unlabeled` is a bare project name (equals `name:`). */
33
- export const SELECTOR_LABELS = Object.freeze(["name", "tag", "directory"]);
33
+ const SELECTOR_LABELS = Object.freeze(["name", "tag", "directory"]);
34
34
 
35
35
  /**
36
36
  * Split a selector into `{exclude, label, value}` — `!` prefix removed, `*`
@@ -71,6 +71,7 @@ export function isValidSelector(value) {
71
71
  * @returns {string[]}
72
72
  */
73
73
  export function selectProjects(selector, nodes) {
74
+ // used by its own test
74
75
  const { label, value } = splitSelector(selector);
75
76
 
76
77
  let found;
@@ -44,10 +44,10 @@ import { readFile as readFileFromDisk } from "node:fs/promises";
44
44
  import { describe, isPlainObject } from "../values.mjs";
45
45
 
46
46
  /** The only `version` this module accepts. A different value is a load error. */
47
- export const CHANGE_INTENT_VERSION = "1";
47
+ const CHANGE_INTENT_VERSION = "1";
48
48
 
49
49
  /** The only keys a valid change-intent file may carry at the top level. */
50
- export const CHANGE_INTENT_TOP_LEVEL_KEYS = Object.freeze([
50
+ const CHANGE_INTENT_TOP_LEVEL_KEYS = Object.freeze([
51
51
  "version",
52
52
  "base",
53
53
  "summary",
@@ -57,16 +57,16 @@ export const CHANGE_INTENT_TOP_LEVEL_KEYS = Object.freeze([
57
57
  ]);
58
58
 
59
59
  /** The keys the `base` section may carry. */
60
- export const CHANGE_INTENT_BASE_KEYS = Object.freeze(["commit"]);
60
+ const CHANGE_INTENT_BASE_KEYS = Object.freeze(["commit"]);
61
61
 
62
62
  /** The sub-keys the `projects` section may carry. */
63
- export const CHANGE_INTENT_PROJECT_SECTION_KEYS = Object.freeze(["add", "remove"]);
63
+ const CHANGE_INTENT_PROJECT_SECTION_KEYS = Object.freeze(["add", "remove"]);
64
64
  /** The sub-keys the `edges` section may carry. */
65
- export const CHANGE_INTENT_EDGE_SECTION_KEYS = Object.freeze(["add", "remove"]);
65
+ const CHANGE_INTENT_EDGE_SECTION_KEYS = Object.freeze(["add", "remove"]);
66
66
  /** The keys an edge row may carry. */
67
- export const CHANGE_INTENT_EDGE_ROW_KEYS = Object.freeze(["from", "to"]);
67
+ const CHANGE_INTENT_EDGE_ROW_KEYS = Object.freeze(["from", "to"]);
68
68
  /** The keys the `constraints` section may carry. */
69
- export const CHANGE_INTENT_CONSTRAINT_KEYS = Object.freeze(["noNewViolations", "noNewCycles"]);
69
+ const CHANGE_INTENT_CONSTRAINT_KEYS = Object.freeze(["noNewViolations", "noNewCycles"]);
70
70
 
71
71
  /**
72
72
  * The constraints the reconciliation judges, in the fixed order their verdict
@@ -204,7 +204,7 @@ function sectionListViolations(section, spec) {
204
204
  * @param {unknown} raw The parsed JSON value.
205
205
  * @returns {string[]}
206
206
  */
207
- export function findChangeIntentViolations(raw) {
207
+ function findChangeIntentViolations(raw) {
208
208
  const violations = [];
209
209
  if (!isPlainObject(raw)) {
210
210
  return [`top level: must be an object, got ${describe(raw)}`];
@@ -351,7 +351,7 @@ export function findChangeIntentViolations(raw) {
351
351
  * with absent raw sections already normalized to empty arrays.
352
352
  * @returns {string[]} Messages; empty when the intent is not a catch-all.
353
353
  */
354
- export function findChangeIntentBreadthViolations(intent) {
354
+ function findChangeIntentBreadthViolations(intent) {
355
355
  const declaredRows =
356
356
  intent.projects.add.length +
357
357
  intent.projects.remove.length +
@@ -389,6 +389,7 @@ export function findChangeIntentBreadthViolations(intent) {
389
389
  * silently would reconcile against an expectation nobody wrote.
390
390
  */
391
391
  export function parseChangeIntent(text, path) {
392
+ // used by its own test
392
393
  let parsed;
393
394
  try {
394
395
  parsed = JSON.parse(text);