@ecoma-io/archkeep 0.23.0 → 0.24.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/markdown.mjs +340 -0
- package/src/analysis/source-util.mjs +5 -4
- package/src/analysis/typescript.mjs +146 -0
- package/src/architecture-intent/model.mjs +2 -1
- package/src/commands/check.mjs +166 -8
- package/src/commands/context-command.mjs +12 -20
- package/src/commands/coverage-verdict.mjs +12 -2
- package/src/commands/delta-snapshot.mjs +1 -5
- package/src/commands/explain.mjs +17 -20
- package/src/commands/graph.mjs +7 -0
- package/src/commands/health.mjs +4 -0
- package/src/commands/plan-context-command.mjs +5 -1
- package/src/commands/policy.mjs +4 -4
- package/src/commands/provenance.mjs +8 -2
- package/src/config.mjs +170 -2
- package/src/errors.mjs +23 -1
- package/src/eslint-config.mjs +2 -5
- package/src/governance/adr-registry.mjs +2 -1
- package/src/governance/evolution-store.mjs +3 -2
- package/src/governance/fitness-registry.mjs +0 -3
- package/src/governance/row-schema.mjs +0 -3
- package/src/intent/intent-manifest.json +6 -6
- package/src/providers/moon.mjs +5 -5
- package/src/rules/index.mjs +30 -0
- package/src/verdict.mjs +33 -2
package/src/config.mjs
CHANGED
|
@@ -639,6 +639,155 @@ function findCoverageViolations(value) {
|
|
|
639
639
|
return violations;
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/**
|
|
643
|
+
* The one edge kind a markdown marker row can declare. The name is the claim
|
|
644
|
+
* the row makes about the graph: the captured symbol is resolved to the project
|
|
645
|
+
* that exports it, and the edge runs from the document's own project to that
|
|
646
|
+
* project. Kept as data rather than inlined at the check site so a second kind
|
|
647
|
+
* (`docs/reference/policy-schema.md`, "markdown") extends this list and nothing
|
|
648
|
+
* else — and so a row naming a kind this reader cannot draw is refused at load,
|
|
649
|
+
* where a law that would silently never run belongs.
|
|
650
|
+
*
|
|
651
|
+
* @type {Readonly<string[]>}
|
|
652
|
+
*/
|
|
653
|
+
export const MARKDOWN_EDGE_KINDS = Object.freeze(["resolvedExportOwner"]);
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* One markdown marker row's problems, prefixed with its index.
|
|
657
|
+
*
|
|
658
|
+
* `pattern` is a regular-expression SOURCE matched against one line of a
|
|
659
|
+
* matched document at a time — never a glob, and never a whole-document
|
|
660
|
+
* match — because the position a violation reports must be a real
|
|
661
|
+
* `file:line:column` a developer can open. Its FIRST capture group must name
|
|
662
|
+
* the exported symbol the marker claims; a pattern with no capture group can
|
|
663
|
+
* match every line it is aimed at and still resolve nothing, which is a law
|
|
664
|
+
* that reads as enforced while testing nothing — refused here for the reason
|
|
665
|
+
* every dead shape in this file is refused.
|
|
666
|
+
*
|
|
667
|
+
* @param {object} row
|
|
668
|
+
* @param {number} index
|
|
669
|
+
* @returns {string[]}
|
|
670
|
+
*/
|
|
671
|
+
function markdownMarkerRowViolations(row, index) {
|
|
672
|
+
const at = `markdown.markers[${index}]`;
|
|
673
|
+
if (!isPlainObject(row)) return [`${at}: must be an object, got ${describe(row)}`];
|
|
674
|
+
|
|
675
|
+
const violations = [];
|
|
676
|
+
if (typeof row.pattern !== "string" || row.pattern === "") {
|
|
677
|
+
violations.push(
|
|
678
|
+
`${at}.pattern: must be a non-empty regular-expression source matched against one line of ` +
|
|
679
|
+
`a matched document, got ${describe(row.pattern)}`,
|
|
680
|
+
);
|
|
681
|
+
} else {
|
|
682
|
+
let compiled = null;
|
|
683
|
+
try {
|
|
684
|
+
compiled = new RegExp(row.pattern, "u");
|
|
685
|
+
} catch (cause) {
|
|
686
|
+
violations.push(
|
|
687
|
+
`${at}.pattern: '${row.pattern}' is not a valid regular expression under the 'u' flag ` +
|
|
688
|
+
`(${cause?.message ?? cause})`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (compiled !== null) {
|
|
692
|
+
// The capture count, read off the standard `pattern|` trick: matching the
|
|
693
|
+
// empty string yields one element per capture group plus the whole match,
|
|
694
|
+
// so a pattern that cannot capture anything reports `length === 1` here.
|
|
695
|
+
// Compiled in its own try, because the appended `|` can fail on a source
|
|
696
|
+
// the pattern alone accepted; the compile violation above is already
|
|
697
|
+
// reported in that case, and a second one would name the same row twice.
|
|
698
|
+
let captures;
|
|
699
|
+
try {
|
|
700
|
+
captures = (new RegExp(`${row.pattern}|`, "u").exec("")?.length ?? 1) - 1;
|
|
701
|
+
} catch {
|
|
702
|
+
captures = 1; // already refused above by the pattern's own compile check
|
|
703
|
+
}
|
|
704
|
+
if (captures < 1) {
|
|
705
|
+
violations.push(
|
|
706
|
+
`${at}.pattern: '${row.pattern}' has no capture group — the first capture group must ` +
|
|
707
|
+
`name the exported symbol the marker claims, and a pattern that captures nothing ` +
|
|
708
|
+
`resolves nothing while reading as enforced`,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (!MARKDOWN_EDGE_KINDS.includes(row.edge)) {
|
|
714
|
+
violations.push(
|
|
715
|
+
`${at}.edge: ${describe(row.edge)} is not an edge kind this reader can draw — expected ` +
|
|
716
|
+
`${MARKDOWN_EDGE_KINDS.map((kind) => `"${kind}"`).join(", ")} (the captured symbol is ` +
|
|
717
|
+
`resolved to the project exporting it, and the edge runs there from the document's own ` +
|
|
718
|
+
`project)`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
for (const key of Object.keys(row)) {
|
|
722
|
+
if (key !== "pattern" && key !== "edge") {
|
|
723
|
+
violations.push(`${at}.${key}: not a markdown marker field — expected 'pattern' and 'edge'`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return violations;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* The markdown document track's problems, as messages; empty when it is
|
|
731
|
+
* well-formed. Pure, so a test drives it without a file on disk.
|
|
732
|
+
*
|
|
733
|
+
* `include` names the documents the track reads, by workspace-relative glob —
|
|
734
|
+
* the same glob machinery `boundarySuppressions` rows and `coverage.unowned`
|
|
735
|
+
* rows match with (`./rules/match.mjs`'s `safeMatchesGlob`), complexity-checked
|
|
736
|
+
* here at load for the reason a suppression's `path` is. An ABSOLUTE pattern is
|
|
737
|
+
* refused rather than left to match nothing forever: it can never match a
|
|
738
|
+
* workspace-relative document path, so declaring one is a law aimed outside
|
|
739
|
+
* the tree it governs (the family test is `ABSOLUTE_ARTIFACT_PATH`'s — the same
|
|
740
|
+
* "absolute in either path family" question, asked of a glob instead of a file).
|
|
741
|
+
*
|
|
742
|
+
* @param {unknown} value The parsed `markdown` value.
|
|
743
|
+
* @returns {string[]}
|
|
744
|
+
*/
|
|
745
|
+
function findMarkdownViolations(value) {
|
|
746
|
+
if (!isPlainObject(value)) {
|
|
747
|
+
return [`markdown: must be an object carrying 'include' and 'markers', got ${describe(value)}`];
|
|
748
|
+
}
|
|
749
|
+
const violations = [];
|
|
750
|
+
if (!Array.isArray(value.include) || value.include.length === 0) {
|
|
751
|
+
violations.push(
|
|
752
|
+
`markdown.include: must be a non-empty array of workspace-relative glob patterns naming ` +
|
|
753
|
+
`the documents the track reads, got ${describe(value.include)}`,
|
|
754
|
+
);
|
|
755
|
+
} else {
|
|
756
|
+
value.include.forEach((pattern, index) => {
|
|
757
|
+
const at = `markdown.include[${index}]`;
|
|
758
|
+
if (typeof pattern !== "string" || pattern === "") {
|
|
759
|
+
violations.push(
|
|
760
|
+
`${at}: must be a non-empty workspace-relative glob, got ${describe(pattern)}`,
|
|
761
|
+
);
|
|
762
|
+
} else if (ABSOLUTE_ARTIFACT_PATH.test(pattern)) {
|
|
763
|
+
violations.push(
|
|
764
|
+
`${at}: '${pattern}' is an absolute path — an include glob is matched against ` +
|
|
765
|
+
`workspace-relative document paths, so an absolute one can never match`,
|
|
766
|
+
);
|
|
767
|
+
} else {
|
|
768
|
+
const problem = globComplexityError(pattern);
|
|
769
|
+
if (problem) violations.push(`${at}: '${pattern}' ${problem}`);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (!Array.isArray(value.markers) || value.markers.length === 0) {
|
|
774
|
+
violations.push(
|
|
775
|
+
`markdown.markers: must be a non-empty array of {pattern, edge} rows, got ` +
|
|
776
|
+
`${describe(value.markers)}`,
|
|
777
|
+
);
|
|
778
|
+
} else {
|
|
779
|
+
value.markers.forEach((row, index) =>
|
|
780
|
+
violations.push(...markdownMarkerRowViolations(row, index)),
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
for (const key of Object.keys(value)) {
|
|
784
|
+
if (key !== "include" && key !== "markers") {
|
|
785
|
+
violations.push(`markdown.${key}: not a markdown field — expected 'include' and 'markers'`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return violations;
|
|
789
|
+
}
|
|
790
|
+
|
|
642
791
|
/**
|
|
643
792
|
* The grammar a custom rule's `name` is written in: lowercase letters and
|
|
644
793
|
* digits, single `-` separators, nothing else.
|
|
@@ -945,6 +1094,7 @@ export function findBoundaryConfigViolations(module, io = {}) {
|
|
|
945
1094
|
fitness,
|
|
946
1095
|
customRules,
|
|
947
1096
|
coverage,
|
|
1097
|
+
markdown,
|
|
948
1098
|
} = module;
|
|
949
1099
|
// F05: the resolution half of the governance block (`row-schema.mjs`'s
|
|
950
1100
|
// `io.resolve`) was validator-only until now — no production caller passed
|
|
@@ -1000,6 +1150,19 @@ export function findBoundaryConfigViolations(module, io = {}) {
|
|
|
1000
1150
|
violations.push(...findCoverageViolations(coverage));
|
|
1001
1151
|
}
|
|
1002
1152
|
|
|
1153
|
+
// The markdown document track — the seventh top-level law, declared here and
|
|
1154
|
+
// executed at graph level (`../analysis/markdown.mjs`): machine-readable
|
|
1155
|
+
// markers inside tracked markdown documents, each resolved to an edge from
|
|
1156
|
+
// the document's own project to the project that exports the named symbol.
|
|
1157
|
+
// Absent means "no document track" — the workspace decision this key exists
|
|
1158
|
+
// to state, and the state every config-absent run must stay byte-identical
|
|
1159
|
+
// to. Present and malformed is refused here, loudly, for the reason the two
|
|
1160
|
+
// blocks above state: a row this reader cannot understand is a document law
|
|
1161
|
+
// that would not run while the policy still says it does.
|
|
1162
|
+
if (markdown !== undefined) {
|
|
1163
|
+
violations.push(...findMarkdownViolations(markdown));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1003
1166
|
// Absent means "nothing is suppressed", which is the only default that fails
|
|
1004
1167
|
// toward reporting — unlike the eight options above, where a missing value
|
|
1005
1168
|
// would be a second copy of something ESLint also reads and this module has
|
|
@@ -1095,7 +1258,10 @@ export function findBoundaryConfigViolations(module, io = {}) {
|
|
|
1095
1258
|
* `coverage.exempt` is that provider's one channel for the same decision —
|
|
1096
1259
|
* and the refusal lives in `./commands/policy.mjs`'s `resolvePolicy` and
|
|
1097
1260
|
* `./providers/native/model.mjs`'s inline-policy check, because only they
|
|
1098
|
-
* know which provider is reading.
|
|
1261
|
+
* know which provider is reading. `markdown` is the seventh: the document
|
|
1262
|
+
* track (`findMarkdownViolations` above owns the shape), read into the graph
|
|
1263
|
+
* by `../analysis/markdown.mjs` on every provider, because documents belong
|
|
1264
|
+
* to projects under all three.
|
|
1099
1265
|
*
|
|
1100
1266
|
* The name says `.json` and the list binds both file dialects: `loadModulePolicy`
|
|
1101
1267
|
* runs the same check over an ES module's exports, which is what makes a
|
|
@@ -1109,6 +1275,7 @@ const JSON_POLICY_KEYS = [
|
|
|
1109
1275
|
"fitness",
|
|
1110
1276
|
"customRules",
|
|
1111
1277
|
"coverage",
|
|
1278
|
+
"markdown",
|
|
1112
1279
|
];
|
|
1113
1280
|
|
|
1114
1281
|
/**
|
|
@@ -1185,7 +1352,7 @@ export function policyKeyViolations(parsed, { allowSchema }) {
|
|
|
1185
1352
|
* inline one.
|
|
1186
1353
|
* @param {string[]} [extraViolations] Violations the caller already found that
|
|
1187
1354
|
* `findBoundaryConfigViolations` does not check on its own.
|
|
1188
|
-
* @returns {{ depConstraints: object[], options: object, suppressions: object[], fitness?: object[], customRules?: object[], coverage?: object }}
|
|
1355
|
+
* @returns {{ depConstraints: object[], options: object, suppressions: object[], fitness?: object[], customRules?: object[], coverage?: object, markdown?: object }}
|
|
1189
1356
|
* `fitness` and `customRules` are present only when the config declares
|
|
1190
1357
|
* them — a workspace without one carries no key, the same "absent is a
|
|
1191
1358
|
* decision" posture `cli.mjs`'s `check` uses for a missing
|
|
@@ -1209,6 +1376,7 @@ export function policyFrom(parsed, sourceLabel, extraViolations = []) {
|
|
|
1209
1376
|
...(parsed.fitness === undefined ? {} : { fitness: parsed.fitness }),
|
|
1210
1377
|
...(parsed.customRules === undefined ? {} : { customRules: parsed.customRules }),
|
|
1211
1378
|
...(parsed.coverage === undefined ? {} : { coverage: parsed.coverage }),
|
|
1379
|
+
...(parsed.markdown === undefined ? {} : { markdown: parsed.markdown }),
|
|
1212
1380
|
};
|
|
1213
1381
|
}
|
|
1214
1382
|
|
package/src/errors.mjs
CHANGED
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
* (`../AGENTS.md`, check's four exit codes).
|
|
24
24
|
*
|
|
25
25
|
* One class, nothing else exported: a second class needs a catch site that
|
|
26
|
-
* treats two of these mistakes differently, and none does.
|
|
26
|
+
* treats two of these mistakes differently, and none does. Beside it sits the
|
|
27
|
+
* one error-SHAPE predicate the engine shares (`isEnoent` below) — a test of
|
|
28
|
+
* what a caught value looks like, not a decision about what one means, which
|
|
29
|
+
* is why it lives with the error primitives rather than at any catch site.
|
|
27
30
|
*/
|
|
28
31
|
export class UsageError extends Error {
|
|
29
32
|
/**
|
|
@@ -34,3 +37,22 @@ export class UsageError extends Error {
|
|
|
34
37
|
this.name = "UsageError";
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Is this caught value the filesystem's "no such file or directory"?
|
|
43
|
+
*
|
|
44
|
+
* Node's `fs` throws the raw error with `code: "ENOENT"` set directly;
|
|
45
|
+
* `./process.mjs`'s `runProcess` wraps its child's failure and carries the
|
|
46
|
+
* original on `cause` — so the shape arrives both ways, and every site that
|
|
47
|
+
* distinguishes "absent" from "could not read" was spelling the test by hand
|
|
48
|
+
* (`#652`). The two legs together are the one definition; a catch site adds
|
|
49
|
+
* its own meaning on top (absent store, absent registry, install Moon), which
|
|
50
|
+
* is exactly the logic this predicate does not own.
|
|
51
|
+
*
|
|
52
|
+
* @param {unknown} error The caught value, of any shape.
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function isEnoent(error) {
|
|
56
|
+
const thrown = /** @type {{code?: unknown, cause?: {code?: unknown}}|null|undefined} */ (error);
|
|
57
|
+
return thrown?.code === "ENOENT" || thrown?.cause?.code === "ENOENT";
|
|
58
|
+
}
|
package/src/eslint-config.mjs
CHANGED
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
import { createRequire } from "node:module";
|
|
72
72
|
import { pathToFileURL } from "node:url";
|
|
73
73
|
|
|
74
|
+
import { isPlainObject } from "./values.mjs";
|
|
75
|
+
|
|
74
76
|
/** The severities ESLint itself recognises for a rule entry. */
|
|
75
77
|
const KNOWN_SEVERITIES = new Set(["off", "warn", "error", 0, 1, 2]);
|
|
76
78
|
const OFF_SEVERITIES = new Set(["off", 0]);
|
|
@@ -94,11 +96,6 @@ const OFF_SEVERITIES = new Set(["off", 0]);
|
|
|
94
96
|
// one — a stricter refusal than strictly necessary, not a silent guess.
|
|
95
97
|
const BARE_EXTENSION_GLOB = /^\*\*\/\*\.[A-Za-z0-9]+$/u;
|
|
96
98
|
|
|
97
|
-
/** @type {(value: unknown) => value is Record<string, unknown>} */
|
|
98
|
-
function isPlainObject(value) {
|
|
99
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
99
|
/** A rule entry's severity — the bare value itself, or the `[severity, …]` pair's first element. */
|
|
103
100
|
function severityOf(value) {
|
|
104
101
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -91,6 +91,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "
|
|
|
91
91
|
import { join } from "node:path";
|
|
92
92
|
|
|
93
93
|
import { containmentViolation } from "../containment.mjs";
|
|
94
|
+
import { isEnoent } from "../errors.mjs";
|
|
94
95
|
import { describe } from "../values.mjs";
|
|
95
96
|
|
|
96
97
|
/** The directory, relative to a workspace root, where ADR files live. */
|
|
@@ -538,7 +539,7 @@ export function loadAdrRegistry(root, io = {}) {
|
|
|
538
539
|
// deliberately silent, for the reason this module's header states.
|
|
539
540
|
names = readDir(dir);
|
|
540
541
|
} catch (cause) {
|
|
541
|
-
if (cause
|
|
542
|
+
if (isEnoent(cause)) return { records: [], byId: new Map() };
|
|
542
543
|
throw new Error(`archkeep: cannot read ${ADR_DIR}: ${cause?.message ?? cause}`, { cause });
|
|
543
544
|
}
|
|
544
545
|
names.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { join, resolve } from "node:path";
|
|
52
52
|
|
|
53
53
|
import { containmentViolation } from "../containment.mjs";
|
|
54
|
+
import { isEnoent } from "../errors.mjs";
|
|
54
55
|
import {
|
|
55
56
|
eventDedupeKey,
|
|
56
57
|
eventId,
|
|
@@ -222,7 +223,7 @@ export function writeEvent(dir, event, io = {}) {
|
|
|
222
223
|
try {
|
|
223
224
|
names = readDir(dirAbs);
|
|
224
225
|
} catch (cause) {
|
|
225
|
-
if (cause
|
|
226
|
+
if (isEnoent(cause)) {
|
|
226
227
|
// An absent optional store is an empty store: the caller's first event.
|
|
227
228
|
makeDir(dirAbs, { recursive: true });
|
|
228
229
|
names = readDir(dirAbs);
|
|
@@ -325,7 +326,7 @@ export function readEvents(dir, io = {}) {
|
|
|
325
326
|
try {
|
|
326
327
|
names = readDir(dirAbs);
|
|
327
328
|
} catch (cause) {
|
|
328
|
-
if (cause
|
|
329
|
+
if (isEnoent(cause)) return [];
|
|
329
330
|
throw new Error(
|
|
330
331
|
`archkeep: cannot read the event store '${dirAbs}': ${cause?.message ?? cause}`,
|
|
331
332
|
{ cause },
|
|
@@ -63,9 +63,6 @@ import {
|
|
|
63
63
|
tagConformance,
|
|
64
64
|
} from "./fitness-rules.mjs";
|
|
65
65
|
|
|
66
|
-
/** The one `fitness` list key in the boundary config. */
|
|
67
|
-
export const FITNESS_KEY = "fitness";
|
|
68
|
-
|
|
69
66
|
/** The condition types the registry can evaluate. */
|
|
70
67
|
export const CONDITION_TYPES = Object.freeze([
|
|
71
68
|
"cycle-free",
|
|
@@ -59,9 +59,6 @@
|
|
|
59
59
|
import { originViolations } from "./provenance-record.mjs";
|
|
60
60
|
import { describe, isPlainObject } from "../values.mjs";
|
|
61
61
|
|
|
62
|
-
/** The shape of any `origin.on` producer. Re-exported for a row owner's own docs. */
|
|
63
|
-
export { clockViolations as clockValidation } from "./clock.mjs";
|
|
64
|
-
|
|
65
62
|
/** The four governance keys a row may carry, in the order reports list them. */
|
|
66
63
|
export const GOVERNANCE_ROW_KEYS = Object.freeze([
|
|
67
64
|
"origin",
|
|
@@ -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": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
|
|
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": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
|
|
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": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
|
|
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",
|
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
|
/**
|
package/src/rules/index.mjs
CHANGED
|
@@ -815,6 +815,36 @@ function annotatedByTable(suppressions, violation, now) {
|
|
|
815
815
|
return violation;
|
|
816
816
|
}
|
|
817
817
|
|
|
818
|
+
/**
|
|
819
|
+
* The suppression table applied to verdicts that did not come from
|
|
820
|
+
* `evaluateRun`'s site walk — suppressing rows remove, active waivers mark
|
|
821
|
+
* `waivedBy`, expired ones re-assert with `evidence`, exactly as the site walk
|
|
822
|
+
* applies them, because a verdict must ride the same table whichever walk
|
|
823
|
+
* produced it.
|
|
824
|
+
*
|
|
825
|
+
* The one caller is `./commands/check.mjs`'s markdown document track: its
|
|
826
|
+
* edges are judged per edge through `./edge-constraints.mjs`'s `judgeEdge` —
|
|
827
|
+
* they have no import site for `candidateGroupsFor` to walk, so there is no
|
|
828
|
+
* group chain, only a flat list — but a suppression row that names a document
|
|
829
|
+
* must silence the same verdict here it would silence on an import site, and
|
|
830
|
+
* a waiver over one must annotate it the same way. Unlike the site walk there
|
|
831
|
+
* is no second candidate group to fall through to: an edge's verdicts are all
|
|
832
|
+
* reported together, so a row that removes some leaves the rest standing
|
|
833
|
+
* rather than promoting anything.
|
|
834
|
+
*
|
|
835
|
+
* @param {object[]} suppressions The validated `boundarySuppressions` table.
|
|
836
|
+
* @param {object[]} violations The unfiltered verdicts, in walk order.
|
|
837
|
+
* @param {string} [now] Reference instant for waiver expiry; defaults to the
|
|
838
|
+
* shared governance clock, as `evaluateRun` does.
|
|
839
|
+
* @returns {object[]} The survivors, in input order, annotations applied.
|
|
840
|
+
*/
|
|
841
|
+
export function applySuppressionTable(suppressions, violations, now = referenceTime()) {
|
|
842
|
+
if (suppressions.length === 0) return violations;
|
|
843
|
+
return violations
|
|
844
|
+
.filter((violation) => !removedByTable(suppressions, violation, now))
|
|
845
|
+
.map((violation) => annotatedByTable(suppressions, violation, now));
|
|
846
|
+
}
|
|
847
|
+
|
|
818
848
|
/**
|
|
819
849
|
* One run of the engine over every import site: the judged verdict per site
|
|
820
850
|
* plus the raw superset it was picked from.
|
package/src/verdict.mjs
CHANGED
|
@@ -61,6 +61,33 @@ export function coverageIncompleteReasons({ unchecked, blindSpots, analyzed }) {
|
|
|
61
61
|
analyzed === 0 ? "no file in scope could be analyzed — coverage incomplete" : null,
|
|
62
62
|
].filter(Boolean);
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The one completeness predicate — the three coverage axes conjoined, the
|
|
67
|
+
* boolean twin of `coverageIncompleteReasons` directly above, which words the
|
|
68
|
+
* same axes as clauses. `coverageVerdict` (`./commands/coverage-verdict.mjs`)
|
|
69
|
+
* reads its `complete` from here, `verdictFor` reads the decision's
|
|
70
|
+
* `coverageComplete` from here, and `check`'s coverage block reads its
|
|
71
|
+
* `complete` from here — three faces of one claim, so the envelope's
|
|
72
|
+
* `coverage.complete` and its `decision.coverageComplete` cannot disagree
|
|
73
|
+
* about a run neither re-derives from the other.
|
|
74
|
+
*
|
|
75
|
+
* The counts come from the caller because a command's coverage universe is
|
|
76
|
+
* its own: `check`'s is wider than `commandContext.analysis` (the go.work and
|
|
77
|
+
* tsconfig whole-file failures it pushes, the accepted `coverage.unowned`
|
|
78
|
+
* files it withdraws), and each face feeds the counts it is a claim about. A
|
|
79
|
+
* caller that reads plain `commandContext.analysis` should call
|
|
80
|
+
* `coverageVerdict` instead — this predicate is the law's last step, not the
|
|
81
|
+
* place failure classes get decided (`./analysis/source-util.mjs`'s
|
|
82
|
+
* classifiers own that line).
|
|
83
|
+
*
|
|
84
|
+
* @param {{unchecked: number, blindSpotCount: number, analyzed: number}} counts
|
|
85
|
+
* @returns {boolean} Whether the run judged everything in its scope.
|
|
86
|
+
*/
|
|
87
|
+
export function coverageComplete({ unchecked, blindSpotCount, analyzed }) {
|
|
88
|
+
return unchecked === 0 && blindSpotCount === 0 && analyzed > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
64
91
|
/**
|
|
65
92
|
* The one place that turns a run's counts into the verdict every format
|
|
66
93
|
* agrees on. `runCheck` uses it for the process's exit code; `check` uses the
|
|
@@ -127,7 +154,11 @@ export function verdictFor({
|
|
|
127
154
|
reasons: coverageReasons,
|
|
128
155
|
decision: buildDecision({
|
|
129
156
|
status: "findings",
|
|
130
|
-
|
|
157
|
+
// The one completeness predicate, not a restatement: the decision's
|
|
158
|
+
// `coverageComplete` and the envelope's `coverage.complete` are the
|
|
159
|
+
// same claim about the same counts (`check` feeds both from one
|
|
160
|
+
// object), so they read it from one expression.
|
|
161
|
+
coverageComplete: coverageComplete({ unchecked, blindSpotCount: blindSpots, analyzed }),
|
|
131
162
|
findings:
|
|
132
163
|
violations +
|
|
133
164
|
declaredEdgeFindings +
|
|
@@ -188,7 +219,7 @@ export function verdictFor({
|
|
|
188
219
|
reasons,
|
|
189
220
|
decision: buildDecision({
|
|
190
221
|
status: "no-verdict",
|
|
191
|
-
coverageComplete: unchecked
|
|
222
|
+
coverageComplete: coverageComplete({ unchecked, blindSpotCount: blindSpots, analyzed }),
|
|
192
223
|
findings: 0,
|
|
193
224
|
reason: reasons.join("; "),
|
|
194
225
|
}),
|