@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.
- 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 +86 -23
- 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 +27 -5
- package/src/commands/delta.mjs +73 -39
- package/src/commands/diff.mjs +3 -9
- 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 +36 -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/snapshot-meta.mjs +33 -0
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +1 -1
- package/src/custom-rules/host.mjs +3 -3
- package/src/custom-rules/values.mjs +1 -4
- 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 +16 -1
- 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 +37 -4
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +18 -12
- 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/json.mjs +4 -2
- 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/values.mjs +11 -0
- 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.
|
|
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",
|
package/src/analysis/csharp.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 = [];
|
package/src/analysis/go.mjs
CHANGED
|
@@ -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) {
|
package/src/analysis/java.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/analysis/kotlin.mjs
CHANGED
|
@@ -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)),
|
package/src/analysis/python.mjs
CHANGED
|
@@ -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
|
package/src/analysis/rust.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
80
|
+
const PROJECT_SECTION_KEYS = Object.freeze(["required", "forbidden"]);
|
|
80
81
|
/** The sub-keys a `dependencies` section may carry. */
|
|
81
|
-
|
|
82
|
+
const DEPENDENCY_SECTION_KEYS = Object.freeze(["allowed", "forbidden"]);
|
|
82
83
|
/** The keys a `projects.required[]` row may carry. */
|
|
83
|
-
|
|
84
|
+
const REQUIRED_PROJECT_KEYS = Object.freeze(["name", "tags", "decisionRef"]);
|
|
84
85
|
/** The keys a `projects.forbidden[]` row may carry. */
|
|
85
|
-
|
|
86
|
+
const FORBIDDEN_PROJECT_KEYS = Object.freeze(["name", "decisionRef"]);
|
|
86
87
|
/** The keys a `dependencies.allowed[]` / `dependencies.forbidden[]` row may carry. */
|
|
87
|
-
|
|
88
|
+
const DEPENDENCY_ROW_KEYS = Object.freeze(["source", "target", "decisionRef"]);
|
|
88
89
|
/** The keys a `forbiddenTags[]` row may carry. */
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
const CHANGE_INTENT_BASE_KEYS = Object.freeze(["commit"]);
|
|
61
61
|
|
|
62
62
|
/** The sub-keys the `projects` section may carry. */
|
|
63
|
-
|
|
63
|
+
const CHANGE_INTENT_PROJECT_SECTION_KEYS = Object.freeze(["add", "remove"]);
|
|
64
64
|
/** The sub-keys the `edges` section may carry. */
|
|
65
|
-
|
|
65
|
+
const CHANGE_INTENT_EDGE_SECTION_KEYS = Object.freeze(["add", "remove"]);
|
|
66
66
|
/** The keys an edge row may carry. */
|
|
67
|
-
|
|
67
|
+
const CHANGE_INTENT_EDGE_ROW_KEYS = Object.freeze(["from", "to"]);
|
|
68
68
|
/** The keys the `constraints` section may carry. */
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|