@ecoma-io/archkeep 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/analysis/csharp.mjs +3 -1
- package/src/analysis/dotnet/csproj.mjs +5 -1
- package/src/analysis/dotnet/namespaces.mjs +1 -0
- package/src/analysis/go.mjs +6 -0
- package/src/analysis/java.mjs +2 -0
- package/src/analysis/jvm/gradle.mjs +3 -1
- package/src/analysis/jvm/maven.mjs +6 -1
- package/src/analysis/jvm/packages.mjs +1 -0
- package/src/analysis/jvm/resolve.mjs +4 -2
- package/src/analysis/kotlin.mjs +2 -0
- package/src/analysis/markdown.mjs +1 -0
- package/src/analysis/python.mjs +8 -0
- package/src/analysis/rust.mjs +5 -1
- package/src/analysis/source-util.mjs +1 -1
- package/src/analysis/typescript.mjs +2 -0
- package/src/architecture-intent/model.mjs +11 -7
- package/src/architecture-intent/selectors.mjs +2 -1
- package/src/commands/change-intent.mjs +10 -9
- package/src/commands/change.mjs +17 -1
- package/src/commands/check.mjs +11 -9
- package/src/commands/completeness.mjs +7 -6
- package/src/commands/coverage-acceptance.mjs +46 -0
- package/src/commands/custom-rules.mjs +1 -0
- package/src/commands/delta-classify.mjs +3 -0
- package/src/commands/delta-snapshot.mjs +2 -1
- package/src/commands/delta.mjs +35 -21
- package/src/commands/drift.mjs +1 -1
- package/src/commands/evaluation-primitives.mjs +4 -4
- package/src/commands/evolution.mjs +2 -0
- package/src/commands/explain.mjs +2 -0
- package/src/commands/graph.mjs +39 -17
- package/src/commands/history.mjs +2 -0
- package/src/commands/plan-context-command.mjs +4 -1
- package/src/commands/policy.mjs +5 -2
- package/src/commands/scenario-evaluation.mjs +1 -1
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +1 -1
- package/src/custom-rules/host.mjs +3 -3
- package/src/eslint-config.mjs +1 -0
- package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
- package/src/go-work.mjs +1 -1
- package/src/governance/adr-registry.mjs +4 -1
- package/src/governance/debt-ledger.mjs +1 -1
- package/src/governance/decision-fitness.mjs +2 -0
- package/src/governance/decision-graph.mjs +1 -0
- package/src/governance/discovery-proposal.mjs +8 -2
- package/src/governance/evolution-event.mjs +42 -0
- package/src/governance/fitness-registry.mjs +2 -0
- package/src/governance/preset-fingerprints.json +14 -14
- package/src/governance/profile-registry.mjs +22 -3
- package/src/governance/provenance-record.mjs +4 -1
- package/src/governance/reconcile-score.mjs +4 -0
- package/src/governance/row-schema.mjs +1 -0
- package/src/governance/verdict.mjs +1 -0
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +3 -3
- package/src/intent/mask-non-code.mjs +1 -0
- package/src/lsp/diagnostics.mjs +3 -2
- package/src/lsp/protocol.mjs +2 -1
- package/src/lsp/server.mjs +3 -0
- package/src/lsp/workspace-index.mjs +3 -1
- package/src/providers/native/differential.fixtures.mjs +29 -11
- package/src/providers/native/index.mjs +2 -1
- package/src/providers/native/model.mjs +4 -0
- package/src/report/envelope-shape.mjs +2 -0
- package/src/report/sarif.mjs +21 -8
- package/src/report/snapshot-text.mjs +3 -3
- package/src/report/text.mjs +10 -2
- package/src/rules/match.mjs +7 -5
- package/src/rules/specifiers.mjs +2 -0
- package/src/rules/tags.mjs +3 -2
- package/src/rules/topology.mjs +6 -1
- package/src/workspace.mjs +1 -0
|
@@ -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)";
|
package/src/report/text.mjs
CHANGED
|
@@ -94,6 +94,7 @@ function formatEdge(violation) {
|
|
|
94
94
|
* @returns {string}
|
|
95
95
|
*/
|
|
96
96
|
export function formatViolation(violation, unresolvedDecisionRefs) {
|
|
97
|
+
// used by its own test
|
|
97
98
|
const message = violation.message
|
|
98
99
|
.split("\n")
|
|
99
100
|
.map((line) => (line === "" ? "" : `${CONTINUED}${line}`))
|
|
@@ -159,6 +160,7 @@ const formatFailure = (failure) =>
|
|
|
159
160
|
* @returns {string}
|
|
160
161
|
*/
|
|
161
162
|
export function formatFailures(failures) {
|
|
163
|
+
// used by its own test
|
|
162
164
|
if (failures.length === 0) return "";
|
|
163
165
|
const unchecked = failures.filter(isWholeFileFailure);
|
|
164
166
|
const blind = failures.filter((failure) => !isWholeFileFailure(failure));
|
|
@@ -236,6 +238,7 @@ export function formatFailures(failures) {
|
|
|
236
238
|
* @returns {string} Empty exactly when there is no go.work verdict to render.
|
|
237
239
|
*/
|
|
238
240
|
export function formatGoWork(goWork) {
|
|
241
|
+
// used by its own test
|
|
239
242
|
if (goWork == null) return "";
|
|
240
243
|
const { findings, moduleProjects } = goWork;
|
|
241
244
|
const modules = `${moduleProjects} Go module project${moduleProjects === 1 ? "" : "s"}`;
|
|
@@ -291,6 +294,7 @@ export function formatGoWork(goWork) {
|
|
|
291
294
|
* @returns {string} Empty exactly when there is no declared-edge verdict to render.
|
|
292
295
|
*/
|
|
293
296
|
export function formatDeclaredEdges(declaredEdges, unresolvedDecisionRefs) {
|
|
297
|
+
// used by its own test
|
|
294
298
|
if (declaredEdges == null) return "";
|
|
295
299
|
const { findings, judged, declaration = "implicitDependencies" } = declaredEdges;
|
|
296
300
|
const label = `${judged} implicit edge${judged === 1 ? "" : "s"} judged`;
|
|
@@ -345,6 +349,7 @@ export function formatDeclaredEdges(declaredEdges, unresolvedDecisionRefs) {
|
|
|
345
349
|
* @returns {string} Empty exactly when there is no paths verdict to render.
|
|
346
350
|
*/
|
|
347
351
|
export function formatTsconfigPaths(tsconfigPaths) {
|
|
352
|
+
// used by its own test
|
|
348
353
|
if (tsconfigPaths == null) return "";
|
|
349
354
|
const { tsConfig, findings, aliases, unjudged } = tsconfigPaths;
|
|
350
355
|
const judged =
|
|
@@ -393,7 +398,7 @@ export function formatTsconfigPaths(tsconfigPaths) {
|
|
|
393
398
|
* unresolvedDecisionRefs?: {kind: string, decisionRef: string}[]}|null|undefined} intent
|
|
394
399
|
* @returns {string} Empty exactly when there is no intent verdict to render.
|
|
395
400
|
*/
|
|
396
|
-
|
|
401
|
+
function formatIntentSection(intent) {
|
|
397
402
|
if (intent == null) return "";
|
|
398
403
|
const { verdict, findings, unresolved, boundaries, unresolvedDecisionRefs = [] } = intent;
|
|
399
404
|
const count = boundaries.length;
|
|
@@ -542,6 +547,7 @@ function customFindingSite(finding) {
|
|
|
542
547
|
* @returns {string} Empty exactly when the policy declared no custom rules.
|
|
543
548
|
*/
|
|
544
549
|
export function formatCustomRulesSection(customRules) {
|
|
550
|
+
// used by its own test
|
|
545
551
|
if (customRules == null || customRules.decisions.length === 0) return "";
|
|
546
552
|
const { decisions, overall } = customRules;
|
|
547
553
|
const entries = decisions.map((decision) => {
|
|
@@ -596,6 +602,7 @@ export function formatCustomRulesSection(customRules) {
|
|
|
596
602
|
* @returns {string} Empty exactly when there is no coverage gap to render.
|
|
597
603
|
*/
|
|
598
604
|
export function formatCoverageGaps(coverageGaps) {
|
|
605
|
+
// used by its own test
|
|
599
606
|
if (coverageGaps.length === 0) return "";
|
|
600
607
|
return coverageGaps.map(formatCoverageGap).join("\n");
|
|
601
608
|
}
|
|
@@ -792,6 +799,7 @@ function formatUntrackedFilesGap(gap) {
|
|
|
792
799
|
* @returns {string} Empty exactly when no policy identity was supplied.
|
|
793
800
|
*/
|
|
794
801
|
export function formatPolicy(policy) {
|
|
802
|
+
// used by its own test
|
|
795
803
|
if (policy == null) return "";
|
|
796
804
|
const { profile, source, fingerprint } = policy;
|
|
797
805
|
const law = profile === null ? source : `profile "${profile}" from ${source}`;
|
|
@@ -812,7 +820,7 @@ export function formatPolicy(policy) {
|
|
|
812
820
|
* @param {Set<string>} [unresolvedDecisionRefs] Forwarded to `formatViolation`.
|
|
813
821
|
* @returns {string}
|
|
814
822
|
*/
|
|
815
|
-
|
|
823
|
+
function formatAcceptedViolations(waived, unresolvedDecisionRefs) {
|
|
816
824
|
const rows = waived.map((violation) => {
|
|
817
825
|
const waiver = violation.waivedBy;
|
|
818
826
|
return [
|
package/src/rules/match.mjs
CHANGED
|
@@ -85,7 +85,7 @@ import { posix } from "node:path";
|
|
|
85
85
|
* costs is bounded at the other end, by `MAX_SPECIFIER_LENGTH` below, because
|
|
86
86
|
* a degree-2 pattern is still quadratic in a subject nothing else limits.
|
|
87
87
|
*/
|
|
88
|
-
|
|
88
|
+
const MAX_RESPLIT_REPETITIONS = 2;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* The most repetitions a SINGLE-DELIMITER chain may carry — the one shape
|
|
@@ -114,7 +114,7 @@ export const MAX_RESPLIT_REPETITIONS = 2;
|
|
|
114
114
|
* path pattern naming every segment of `libs/<area>/<name>/<entry>` needs
|
|
115
115
|
* four.
|
|
116
116
|
*/
|
|
117
|
-
export const MAX_DELIMITED_SEGMENTS = 8;
|
|
117
|
+
export const MAX_DELIMITED_SEGMENTS = 8; // used by its own test
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* The longest import specifier this engine will match a consumer-written
|
|
@@ -148,7 +148,7 @@ export const MAX_DELIMITED_SEGMENTS = 8;
|
|
|
148
148
|
* 1024 `b`s. That is the number to compare a change here against; before this
|
|
149
149
|
* bound and the refusals above, the same sweep found 59 SECONDS.
|
|
150
150
|
*/
|
|
151
|
-
export const MAX_SPECIFIER_LENGTH = 1024;
|
|
151
|
+
export const MAX_SPECIFIER_LENGTH = 1024; // used by its own test
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* The deepest group nesting this model reads. Past it a pattern is refused
|
|
@@ -1123,6 +1123,7 @@ function tradesWithNeighbour(elements, index, last) {
|
|
|
1123
1123
|
* @returns {string|null}
|
|
1124
1124
|
*/
|
|
1125
1125
|
export function regexComplexityError(source) {
|
|
1126
|
+
// used by its own test
|
|
1126
1127
|
const { root, tooDeep } = parsePattern(source);
|
|
1127
1128
|
if (tooDeep) {
|
|
1128
1129
|
return (
|
|
@@ -1174,7 +1175,7 @@ function resplitReason(degree) {
|
|
|
1174
1175
|
* @param {string} specifier
|
|
1175
1176
|
* @returns {string|null}
|
|
1176
1177
|
*/
|
|
1177
|
-
|
|
1178
|
+
function specifierLengthError(specifier) {
|
|
1178
1179
|
if (specifier.length <= MAX_SPECIFIER_LENGTH) return null;
|
|
1179
1180
|
return (
|
|
1180
1181
|
`is ${specifier.length} characters, past the ${MAX_SPECIFIER_LENGTH} this engine will match ` +
|
|
@@ -1538,7 +1539,7 @@ export function findMatchingProjects(patterns, nodes) {
|
|
|
1538
1539
|
* anything a real suppression, exemption or project rule plausibly needs to
|
|
1539
1540
|
* name from ONE glob string.
|
|
1540
1541
|
*/
|
|
1541
|
-
export const MAX_GLOB_EXPANSIONS = 512;
|
|
1542
|
+
export const MAX_GLOB_EXPANSIONS = 512; // used by its own test
|
|
1542
1543
|
|
|
1543
1544
|
/**
|
|
1544
1545
|
* A brace group's content matches one of these two shapes instead of a
|
|
@@ -1620,6 +1621,7 @@ function rangeCardinality(content) {
|
|
|
1620
1621
|
* certain to exceed `cap`.
|
|
1621
1622
|
*/
|
|
1622
1623
|
export function braceExpansionCount(pattern, cap) {
|
|
1624
|
+
// used by its own test
|
|
1623
1625
|
const limit = cap + 1;
|
|
1624
1626
|
/** @type {{alternatives: number, branchProduct: number, start: number}[]} */
|
|
1625
1627
|
const frames = [{ alternatives: 0, branchProduct: 1, start: 0 }];
|
package/src/rules/specifiers.mjs
CHANGED
|
@@ -48,6 +48,7 @@ export const DEFAULT_WORKSPACE_LAYOUT = Object.freeze({ libsDir: "libs", appsDir
|
|
|
48
48
|
|
|
49
49
|
/** `./x` or `../x` — upstream's `isRelative`, from `runtime-lint-utils`. */
|
|
50
50
|
export function isRelative(s) {
|
|
51
|
+
// used by its own test
|
|
51
52
|
return s.startsWith("./") || s.startsWith("../");
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -191,6 +192,7 @@ export function getTargetProjectBasedOnRelativeImport(imp, sourceFile, projectRo
|
|
|
191
192
|
* @returns {boolean}
|
|
192
193
|
*/
|
|
193
194
|
export function isConstraintBanningProject(externalProject, constraint, imp) {
|
|
195
|
+
// used by its own test
|
|
194
196
|
assertMatchableSpecifier(imp, "import specifier judged against the constraint table");
|
|
195
197
|
const { allowedExternalImports, bannedExternalImports } = constraint;
|
|
196
198
|
const { packageName } = externalProject.data;
|
package/src/rules/tags.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { tagMatches } from "./match.mjs";
|
|
|
29
29
|
import { getPath, pathExists } from "./reachability.mjs";
|
|
30
30
|
|
|
31
31
|
/** `"a", "b"` — how upstream renders a tag list inside a message. */
|
|
32
|
-
|
|
32
|
+
function stringifyTags(tags) {
|
|
33
33
|
return tags.map((t) => `"${t}"`).join(", ");
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -55,7 +55,7 @@ export function hasTag(project, tag) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** True when the project carries NONE of these tags — upstream's spelling. */
|
|
58
|
-
|
|
58
|
+
function hasNoneOfTheseTags(project, tags) {
|
|
59
59
|
return tags.filter((tag) => hasTag(project, tag)).length === 0;
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -165,6 +165,7 @@ export function orphanedNotDependOnTags(depConstraints, graph) {
|
|
|
165
165
|
* are what the message prints so a reader can see the hop that did it.
|
|
166
166
|
*/
|
|
167
167
|
export function findDependenciesWithTags(targetProject, tags, graph, reach) {
|
|
168
|
+
// used by its own test
|
|
168
169
|
const reachable = Object.keys(graph.nodes)
|
|
169
170
|
.sort()
|
|
170
171
|
.filter(
|