@ecoma-io/archkeep 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/analysis/csharp.mjs +3 -1
- package/src/analysis/dotnet/csproj.mjs +5 -1
- package/src/analysis/dotnet/namespaces.mjs +1 -0
- package/src/analysis/go.mjs +6 -0
- package/src/analysis/java.mjs +2 -0
- package/src/analysis/jvm/gradle.mjs +3 -1
- package/src/analysis/jvm/maven.mjs +6 -1
- package/src/analysis/jvm/packages.mjs +1 -0
- package/src/analysis/jvm/resolve.mjs +4 -2
- package/src/analysis/kotlin.mjs +2 -0
- package/src/analysis/markdown.mjs +341 -0
- package/src/analysis/python.mjs +8 -0
- package/src/analysis/rust.mjs +5 -1
- package/src/analysis/source-util.mjs +6 -5
- package/src/analysis/typescript.mjs +148 -0
- package/src/architecture-intent/model.mjs +13 -8
- package/src/architecture-intent/selectors.mjs +2 -1
- package/src/commands/change-intent.mjs +10 -9
- package/src/commands/change.mjs +17 -1
- package/src/commands/check.mjs +177 -17
- package/src/commands/completeness.mjs +7 -6
- package/src/commands/context-command.mjs +12 -20
- package/src/commands/coverage-acceptance.mjs +46 -0
- package/src/commands/coverage-verdict.mjs +12 -2
- package/src/commands/custom-rules.mjs +1 -0
- package/src/commands/delta-classify.mjs +3 -0
- package/src/commands/delta-snapshot.mjs +3 -6
- package/src/commands/delta.mjs +35 -21
- package/src/commands/drift.mjs +1 -1
- package/src/commands/evaluation-primitives.mjs +4 -4
- package/src/commands/evolution.mjs +2 -0
- package/src/commands/explain.mjs +19 -20
- package/src/commands/graph.mjs +44 -15
- package/src/commands/health.mjs +4 -0
- package/src/commands/history.mjs +2 -0
- package/src/commands/plan-context-command.mjs +9 -2
- package/src/commands/policy.mjs +8 -5
- package/src/commands/provenance.mjs +8 -2
- package/src/commands/scenario-evaluation.mjs +1 -1
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +170 -2
- package/src/custom-rules/host.mjs +3 -3
- package/src/errors.mjs +23 -1
- package/src/eslint-config.mjs +3 -5
- package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
- package/src/go-work.mjs +1 -1
- package/src/governance/adr-registry.mjs +6 -2
- 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/evolution-store.mjs +3 -2
- package/src/governance/fitness-registry.mjs +2 -3
- 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 -3
- package/src/governance/verdict.mjs +1 -0
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +6 -6
- 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/moon.mjs +5 -5
- package/src/providers/native/differential.fixtures.mjs +29 -11
- package/src/providers/native/index.mjs +2 -1
- package/src/providers/native/model.mjs +4 -0
- package/src/report/envelope-shape.mjs +2 -0
- package/src/report/sarif.mjs +21 -8
- package/src/report/snapshot-text.mjs +3 -3
- package/src/report/text.mjs +10 -2
- package/src/rules/index.mjs +30 -0
- 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/verdict.mjs +33 -2
- package/src/workspace.mjs +1 -0
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"type": "architecture-test",
|
|
21
21
|
"path": "src/conformance/boundary.test.mjs",
|
|
22
22
|
"assertion": "SHIPPED_PACKAGES allow-list prevents unapproved dependencies; specifiersIn() walk catches all imports including createRequire; no provider import found in core layers",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "e4e24ed9f36e31126249c7d079b3656d17ac78ee4f12033321467ffa6666f033"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"type": "source-evidence",
|
|
@@ -104,13 +104,13 @@
|
|
|
104
104
|
"type": "source-evidence",
|
|
105
105
|
"path": "src/commands/graph.mjs",
|
|
106
106
|
"assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped; SCHEMA_VERSION = 2",
|
|
107
|
-
"sha256": "
|
|
107
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"type": "source-evidence",
|
|
111
111
|
"path": "src/commands/graph.mjs",
|
|
112
112
|
"assertion": "computePolicyFingerprint produces SHA-256 of canonicalized policy",
|
|
113
|
-
"sha256": "
|
|
113
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
114
114
|
}
|
|
115
115
|
],
|
|
116
116
|
"status": "proven"
|
|
@@ -248,7 +248,7 @@
|
|
|
248
248
|
"type": "source-evidence",
|
|
249
249
|
"path": "src/commands/context-command.mjs",
|
|
250
250
|
"assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)",
|
|
251
|
-
"sha256": "
|
|
251
|
+
"sha256": "f906671ff4a5720d80ea5a3dab2779f1cfd3648fbc1e6a1f3a6d0e6efef21feb"
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
"type": "source-evidence",
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"type": "source-evidence",
|
|
287
287
|
"path": "src/commands/graph.mjs",
|
|
288
288
|
"assertion": "Plain string comparison throughout; never localeCompare",
|
|
289
|
-
"sha256": "
|
|
289
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
290
290
|
}
|
|
291
291
|
],
|
|
292
292
|
"status": "proven"
|
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
"type": "source-evidence",
|
|
313
313
|
"path": "src/providers/moon.mjs",
|
|
314
314
|
"assertion": "inferWorkspaceLayout returns null for partial layouts — same all-or-nothing contract as Nx and Native",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "2b874da56bda5bc6cfcfb4a984556ace0eafe3a6a98d964c78204bd96cc30e48"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"type": "behavioral-test",
|
|
@@ -536,6 +536,7 @@ function scanTemplate(src, i) {
|
|
|
536
536
|
* same line structure.
|
|
537
537
|
*/
|
|
538
538
|
export function maskNonCode(src) {
|
|
539
|
+
// used by its own test
|
|
539
540
|
/**
|
|
540
541
|
* Chunks of the result, joined once at the end. Code is copied through in
|
|
541
542
|
* RUNS rather than a token at a time — `plainFrom` is where the current
|
package/src/lsp/diagnostics.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { DIAGNOSTIC_SEVERITY, SERVER_INFO } from "./protocol.mjs";
|
|
|
38
38
|
* prints beside the message to say which tool spoke. Taken from the server's
|
|
39
39
|
* own identity so the two can never disagree.
|
|
40
40
|
*/
|
|
41
|
-
export const DIAGNOSTIC_SOURCE = SERVER_INFO.name;
|
|
41
|
+
export const DIAGNOSTIC_SOURCE = SERVER_INFO.name; // used by its own test
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The `code` on a diagnostic that reports the ABSENCE of a verdict rather than
|
|
@@ -46,7 +46,7 @@ export const DIAGNOSTIC_SOURCE = SERVER_INFO.name;
|
|
|
46
46
|
* against `MESSAGE_IDS` at load so a future upstream id cannot silently collide
|
|
47
47
|
* with it.
|
|
48
48
|
*/
|
|
49
|
-
export const ANALYSIS_FAILURE_CODE = "analysisFailure";
|
|
49
|
+
export const ANALYSIS_FAILURE_CODE = "analysisFailure"; // used by its own test
|
|
50
50
|
|
|
51
51
|
if (MESSAGE_IDS.includes(ANALYSIS_FAILURE_CODE)) {
|
|
52
52
|
throw new Error(
|
|
@@ -100,6 +100,7 @@ const QUOTES = new Set(['"', "'", "`"]);
|
|
|
100
100
|
* @returns {{start: {line: number, character: number}, end: {line: number, character: number}}}
|
|
101
101
|
*/
|
|
102
102
|
export function rangeAt(at, lines) {
|
|
103
|
+
// used by its own test
|
|
103
104
|
// A failure about the file as a whole carries no position (`contract.md`
|
|
104
105
|
// fixes it as an explicit `null`). It gets the first line, whole: a
|
|
105
106
|
// zero-width range at the origin renders as an invisible caret in most
|
package/src/lsp/protocol.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export const DIAGNOSTIC_SEVERITY = Object.freeze({
|
|
|
95
95
|
* more is not a supported conversation, and saying so beats waiting for bytes
|
|
96
96
|
* that will never arrive.
|
|
97
97
|
*/
|
|
98
|
-
export const MAX_CONTENT_LENGTH = 64 * 1024 * 1024;
|
|
98
|
+
export const MAX_CONTENT_LENGTH = 64 * 1024 * 1024; // used by its own test
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* LSP `MessageType`, as `window/showMessage` reports it.
|
|
@@ -211,5 +211,6 @@ export function uriToPath(uri) {
|
|
|
211
211
|
* @returns {string}
|
|
212
212
|
*/
|
|
213
213
|
export function pathToUri(path) {
|
|
214
|
+
// used by its own test
|
|
214
215
|
return pathToFileURL(path).href;
|
|
215
216
|
}
|
package/src/lsp/server.mjs
CHANGED
|
@@ -292,6 +292,7 @@ const POLYGLOT_GRAPH_MANIFESTS = Object.freeze([
|
|
|
292
292
|
* @returns {readonly string[]}
|
|
293
293
|
*/
|
|
294
294
|
export function watchedFilesFor(options, { unresolved = false } = {}) {
|
|
295
|
+
// used by its own test
|
|
295
296
|
return Object.freeze([
|
|
296
297
|
...(typeof options.boundaryConfig === "string" ? [options.boundaryConfig] : []),
|
|
297
298
|
...(unresolved
|
|
@@ -376,6 +377,7 @@ function markersAt(root) {
|
|
|
376
377
|
* convention chain can find while carrying files that need one.
|
|
377
378
|
*/
|
|
378
379
|
export function readWorkspaceOptions(root) {
|
|
380
|
+
// used by its own test
|
|
379
381
|
const { hasNx, hasNative } = markersAt(root);
|
|
380
382
|
if (hasNx && hasNative) {
|
|
381
383
|
throw new Error(
|
|
@@ -450,6 +452,7 @@ const WATCHER_REGISTRATION_ID = "archkeep/watched-files";
|
|
|
450
452
|
* text than the one on screen.
|
|
451
453
|
*/
|
|
452
454
|
export const SERVER_CAPABILITIES = Object.freeze({
|
|
455
|
+
// used by its own test
|
|
453
456
|
textDocumentSync: Object.freeze({
|
|
454
457
|
openClose: true,
|
|
455
458
|
change: TEXT_DOCUMENT_SYNC_KIND.full,
|
|
@@ -156,7 +156,7 @@ export { PROJECT_CONFIG_FILE, nodeTypeOf, buildDependencies };
|
|
|
156
156
|
* @returns {object} Whatever the JSON describes.
|
|
157
157
|
* @throws {Error} when neither parser can read it.
|
|
158
158
|
*/
|
|
159
|
-
|
|
159
|
+
const parseProjectJson = parseNxJson;
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Every file git considers part of the working tree, workspace-relative and
|
|
@@ -205,6 +205,7 @@ const directoryOf = (file) => {
|
|
|
205
205
|
* @returns {{projects: {name: string, root: string, config: object}[], skipped: {file: string, reason: string}[]}}
|
|
206
206
|
*/
|
|
207
207
|
export function discoverProjects({ files, readFile }) {
|
|
208
|
+
// used by its own test
|
|
208
209
|
const projects = [];
|
|
209
210
|
const skipped = [];
|
|
210
211
|
for (const file of files) {
|
|
@@ -263,6 +264,7 @@ export function discoverProjects({ files, readFile }) {
|
|
|
263
264
|
* publishes the collision through `indexGaps`.
|
|
264
265
|
*/
|
|
265
266
|
export function buildNodes(projects) {
|
|
267
|
+
// used by its own test
|
|
266
268
|
// Null-prototype for the same reason `../providers/native/graph.mjs` and
|
|
267
269
|
// `../providers/moon.mjs` use them: every key here is a project NAME, and
|
|
268
270
|
// project names come from a `project.json`'s own `name` field —
|
package/src/providers/moon.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync } from "node:fs";
|
|
|
31
31
|
import { delimiter, join, posix } from "node:path";
|
|
32
32
|
|
|
33
33
|
import { environmentForTree, runProcess } from "../process.mjs";
|
|
34
|
+
import { isEnoent } from "../errors.mjs";
|
|
34
35
|
import { buildDependencies } from "./native/graph.mjs";
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -905,16 +906,15 @@ function resolveMoonCli(workspaceRoot, { resolveMoon = () => "moon" } = {}) {
|
|
|
905
906
|
* `nxCli` guards against from the other direction, where only a
|
|
906
907
|
* `MODULE_NOT_FOUND` earns the "not installed" story.
|
|
907
908
|
*
|
|
908
|
-
* `../
|
|
909
|
-
*
|
|
910
|
-
*
|
|
909
|
+
* The shape test itself is `../errors.mjs`'s `isEnoent` — the name stays
|
|
910
|
+
* because what this file asks is not "is this ENOENT" but "does this failure
|
|
911
|
+
* carry an install action".
|
|
911
912
|
*
|
|
912
913
|
* @param {unknown} error
|
|
913
914
|
* @returns {boolean}
|
|
914
915
|
*/
|
|
915
916
|
function isMoonBinaryMissing(error) {
|
|
916
|
-
|
|
917
|
-
return thrown?.code === "ENOENT" || thrown?.cause?.code === "ENOENT";
|
|
917
|
+
return isEnoent(error);
|
|
918
918
|
}
|
|
919
919
|
|
|
920
920
|
/**
|
|
@@ -41,11 +41,13 @@ import { nativeProvider } from "./index.mjs";
|
|
|
41
41
|
* @param {Record<string, string>} files `{relativePath: text}`.
|
|
42
42
|
*/
|
|
43
43
|
export function writeAll(write, files) {
|
|
44
|
+
// used by its own test
|
|
44
45
|
for (const [path, text] of Object.entries(files)) write(path, text);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** @param {string} root */
|
|
48
49
|
export function writeIn(root) {
|
|
50
|
+
// used by its own test
|
|
49
51
|
return (relativePath, text) => {
|
|
50
52
|
mkdirSync(join(root, relativePath, ".."), { recursive: true });
|
|
51
53
|
writeFileSync(join(root, relativePath), text);
|
|
@@ -53,7 +55,7 @@ export function writeIn(root) {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** @param {string} root */
|
|
56
|
-
|
|
58
|
+
function readFileFrom(root) {
|
|
57
59
|
return (path) => {
|
|
58
60
|
try {
|
|
59
61
|
return readFileSync(join(root, path), "utf8");
|
|
@@ -76,6 +78,7 @@ function analyze(root, graph, files, tsConfig) {
|
|
|
76
78
|
|
|
77
79
|
/** `{name → {root, type, tags}}`, comparable whichever provider produced it. */
|
|
78
80
|
export const nodeShape = (nodes) =>
|
|
81
|
+
// used by its own test
|
|
79
82
|
Object.fromEntries(
|
|
80
83
|
Object.entries(nodes).map(([name, node]) => [
|
|
81
84
|
name,
|
|
@@ -105,13 +108,14 @@ export const nodeShape = (nodes) =>
|
|
|
105
108
|
* canonicalisation consistent with the code under test rather than a weaker
|
|
106
109
|
* copy of it. */
|
|
107
110
|
export const dependencyShape = (dependencies) =>
|
|
111
|
+
// used by its own test
|
|
108
112
|
Object.values(dependencies)
|
|
109
113
|
.flat()
|
|
110
114
|
.map(({ source, target, type }) => JSON.stringify([source, target, type]))
|
|
111
115
|
.sort();
|
|
112
116
|
|
|
113
117
|
/** `["messageId sourceFile:line:column", ...]`, sorted the same way. */
|
|
114
|
-
|
|
118
|
+
const verdictShape = (violations) =>
|
|
115
119
|
violations.map((v) => `${v.messageId} ${v.sourceFile}:${v.line}:${v.column}`).sort();
|
|
116
120
|
|
|
117
121
|
/**
|
|
@@ -128,6 +132,7 @@ export const verdictShape = (violations) =>
|
|
|
128
132
|
* @type {typeof runProcess}
|
|
129
133
|
*/
|
|
130
134
|
export function runNxGraphSpawn(file, args, cwd) {
|
|
135
|
+
// used by its own test
|
|
131
136
|
const previous = process.env.NX_DAEMON;
|
|
132
137
|
process.env.NX_DAEMON = "false";
|
|
133
138
|
try {
|
|
@@ -157,6 +162,7 @@ export function runNxGraphSpawn(file, args, cwd) {
|
|
|
157
162
|
* @throws {Error} naming every failing file, when either list is non-empty.
|
|
158
163
|
*/
|
|
159
164
|
export function assertNoAnalysisFailures(nxFailures, nativeFailures) {
|
|
165
|
+
// used by its own test
|
|
160
166
|
const named = [
|
|
161
167
|
...nxFailures.map((f) => `nx:${f.sourceFile} (${f.reason})`),
|
|
162
168
|
...nativeFailures.map((f) => `native:${f.sourceFile} (${f.reason})`),
|
|
@@ -188,6 +194,7 @@ export function assertNoAnalysisFailures(nxFailures, nativeFailures) {
|
|
|
188
194
|
* inherits the `NX_DAEMON` handling rather than choosing between the two.
|
|
189
195
|
*/
|
|
190
196
|
export async function runBothProviders({
|
|
197
|
+
// used by its own test
|
|
191
198
|
nxRoot,
|
|
192
199
|
nxFiles,
|
|
193
200
|
nativeRoot,
|
|
@@ -280,6 +287,7 @@ export async function runBothProviders({
|
|
|
280
287
|
* @returns {{kind: string, subject: string, field: string, nx: unknown, native: unknown}[]}
|
|
281
288
|
*/
|
|
282
289
|
export function diffGraphs(nxSide, nativeSide) {
|
|
290
|
+
// used by its own test
|
|
283
291
|
const rows = [];
|
|
284
292
|
|
|
285
293
|
const nxNames = new Set(Object.keys(nxSide.nodes));
|
|
@@ -417,6 +425,7 @@ export function diffGraphs(nxSide, nativeSide) {
|
|
|
417
425
|
* detection) are scoped to one fixture pair and cannot silently absorb a
|
|
418
426
|
* disagreement in another. */
|
|
419
427
|
export function namespaced(rows, label) {
|
|
428
|
+
// used by its own test
|
|
420
429
|
return rows.map((row) => ({ ...row, subject: `${label}:${row.subject}` }));
|
|
421
430
|
}
|
|
422
431
|
|
|
@@ -458,7 +467,7 @@ export function namespaced(rows, label) {
|
|
|
458
467
|
* `classifyDifferences`, not a search for every place `"native-only"` was
|
|
459
468
|
* spelled as a bare string.
|
|
460
469
|
*/
|
|
461
|
-
|
|
470
|
+
const LEDGER_DIRECTIONS = Object.freeze(/** @type {const} */ (["native-only"]));
|
|
462
471
|
|
|
463
472
|
/**
|
|
464
473
|
* Every entry here is a **difference native reports and Nx does not** — the
|
|
@@ -479,7 +488,7 @@ export const LEDGER_DIRECTIONS = Object.freeze(/** @type {const} */ (["native-on
|
|
|
479
488
|
*
|
|
480
489
|
* @type {readonly LedgerRow[]}
|
|
481
490
|
*/
|
|
482
|
-
export const LEDGER = Object.freeze([]);
|
|
491
|
+
export const LEDGER = Object.freeze([]); // used by its own test
|
|
483
492
|
|
|
484
493
|
/**
|
|
485
494
|
* Every fixture-pair label this file actually runs a comparison over — the
|
|
@@ -487,7 +496,7 @@ export const LEDGER = Object.freeze([]);
|
|
|
487
496
|
* list every `assertPairAgrees`/`pairProblems` call site draws its `label`
|
|
488
497
|
* argument from, so the two can never drift into naming different pairs.
|
|
489
498
|
*/
|
|
490
|
-
export const PAIR_LABELS = Object.freeze(["simple", "composite", "layout"]);
|
|
499
|
+
export const PAIR_LABELS = Object.freeze(["simple", "composite", "layout"]); // used by its own test
|
|
491
500
|
|
|
492
501
|
/**
|
|
493
502
|
* The direction a `diffGraphs` row itself claims, in `LedgerRow.direction`'s
|
|
@@ -560,6 +569,7 @@ function differenceDirection(difference) {
|
|
|
560
569
|
* difference no ledger row may ever cover.
|
|
561
570
|
*/
|
|
562
571
|
export function classifyDifferences(differences, ledger) {
|
|
572
|
+
// used by its own test
|
|
563
573
|
for (const row of ledger) {
|
|
564
574
|
if (!row.reason?.trim()) {
|
|
565
575
|
throw new Error(`ledger row for "${row.subject}"/"${row.field}" has an empty reason`);
|
|
@@ -632,6 +642,7 @@ export function classifyDifferences(differences, ledger) {
|
|
|
632
642
|
* @returns {string[]}
|
|
633
643
|
*/
|
|
634
644
|
export function emptyVerdictBreaches(label, counts) {
|
|
645
|
+
// used by its own test
|
|
635
646
|
const breaches = [];
|
|
636
647
|
for (const [engine, count] of Object.entries(counts)) {
|
|
637
648
|
if (count === 0) {
|
|
@@ -659,6 +670,7 @@ export function emptyVerdictBreaches(label, counts) {
|
|
|
659
670
|
* @returns {string[]}
|
|
660
671
|
*/
|
|
661
672
|
export function perMessageBreaches(label, nxViolations, nativeViolations) {
|
|
673
|
+
// used by its own test
|
|
662
674
|
/** @param {{messageId: string}[]} violations @returns {Map<string, number>} */
|
|
663
675
|
const countBy = (violations) => {
|
|
664
676
|
const counts = new Map();
|
|
@@ -696,6 +708,7 @@ export function perMessageBreaches(label, nxViolations, nativeViolations) {
|
|
|
696
708
|
* @returns {string[]}
|
|
697
709
|
*/
|
|
698
710
|
export function pairProblems(label, result, ledger = LEDGER) {
|
|
711
|
+
// used by its own test
|
|
699
712
|
const problems = [
|
|
700
713
|
...emptyVerdictBreaches(label, {
|
|
701
714
|
nx: result.nx.violations.length,
|
|
@@ -734,6 +747,7 @@ export function pairProblems(label, result, ledger = LEDGER) {
|
|
|
734
747
|
* @param {readonly LedgerRow[]} [ledger]
|
|
735
748
|
*/
|
|
736
749
|
export function assertPairAgrees(label, result, ledger = LEDGER) {
|
|
750
|
+
// used by its own test
|
|
737
751
|
const problems = pairProblems(label, result, ledger);
|
|
738
752
|
if (problems.length > 0) {
|
|
739
753
|
throw new Error(`${label}: provider differential disagreement:\n ${problems.join("\n ")}`);
|
|
@@ -758,6 +772,7 @@ export function assertPairAgrees(label, result, ledger = LEDGER) {
|
|
|
758
772
|
* @returns {LedgerRow[]}
|
|
759
773
|
*/
|
|
760
774
|
export function unknownLabelRows(ledger, knownLabels) {
|
|
775
|
+
// used by its own test
|
|
761
776
|
return ledger.filter((row) => !knownLabels.includes(row.subject.split(":")[0]));
|
|
762
777
|
}
|
|
763
778
|
|
|
@@ -773,7 +788,7 @@ export function unknownLabelRows(ledger, knownLabels) {
|
|
|
773
788
|
// staying importable and spawn-free at import time for exactly that reuse,
|
|
774
789
|
// and a second copy here is the drift `../../../../../AGENTS.md`'s "never
|
|
775
790
|
// state a rule twice" rule exists to catch.
|
|
776
|
-
export const SIMPLE_BOUNDARY_CONFIG = `export const depConstraints = [
|
|
791
|
+
export const SIMPLE_BOUNDARY_CONFIG = `export const depConstraints = [ // used by its own test
|
|
777
792
|
{ sourceTag: "layer:domain", onlyDependOnLibsWithTags: ["layer:domain"] },
|
|
778
793
|
{ sourceTag: "layer:adapter", onlyDependOnLibsWithTags: ["layer:domain", "layer:adapter"] },
|
|
779
794
|
];
|
|
@@ -794,6 +809,7 @@ export const moduleBoundaryOptions = {
|
|
|
794
809
|
// the wrong way (`domain` reaching into `adapter`). Exported for the same
|
|
795
810
|
// reuse reason as `SIMPLE_BOUNDARY_CONFIG` above.
|
|
796
811
|
export const SIMPLE_GO_FILES = {
|
|
812
|
+
// used by its own test
|
|
797
813
|
"libs/domain/go.mod": "module example.com/domain\n\ngo 1.24\n",
|
|
798
814
|
"libs/adapter/go.mod": "module example.com/adapter\n\ngo 1.24\n",
|
|
799
815
|
"libs/adapter/adapter.go": 'package adapter\n\nvar Name = "adapter"\n',
|
|
@@ -819,6 +835,7 @@ var _ = adapter.Name
|
|
|
819
835
|
* @returns {string[]}
|
|
820
836
|
*/
|
|
821
837
|
export function buildSimpleNxTree(root, { boundaryConfig = "module-boundaries.config.mjs" } = {}) {
|
|
838
|
+
// used by its own test
|
|
822
839
|
const write = writeIn(root);
|
|
823
840
|
write(
|
|
824
841
|
"nx.json",
|
|
@@ -851,7 +868,7 @@ export function buildSimpleNxTree(root, { boundaryConfig = "module-boundaries.co
|
|
|
851
868
|
* @param {{boundaryConfig?: string}} [options]
|
|
852
869
|
* @returns {string[]}
|
|
853
870
|
*/
|
|
854
|
-
export function buildSimpleNativeTree(
|
|
871
|
+
export function buildSimpleNativeTree( // used by its own test
|
|
855
872
|
root,
|
|
856
873
|
{ boundaryConfig = "module-boundaries.config.mjs" } = {},
|
|
857
874
|
) {
|
|
@@ -950,7 +967,7 @@ var _ = parent.Name
|
|
|
950
967
|
* @param {{boundaryConfig?: string}} [options]
|
|
951
968
|
* @returns {string[]}
|
|
952
969
|
*/
|
|
953
|
-
export function buildCompositeNxTree(
|
|
970
|
+
export function buildCompositeNxTree( // used by its own test
|
|
954
971
|
root,
|
|
955
972
|
{ boundaryConfig = "module-boundaries.config.mjs" } = {},
|
|
956
973
|
) {
|
|
@@ -1029,7 +1046,7 @@ export function buildCompositeNxTree(
|
|
|
1029
1046
|
* @param {{boundaryConfig?: string}} [options]
|
|
1030
1047
|
* @returns {string[]}
|
|
1031
1048
|
*/
|
|
1032
|
-
export function buildCompositeNativeTree(
|
|
1049
|
+
export function buildCompositeNativeTree( // used by its own test
|
|
1033
1050
|
root,
|
|
1034
1051
|
{ boundaryConfig = "module-boundaries.config.mjs" } = {},
|
|
1035
1052
|
) {
|
|
@@ -1175,7 +1192,7 @@ export function buildCompositeNativeTree(
|
|
|
1175
1192
|
const LAYOUT_BOUNDARY_CONFIG = `export const depConstraints = [
|
|
1176
1193
|
{ sourceTag: "layer:thing", onlyDependOnLibsWithTags: ["layer:thing"] },
|
|
1177
1194
|
];
|
|
1178
|
-
export const moduleBoundaryOptions = {
|
|
1195
|
+
export const moduleBoundaryOptions = { // used by its own test
|
|
1179
1196
|
allow: [],
|
|
1180
1197
|
buildTargets: ["build"],
|
|
1181
1198
|
enforceBuildableLibDependency: false,
|
|
@@ -1227,6 +1244,7 @@ var _ = blocked.Name
|
|
|
1227
1244
|
* @returns {string[]}
|
|
1228
1245
|
*/
|
|
1229
1246
|
export function buildLayoutNxTree(root, { boundaryConfig = "module-boundaries.config.mjs" } = {}) {
|
|
1247
|
+
// used by its own test
|
|
1230
1248
|
const write = writeIn(root);
|
|
1231
1249
|
write(
|
|
1232
1250
|
"nx.json",
|
|
@@ -1262,7 +1280,7 @@ export function buildLayoutNxTree(root, { boundaryConfig = "module-boundaries.co
|
|
|
1262
1280
|
* @param {{boundaryConfig?: string}} [options]
|
|
1263
1281
|
* @returns {string[]}
|
|
1264
1282
|
*/
|
|
1265
|
-
export function buildLayoutNativeTree(
|
|
1283
|
+
export function buildLayoutNativeTree( // used by its own test
|
|
1266
1284
|
root,
|
|
1267
1285
|
{ boundaryConfig = "module-boundaries.config.mjs" } = {},
|
|
1268
1286
|
) {
|
|
@@ -61,6 +61,7 @@ function projectOfFactory(projects) {
|
|
|
61
61
|
* stale `coverage.exempt` row.
|
|
62
62
|
*/
|
|
63
63
|
export function discover({ root, files, readFile }) {
|
|
64
|
+
// used by its own test
|
|
64
65
|
const model = loadNativeModel(root, { readFile });
|
|
65
66
|
const { projects, failures: discoveryFailures } = discoverNativeProjects({
|
|
66
67
|
root,
|
|
@@ -128,7 +129,7 @@ export function discover({ root, files, readFile }) {
|
|
|
128
129
|
* @param {{discovered: ReturnType<typeof discover>, importSites: object[]}} args
|
|
129
130
|
* @returns {{nodes: Record<string, object>, dependencies: Record<string, object[]>, workspaceLayout?: {appsDir: string, libsDir: string}, exemptedFiles?: string[]}}
|
|
130
131
|
*/
|
|
131
|
-
|
|
132
|
+
function buildGraph({ discovered, importSites }) {
|
|
132
133
|
return buildNativeGraph({
|
|
133
134
|
projects: discovered.projects,
|
|
134
135
|
importSites,
|
|
@@ -191,6 +191,7 @@ function parseArchkeepJson(text) {
|
|
|
191
191
|
* for a tree that kept Nx-shaped project boundaries without keeping Nx.
|
|
192
192
|
*/
|
|
193
193
|
export const DEFAULT_MANIFEST_NAMES = Object.freeze([
|
|
194
|
+
// used by its own test
|
|
194
195
|
"project.json",
|
|
195
196
|
"package.json",
|
|
196
197
|
"go.mod",
|
|
@@ -255,6 +256,7 @@ const PROJECT_TYPES = ["app", "lib", "e2e"];
|
|
|
255
256
|
* @see DEFAULT_MANIFEST_NAMES
|
|
256
257
|
*/
|
|
257
258
|
export const DEFAULT_INFER_EXCLUDE = Object.freeze([
|
|
259
|
+
// used by its own test
|
|
258
260
|
"**/docs/**",
|
|
259
261
|
"**/fixtures/**",
|
|
260
262
|
"**/__fixtures__/**",
|
|
@@ -549,6 +551,7 @@ const TOP_LEVEL_KEYS = [
|
|
|
549
551
|
* @returns {string[]}
|
|
550
552
|
*/
|
|
551
553
|
export function findNativeModelViolations(raw) {
|
|
554
|
+
// used by its own test
|
|
552
555
|
if (!isPlainObject(raw)) return [`archkeep.json: expected an object, got ${describe(raw)}`];
|
|
553
556
|
|
|
554
557
|
const violations = [];
|
|
@@ -699,6 +702,7 @@ export function findNativeModelViolations(raw) {
|
|
|
699
702
|
* @returns {object} `NativeModel` — see `./index.mjs`.
|
|
700
703
|
*/
|
|
701
704
|
export function normalizeNativeModel(raw) {
|
|
705
|
+
// used by its own test
|
|
702
706
|
// `raw.projects` may be absent — `findNativeModelViolations` above validates
|
|
703
707
|
// that shape the same way it validates `projects: {}`, so this has to read
|
|
704
708
|
// it back the same way rather than assume the key is always present.
|
|
@@ -89,6 +89,7 @@ function typeName(value) {
|
|
|
89
89
|
* JSON document can carry.
|
|
90
90
|
*/
|
|
91
91
|
export function envelopeFieldPaths(envelope) {
|
|
92
|
+
// used by its own test
|
|
92
93
|
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
93
94
|
throw new Error(
|
|
94
95
|
`archkeep: refusing to build an envelope roster over ${envelope === null ? "null" : `a ${Array.isArray(envelope) ? "array" : typeof envelope}`} ` +
|
|
@@ -152,6 +153,7 @@ function walk(value, path, byPath) {
|
|
|
152
153
|
* @returns {{added: string[], removed: string[]}}
|
|
153
154
|
*/
|
|
154
155
|
export function compareFieldPaths(recorded, observed) {
|
|
156
|
+
// used by its own test
|
|
155
157
|
const recordedSet = new Set(recorded);
|
|
156
158
|
const observedSet = new Set(observed);
|
|
157
159
|
return {
|
package/src/report/sarif.mjs
CHANGED
|
@@ -62,15 +62,15 @@ import { TSCONFIG_PATHS_MESSAGE_IDS, TSCONFIG_PATHS_MESSAGES } from "../tsconfig
|
|
|
62
62
|
import { formatConstraint } from "./text.mjs";
|
|
63
63
|
|
|
64
64
|
/** The schema every consumer of this file validates against. */
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
66
|
+
const SARIF_VERSION = "2.1.0";
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* The single rule id every failing fitness function's result shares — see
|
|
70
70
|
* `sarifRules()`'s own comment for why one id stands in for the whole
|
|
71
71
|
* open-ended, workspace-declared set.
|
|
72
72
|
*/
|
|
73
|
-
export const FITNESS_FAILED_RULE_ID = "fitnessFunctionFailed";
|
|
73
|
+
export const FITNESS_FAILED_RULE_ID = "fitnessFunctionFailed"; // used by its own test
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* A workspace-relative path as a URI reference: each segment percent-encoded,
|
|
@@ -106,6 +106,7 @@ export const FITNESS_FAILED_RULE_ID = "fitnessFunctionFailed";
|
|
|
106
106
|
* @returns {string}
|
|
107
107
|
*/
|
|
108
108
|
export function toUriReference(path) {
|
|
109
|
+
// used by its own test
|
|
109
110
|
return path.split("/").map(encodeURIComponent).join("/");
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -139,6 +140,7 @@ export function toUriReference(path) {
|
|
|
139
140
|
* @returns {object[]}
|
|
140
141
|
*/
|
|
141
142
|
export function sarifRules(customCatalogue = []) {
|
|
143
|
+
// used by its own test
|
|
142
144
|
return [
|
|
143
145
|
...MESSAGE_IDS.map((id) => ({
|
|
144
146
|
id,
|
|
@@ -231,6 +233,7 @@ const CUSTOM_RULE_INDEX_BASE =
|
|
|
231
233
|
* @returns {object}
|
|
232
234
|
*/
|
|
233
235
|
export function sarifCustomRuleResult(finding, ruleIndex) {
|
|
236
|
+
// used by its own test
|
|
234
237
|
const physicalLocation =
|
|
235
238
|
finding.sourceFile === undefined
|
|
236
239
|
? null
|
|
@@ -283,6 +286,7 @@ export function sarifCustomRuleResult(finding, ruleIndex) {
|
|
|
283
286
|
* @returns {object}
|
|
284
287
|
*/
|
|
285
288
|
export function sarifCustomRuleNotification(decision) {
|
|
289
|
+
// used by its own test
|
|
286
290
|
const posture =
|
|
287
291
|
decision.verdict === "not_applicable" ? "did not apply to this run" : "could not be judged";
|
|
288
292
|
return {
|
|
@@ -305,6 +309,7 @@ export function sarifCustomRuleNotification(decision) {
|
|
|
305
309
|
* @returns {object}
|
|
306
310
|
*/
|
|
307
311
|
export function sarifResult(violation) {
|
|
312
|
+
// used by its own test
|
|
308
313
|
const detail =
|
|
309
314
|
`Import ${JSON.stringify(violation.specifier)} (${violation.kind}) ` +
|
|
310
315
|
`from ${violation.sourceProject ?? "(no project)"} ` +
|
|
@@ -364,7 +369,7 @@ export function sarifResult(violation) {
|
|
|
364
369
|
* @param {object} finding A finding from `../go-work.mjs` `compareGoWork`.
|
|
365
370
|
* @returns {object}
|
|
366
371
|
*/
|
|
367
|
-
|
|
372
|
+
function sarifGoWorkResult(finding) {
|
|
368
373
|
const physicalLocation = { artifactLocation: { uri: toUriReference(finding.file) } };
|
|
369
374
|
if (finding.line !== null) {
|
|
370
375
|
physicalLocation.region = { startLine: finding.line, startColumn: finding.column };
|
|
@@ -395,7 +400,7 @@ export function sarifGoWorkResult(finding) {
|
|
|
395
400
|
* @param {object} finding A finding from `../tsconfig-paths.mjs`.
|
|
396
401
|
* @returns {object}
|
|
397
402
|
*/
|
|
398
|
-
|
|
403
|
+
function sarifTsconfigPathsResult(finding) {
|
|
399
404
|
return {
|
|
400
405
|
ruleId: finding.messageId,
|
|
401
406
|
ruleIndex:
|
|
@@ -444,7 +449,7 @@ export function sarifTsconfigPathsResult(finding) {
|
|
|
444
449
|
* `declaredEdgeViolationsForCheck`, extended with `file` — workspace-relative.
|
|
445
450
|
* @returns {object}
|
|
446
451
|
*/
|
|
447
|
-
|
|
452
|
+
function sarifDeclaredEdgeResult(finding) {
|
|
448
453
|
return {
|
|
449
454
|
ruleId: finding.messageId,
|
|
450
455
|
ruleIndex: MESSAGE_IDS.indexOf(finding.messageId),
|
|
@@ -472,6 +477,7 @@ export function sarifDeclaredEdgeResult(finding) {
|
|
|
472
477
|
* @returns {object}
|
|
473
478
|
*/
|
|
474
479
|
export function sarifIntentResult(finding) {
|
|
480
|
+
// used by its own test
|
|
475
481
|
return {
|
|
476
482
|
ruleId: finding.rule,
|
|
477
483
|
ruleIndex:
|
|
@@ -516,6 +522,7 @@ export function sarifIntentResult(finding) {
|
|
|
516
522
|
* @returns {object}
|
|
517
523
|
*/
|
|
518
524
|
export function sarifFitnessResult(decision, policySource = null) {
|
|
525
|
+
// used by its own test
|
|
519
526
|
const result = {
|
|
520
527
|
ruleId: FITNESS_FAILED_RULE_ID,
|
|
521
528
|
ruleIndex:
|
|
@@ -548,6 +555,7 @@ export function sarifFitnessResult(decision, policySource = null) {
|
|
|
548
555
|
* @returns {object}
|
|
549
556
|
*/
|
|
550
557
|
export function sarifFitnessNotification(decision) {
|
|
558
|
+
// used by its own test
|
|
551
559
|
return {
|
|
552
560
|
level: "warning",
|
|
553
561
|
message: {
|
|
@@ -567,7 +575,7 @@ export function sarifFitnessNotification(decision) {
|
|
|
567
575
|
* @param {object} failure An `AnalysisFailure`.
|
|
568
576
|
* @returns {object}
|
|
569
577
|
*/
|
|
570
|
-
|
|
578
|
+
function sarifNotification(failure) {
|
|
571
579
|
const physicalLocation = { artifactLocation: { uri: toUriReference(failure.sourceFile) } };
|
|
572
580
|
if (failure.line !== null) {
|
|
573
581
|
physicalLocation.region = { startLine: failure.line, startColumn: failure.column };
|
|
@@ -601,6 +609,7 @@ export function sarifNotification(failure) {
|
|
|
601
609
|
* @returns {object}
|
|
602
610
|
*/
|
|
603
611
|
export function sarifIntentNotification(entry) {
|
|
612
|
+
// used by its own test
|
|
604
613
|
return {
|
|
605
614
|
level: "warning",
|
|
606
615
|
message: {
|
|
@@ -641,6 +650,7 @@ const UNOWNED_SAMPLE_LIMIT = 10;
|
|
|
641
650
|
* @returns {object}
|
|
642
651
|
*/
|
|
643
652
|
export function sarifCoverageGapNotification(gap) {
|
|
653
|
+
// used by its own test
|
|
644
654
|
const manifests = gap.manifests ?? [];
|
|
645
655
|
const found = manifests.length > 0 ? `: ${manifests.join(", ")}` : "";
|
|
646
656
|
if (gap.kind === "unregistered-plugin") {
|
|
@@ -760,7 +770,7 @@ export function sarifCoverageGapNotification(gap) {
|
|
|
760
770
|
* @param {string} decisionRef The cited value, exactly as the row spelled it.
|
|
761
771
|
* @returns {object}
|
|
762
772
|
*/
|
|
763
|
-
|
|
773
|
+
function sarifDecisionRefNotification(decisionRef) {
|
|
764
774
|
return {
|
|
765
775
|
level: "warning",
|
|
766
776
|
message: {
|
|
@@ -853,6 +863,7 @@ export function sarifDecisionRefNotification(decisionRef) {
|
|
|
853
863
|
* @returns {object} A SARIF 2.1.0 log, ready to `JSON.stringify`.
|
|
854
864
|
*/
|
|
855
865
|
export function buildSarifLog({
|
|
866
|
+
// used by its own test
|
|
856
867
|
violations,
|
|
857
868
|
failures,
|
|
858
869
|
goWork,
|
|
@@ -1016,6 +1027,7 @@ export function buildSarifLog({
|
|
|
1016
1027
|
* @returns {object}
|
|
1017
1028
|
*/
|
|
1018
1029
|
export function sarifDeltaResult(entry, site) {
|
|
1030
|
+
// used by its own test
|
|
1019
1031
|
const target = entry.targetIsSpecifier
|
|
1020
1032
|
? `specifier ${JSON.stringify(entry.target)}`
|
|
1021
1033
|
: entry.target;
|
|
@@ -1102,6 +1114,7 @@ export function sarifDeltaResult(entry, site) {
|
|
|
1102
1114
|
* @returns {object} A SARIF 2.1.0 log, ready to `JSON.stringify`.
|
|
1103
1115
|
*/
|
|
1104
1116
|
export function buildDeltaSarifLog({ delta, coverage, customCatalogue = [] }) {
|
|
1117
|
+
// used by its own test
|
|
1105
1118
|
const customRuleIndex = new Map(
|
|
1106
1119
|
customCatalogue.map((entry, index) => [entry.ruleId, CUSTOM_RULE_INDEX_BASE + index]),
|
|
1107
1120
|
);
|
|
@@ -35,7 +35,7 @@ export function sanitize(text) {
|
|
|
35
35
|
* @param {{name: string, root: string, tags: string[]}} project
|
|
36
36
|
* @returns {string}
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
function formatProject(project) {
|
|
39
39
|
const tags =
|
|
40
40
|
project.tags.length > 0 ? ` [${project.tags.map((t) => sanitize(t)).join(", ")}]` : "";
|
|
41
41
|
return ` ${sanitize(project.name)} ${sanitize(project.root)}${tags}`;
|
|
@@ -47,7 +47,7 @@ export function formatProject(project) {
|
|
|
47
47
|
* @param {{source: string, target: string, type: string}} edge
|
|
48
48
|
* @returns {string}
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
function formatEdge(edge) {
|
|
51
51
|
return ` ${sanitize(edge.source)} → ${sanitize(edge.target)} (${sanitize(edge.type)})`;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -57,7 +57,7 @@ export function formatEdge(edge) {
|
|
|
57
57
|
* @param {{field: string, baseline: *, head: *}} change
|
|
58
58
|
* @returns {string}
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
function formatChange(change) {
|
|
61
61
|
const formatValue = (v) => {
|
|
62
62
|
if (Array.isArray(v)) return v.length > 0 ? v.map((x) => sanitize(x)).join(", ") : "(none)";
|
|
63
63
|
if (v === null || v === undefined) return "(none)";
|