@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/commands/check.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { statSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
+
import { foldMarkdownTrack } from "../analysis/markdown.mjs";
|
|
15
16
|
import {
|
|
16
17
|
blindSpotRows,
|
|
17
18
|
fileFailure,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
import { partitionUnownedCoverage } from "./coverage-acceptance.mjs";
|
|
32
33
|
import { readAdrContext } from "./adr.mjs";
|
|
33
34
|
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
34
|
-
import { declaredEdgeViolationsForCheck } from "../rules/edge-constraints.mjs";
|
|
35
|
+
import { declaredEdgeViolationsForCheck, judgeEdge } from "../rules/edge-constraints.mjs";
|
|
35
36
|
import { customRulesForCheck, declaresCustomRules } from "./custom-rules.mjs";
|
|
36
37
|
import { driftForCheck } from "./drift.mjs";
|
|
37
38
|
import { fitnessForCheck } from "./fitness.mjs";
|
|
@@ -40,14 +41,16 @@ import { resolvePolicy } from "./policy.mjs";
|
|
|
40
41
|
import { resolveProvenance } from "./provenance.mjs";
|
|
41
42
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
42
43
|
import { compareGoWork, parseGoWorkUse } from "../go-work.mjs";
|
|
44
|
+
import { mergeDeclaredEdges } from "../providers/native/graph.mjs";
|
|
43
45
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
44
46
|
import { formatSarif } from "../report/sarif.mjs";
|
|
45
47
|
import { formatReport } from "../report/text.mjs";
|
|
46
48
|
import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
|
|
47
|
-
import { evaluateRun, exemptResolvedFile } from "../rules/index.mjs";
|
|
49
|
+
import { applySuppressionTable, evaluateRun, exemptResolvedFile } from "../rules/index.mjs";
|
|
50
|
+
import { buildReachability } from "../rules/reachability.mjs";
|
|
48
51
|
import { orphanedNotDependOnTags, unmatchedConstraintRows } from "../rules/tags.mjs";
|
|
49
52
|
import { judgeTsconfigPaths } from "../tsconfig-paths.mjs";
|
|
50
|
-
import { verdictFor } from "../verdict.mjs";
|
|
53
|
+
import { coverageComplete, verdictFor } from "../verdict.mjs";
|
|
51
54
|
import { listTrackedFiles, listUntrackedFiles } from "../workspace.mjs";
|
|
52
55
|
|
|
53
56
|
/**
|
|
@@ -453,6 +456,41 @@ export async function check(
|
|
|
453
456
|
}
|
|
454
457
|
}
|
|
455
458
|
|
|
459
|
+
// The markdown document track, keyed by presence like every fold above it:
|
|
460
|
+
// a policy that declares no `markdown` block reaches nothing here and hears
|
|
461
|
+
// nothing anywhere — no edges, no failures, no envelope key — so a
|
|
462
|
+
// config-absent run is byte-identical to one this fold never existed for
|
|
463
|
+
// (`../../../../AGENTS.md`, "a change to what is reported on an unchanged
|
|
464
|
+
// workspace is a breaking change"). A policy that DOES declare one has its
|
|
465
|
+
// documents read (`../analysis/markdown.mjs`'s `foldMarkdownTrack`), the
|
|
466
|
+
// resolved pairings folded into the graph the way the declared manifest
|
|
467
|
+
// track's edges already are (`mergeDeclaredEdges`, the same dedup key), and
|
|
468
|
+
// every whole-file failure the read earned pushed into the same `failures`
|
|
469
|
+
// funnel the analysis uses — an unresolvable marker is an `unchecked` file,
|
|
470
|
+
// never a clean one.
|
|
471
|
+
//
|
|
472
|
+
// The fold runs BEFORE the walk below on purpose: a document pairing is a
|
|
473
|
+
// project-to-project dependency claim, and from here on this run's graph
|
|
474
|
+
// carries it exactly as it carries a csproj's `ProjectReference` — the tag
|
|
475
|
+
// rows, the circularity walk and the reachability behind them all read one
|
|
476
|
+
// graph, so a pairing cannot be legal here and violating in `context`.
|
|
477
|
+
// What it does not join is the twelve non-tag import-site rules: those need
|
|
478
|
+
// a specifier, a file and a resolution, and a document marker has none of
|
|
479
|
+
// them — it is judged as the EDGE it drew, the same 3-of-15 limit
|
|
480
|
+
// `declaredEdgeViolationsForCheck` documents for `implicit` edges.
|
|
481
|
+
let markdownTrack = null;
|
|
482
|
+
if (config !== null && config.markdown !== undefined) {
|
|
483
|
+
markdownTrack = foldMarkdownTrack({
|
|
484
|
+
tracked,
|
|
485
|
+
owned: commandContext.owned,
|
|
486
|
+
readFile: (file) => workspace.readFile(file),
|
|
487
|
+
workspace,
|
|
488
|
+
markdown: config.markdown,
|
|
489
|
+
});
|
|
490
|
+
mergeDeclaredEdges(graph, markdownTrack.edges);
|
|
491
|
+
failures.push(...markdownTrack.failures);
|
|
492
|
+
}
|
|
493
|
+
|
|
456
494
|
// Both faces of one walk: the run's verdict, and the raw superset it was
|
|
457
495
|
// picked from — every candidate up to each site's surviving group, including
|
|
458
496
|
// the verdicts the suppression table removed to get there. `evaluate` alone
|
|
@@ -462,8 +500,66 @@ export async function check(
|
|
|
462
500
|
// the gate's waiver-expiry judgement below and the engine's are the same
|
|
463
501
|
// judgement, not two reads of the clock a boundary instant could split.
|
|
464
502
|
const now = referenceTime();
|
|
465
|
-
const { violations: judged, rawViolations } = evaluateRun(imports, graph, {
|
|
466
|
-
|
|
503
|
+
const { violations: judged, rawViolations: judgedRaw } = evaluateRun(imports, graph, {
|
|
504
|
+
...config,
|
|
505
|
+
now,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// The document pairings' own verdicts, judged by the machinery that already
|
|
509
|
+
// existed: `judgeEdge` — the same function `declaredEdgeViolationsForCheck`
|
|
510
|
+
// runs `implicit` edges through, so a doc pairing and a declared manifest
|
|
511
|
+
// edge can never disagree about the same boundary — over the graph the fold
|
|
512
|
+
// above already joined, with reachability built once for the whole claim
|
|
513
|
+
// list. The same empty-table exit `declaredEdgeViolationsForCheck` takes: a
|
|
514
|
+
// workspace declaring no `depConstraints` has opted out of tag enforcement
|
|
515
|
+
// entirely, and folding `projectWithoutTagsCannotHaveDependencies` onto
|
|
516
|
+
// every marker anyway would flag an opted-out workspace for a reason its
|
|
517
|
+
// import sites are never flagged for. Each verdict is then reshaped into the
|
|
518
|
+
// exact `Violation` record `violationOf` builds — position and specifier
|
|
519
|
+
// from the MARKER (the line a reader edits is the document's), project and
|
|
520
|
+
// constraint from the edge — so suppression, waiver annotation, sorting and
|
|
521
|
+
// the SARIF face all treat it as the ordinary violation it is.
|
|
522
|
+
/** @type {object[]} */
|
|
523
|
+
const markdownRaw = [];
|
|
524
|
+
if (config !== null && markdownTrack !== null && config.depConstraints.length > 0) {
|
|
525
|
+
const reachability = buildReachability(graph);
|
|
526
|
+
for (const claim of markdownTrack.claims) {
|
|
527
|
+
for (const verdict of judgeEdge(
|
|
528
|
+
{ source: claim.source, target: claim.target },
|
|
529
|
+
graph.nodes,
|
|
530
|
+
graph.dependencies,
|
|
531
|
+
config.depConstraints,
|
|
532
|
+
reachability,
|
|
533
|
+
)) {
|
|
534
|
+
markdownRaw.push({
|
|
535
|
+
sourceFile: claim.file,
|
|
536
|
+
line: claim.line,
|
|
537
|
+
column: claim.column,
|
|
538
|
+
specifier: claim.name,
|
|
539
|
+
kind: claim.type,
|
|
540
|
+
messageId: verdict.messageId,
|
|
541
|
+
message: verdict.message,
|
|
542
|
+
sourceProject: verdict.source,
|
|
543
|
+
targetProject: verdict.target,
|
|
544
|
+
constraint: verdict.constraint,
|
|
545
|
+
data: verdict.data,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// The same table, the same behaviour: a suppression row whose glob covers a
|
|
551
|
+
// document removes that document's verdict (or waives it, with the same
|
|
552
|
+
// expiry evidence an import-site waiver carries), and the RAW records join
|
|
553
|
+
// `rawViolations` below so a row covering only document verdicts still
|
|
554
|
+
// counts as alive — a table that is doing its job must not read as dead
|
|
555
|
+
// because the violations it removes live in a different fold.
|
|
556
|
+
const markdownViolations =
|
|
557
|
+
config !== null && markdownTrack !== null
|
|
558
|
+
? applySuppressionTable(config.suppressions, markdownRaw, now)
|
|
559
|
+
: [];
|
|
560
|
+
|
|
561
|
+
const rawViolations = [...judgedRaw, ...markdownRaw];
|
|
562
|
+
const violations = sortViolations([...judged, ...markdownViolations]);
|
|
467
563
|
// An ACTIVE waiver keeps the violation it accepts in the findings list,
|
|
468
564
|
// marked `waivedBy` — the run is still non-zero (waiving does not flip
|
|
469
565
|
// exit 1 → 0), and this count is the additive "accepted violations" number
|
|
@@ -704,7 +800,10 @@ export async function check(
|
|
|
704
800
|
config !== null &&
|
|
705
801
|
options.paths.length === 0 &&
|
|
706
802
|
failures.length === 0 &&
|
|
707
|
-
(config.suppressions.length > 0 ||
|
|
803
|
+
(config.suppressions.length > 0 ||
|
|
804
|
+
config.depConstraints.length > 0 ||
|
|
805
|
+
coverageRows.length > 0 ||
|
|
806
|
+
config.markdown !== undefined)
|
|
708
807
|
) {
|
|
709
808
|
const deadRows = [];
|
|
710
809
|
// The third dead table: a `coverage.unowned` row matching no unowned file
|
|
@@ -755,6 +854,34 @@ export async function check(
|
|
|
755
854
|
);
|
|
756
855
|
}
|
|
757
856
|
}
|
|
857
|
+
// The document track's two dead tables, measured against what the fold
|
|
858
|
+
// actually selected (`markdownTrack.includeCounts`/`rowMatches` — the
|
|
859
|
+
// same counts the fold's own selection is built from, not a re-derivation
|
|
860
|
+
// that could disagree): an include glob matching no tracked document, and
|
|
861
|
+
// a marker row matching no line in any included document, both govern
|
|
862
|
+
// nothing while reading as enforced — every pairing the workspace meant
|
|
863
|
+
// to declare goes unjudged and the run stays green. A whole-file failure
|
|
864
|
+
// above already disabled this gate, so reaching here with a row that
|
|
865
|
+
// matched nothing means the tree genuinely has no such line.
|
|
866
|
+
if (markdownTrack !== null && config.markdown !== undefined) {
|
|
867
|
+
markdownTrack.includeCounts.forEach((count, index) => {
|
|
868
|
+
if (count > 0) return;
|
|
869
|
+
deadRows.push(
|
|
870
|
+
`markdown.include[${index}]: '${config.markdown.include[index]}' matches no tracked ` +
|
|
871
|
+
`document — the document track reads nothing, so every marker row below it governs ` +
|
|
872
|
+
`nothing while reading as enforced. Either the path was never right, or the ` +
|
|
873
|
+
`documents are not tracked`,
|
|
874
|
+
);
|
|
875
|
+
});
|
|
876
|
+
markdownTrack.rowMatches.forEach((count, index) => {
|
|
877
|
+
if (count > 0) return;
|
|
878
|
+
deadRows.push(
|
|
879
|
+
`markdown.markers[${index}]: the pattern matches no line in any included document — ` +
|
|
880
|
+
`this row extracts no pairing, so it enforces nothing while reading as enforced. ` +
|
|
881
|
+
`Either the documents do not carry the marker, or the pattern was never right`,
|
|
882
|
+
);
|
|
883
|
+
});
|
|
884
|
+
}
|
|
758
885
|
if (deadRows.length > 0) {
|
|
759
886
|
throw new Error(
|
|
760
887
|
`archkeep: ${policySource ?? "the boundary config"} describes a workspace that does not ` +
|
|
@@ -977,8 +1104,14 @@ export async function check(
|
|
|
977
1104
|
// Complete means the run judged everything in scope: no
|
|
978
1105
|
// whole-file failure (unchecked), no unresolvable site (#595),
|
|
979
1106
|
// and at least one file analyzed (#599 — a run that judged
|
|
980
|
-
// nothing has no verdict to claim).
|
|
981
|
-
|
|
1107
|
+
// nothing has no verdict to claim). Read from the one predicate
|
|
1108
|
+
// `verdictFor`'s decision face reads, so this field and the
|
|
1109
|
+
// envelope's `decision.coverageComplete` are one derivation —
|
|
1110
|
+
// the counts are this command's (the go.work/tsconfig failures
|
|
1111
|
+
// pushed above and the accepted files withdrawn above make the
|
|
1112
|
+
// universe wider than `commandContext.analysis`), the law is
|
|
1113
|
+
// not.
|
|
1114
|
+
complete: coverageComplete({ unchecked, blindSpotCount, analyzed }),
|
|
982
1115
|
projects: Object.keys(graph.nodes).length,
|
|
983
1116
|
analyzedFiles: analyzed,
|
|
984
1117
|
imports: imports.length,
|
|
@@ -1029,6 +1162,31 @@ export async function check(
|
|
|
1029
1162
|
judged: declaredEdges.judged,
|
|
1030
1163
|
findings: declaredEdges.findings,
|
|
1031
1164
|
},
|
|
1165
|
+
// The document track is a policy DECLARATION, so it takes the
|
|
1166
|
+
// same omitted-key-not-null discipline the intent/fitness/
|
|
1167
|
+
// customRules blocks below state: a workspace whose policy
|
|
1168
|
+
// declares no `markdown` block gets no key at all, and its
|
|
1169
|
+
// envelope is byte-identical to the one it got before this
|
|
1170
|
+
// section existed. The findings themselves are NOT restated
|
|
1171
|
+
// here — they are members of `violations` above (sorted with,
|
|
1172
|
+
// suppressible by, and counted into the verdict exactly like
|
|
1173
|
+
// every import-site violation), so an array here would be one
|
|
1174
|
+
// fact counted in two places; the block states what the track
|
|
1175
|
+
// DID: how many documents the globs selected, how many markers
|
|
1176
|
+
// they carried, where each resolution went.
|
|
1177
|
+
...(markdownTrack === null
|
|
1178
|
+
? {}
|
|
1179
|
+
: {
|
|
1180
|
+
markdown: {
|
|
1181
|
+
checked: true,
|
|
1182
|
+
documents: markdownTrack.documents,
|
|
1183
|
+
judged: markdownTrack.judged,
|
|
1184
|
+
resolved: markdownTrack.resolved,
|
|
1185
|
+
...(markdownTrack.selfPaired > 0
|
|
1186
|
+
? { selfPaired: markdownTrack.selfPaired }
|
|
1187
|
+
: {}),
|
|
1188
|
+
},
|
|
1189
|
+
}),
|
|
1032
1190
|
// Intent is a governance DECLARATION, absent when the workspace
|
|
1033
1191
|
// chose not to make one: the key is omitted, never written as
|
|
1034
1192
|
// null — the design contract `docs/reference/json-output.md` will
|
|
@@ -32,16 +32,12 @@
|
|
|
32
32
|
* than explaining constraints from a graph whose edges silently under-represent
|
|
33
33
|
* the real architecture.
|
|
34
34
|
*/
|
|
35
|
-
import {
|
|
36
|
-
blindSpotRows,
|
|
37
|
-
isWholeFileFailure,
|
|
38
|
-
unresolvableLiteralCount,
|
|
39
|
-
} from "../analysis/source-util.mjs";
|
|
40
35
|
import { UsageError } from "../errors.mjs";
|
|
41
36
|
import { judgeEdge } from "../rules/edge-constraints.mjs";
|
|
42
37
|
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
43
38
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
44
39
|
import { formatContextReport } from "../report/context-text.mjs";
|
|
40
|
+
import { coverageVerdict } from "./coverage-verdict.mjs";
|
|
45
41
|
import { resolveProvenance } from "./provenance.mjs";
|
|
46
42
|
import { readAdrContext } from "./adr.mjs";
|
|
47
43
|
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
@@ -150,27 +146,23 @@ export function contextCommand(projectName, commandContext, config) {
|
|
|
150
146
|
);
|
|
151
147
|
}
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const complete =
|
|
163
|
-
notAnalyzed.length === 0 && blindSpotCount === 0 && commandContext.analysis.analyzed > 0;
|
|
164
|
-
const status = complete ? "ok" : "no-verdict";
|
|
165
|
-
const exitCode = complete ? 0 : 3;
|
|
149
|
+
// The completeness verdict is the shared constructor's, not this file's —
|
|
150
|
+
// the same law `graph`/`discover` run (#595, #599): unjudged sites and a
|
|
151
|
+
// zero-analyzed run defeat it here exactly as they do there, so a context
|
|
152
|
+
// report cannot look complete over a tree the run could not fully read
|
|
153
|
+
// while the faces beside it refuse the same tree. The restatement this
|
|
154
|
+
// replaces carried all three axes, so composing it moves no byte on any
|
|
155
|
+
// input.
|
|
156
|
+
const verdict = coverageVerdict(commandContext);
|
|
157
|
+
const { complete, status, exitCode } = verdict;
|
|
166
158
|
|
|
167
159
|
const coverage = {
|
|
168
160
|
complete,
|
|
169
161
|
projects: Object.keys(graph.nodes).length,
|
|
170
162
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
171
163
|
imports: commandContext.analysis.imports.length,
|
|
172
|
-
notAnalyzed,
|
|
173
|
-
blindSpots:
|
|
164
|
+
notAnalyzed: verdict.notAnalyzed,
|
|
165
|
+
blindSpots: verdict.blindSpots,
|
|
174
166
|
notes: [
|
|
175
167
|
"per-edge violations cover only depConstraints (3 of 15 violation types). " +
|
|
176
168
|
"A dependency with no violations here may still violate npm-ban, circular-dependency, " +
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
isWholeFileFailure,
|
|
50
50
|
unresolvableLiteralCount,
|
|
51
51
|
} from "../analysis/source-util.mjs";
|
|
52
|
-
import { EXIT, coverageIncompleteReasons } from "../verdict.mjs";
|
|
52
|
+
import { EXIT, coverageComplete, coverageIncompleteReasons } from "../verdict.mjs";
|
|
53
53
|
import { buildDecision } from "../report/evidence.mjs";
|
|
54
54
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
55
55
|
import { formatCoverageIncomplete } from "../report/text.mjs";
|
|
@@ -96,7 +96,17 @@ export function coverageVerdict(commandContext, { acceptedFiles } = {}) {
|
|
|
96
96
|
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
97
97
|
const blindSpots = blindSpotRows(analysis.failures);
|
|
98
98
|
const blindSpotCount = unresolvableLiteralCount(analysis.failures);
|
|
99
|
-
|
|
99
|
+
// The conjunction is `../verdict.mjs`'s `coverageComplete` — the same
|
|
100
|
+
// predicate `verdictFor`'s decision face reads, so `check`'s envelope
|
|
101
|
+
// cannot carry a `coverage.complete` and a `decision.coverageComplete`
|
|
102
|
+
// that disagree about one run. The predicate is defined there, not here,
|
|
103
|
+
// because `verdictFor` needs it and this module already imports from that
|
|
104
|
+
// one: the reverse import would be a cycle.
|
|
105
|
+
const complete = coverageComplete({
|
|
106
|
+
unchecked: notAnalyzed.length,
|
|
107
|
+
blindSpotCount,
|
|
108
|
+
analyzed: analysis.analyzed,
|
|
109
|
+
});
|
|
100
110
|
return {
|
|
101
111
|
notAnalyzed,
|
|
102
112
|
blindSpots,
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
import { readFileSync } from "node:fs";
|
|
49
49
|
|
|
50
50
|
import { canonicalJsonReplacer } from "../canonical.mjs";
|
|
51
|
+
import { isPlainObject } from "../values.mjs";
|
|
51
52
|
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
52
53
|
|
|
53
54
|
/** The only snapshot schemaVersion this module writes and reads. */
|
|
@@ -518,11 +519,6 @@ export function providerMismatch(baselineProvider, currentProvider) {
|
|
|
518
519
|
);
|
|
519
520
|
}
|
|
520
521
|
|
|
521
|
-
/** Non-empty plain-object guard used across validation. */
|
|
522
|
-
function isPlainObject(value) {
|
|
523
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
522
|
/** Describes a value for error messages without dumping it. */
|
|
527
523
|
function describe(value) {
|
|
528
524
|
if (value === null) return "null";
|
package/src/commands/explain.mjs
CHANGED
|
@@ -79,16 +79,13 @@
|
|
|
79
79
|
* was: the field, and its rendered lines, exist only when the comparison was
|
|
80
80
|
* requested.
|
|
81
81
|
*/
|
|
82
|
-
import {
|
|
83
|
-
blindSpotRows,
|
|
84
|
-
isWholeFileFailure,
|
|
85
|
-
unresolvableLiteralCount,
|
|
86
|
-
} from "../analysis/source-util.mjs";
|
|
82
|
+
import { isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
87
83
|
import { UsageError } from "../errors.mjs";
|
|
88
84
|
import { evaluate } from "../rules/index.mjs";
|
|
89
85
|
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
90
86
|
import { findProjectForPath, createProjectRootMappings } from "../rules/specifiers.mjs";
|
|
91
87
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
88
|
+
import { coverageVerdict } from "./coverage-verdict.mjs";
|
|
92
89
|
import { formatExplainReport } from "../report/explain-text.mjs";
|
|
93
90
|
import { resolveProvenance } from "./provenance.mjs";
|
|
94
91
|
import { readAdrContext } from "./adr.mjs";
|
|
@@ -369,18 +366,18 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
369
366
|
// Normalize backslash separators (Windows paths) to forward slashes.
|
|
370
367
|
parsed.sourceFile = sep === "\\" ? normalizedFile.replaceAll("\\", "/") : normalizedFile;
|
|
371
368
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
369
|
+
// The completeness verdict is the shared constructor's, not this file's —
|
|
370
|
+
// the same contract `graph`/`discover` run: an unresolvable site was seen
|
|
371
|
+
// but never judged (#595), the graph is missing whatever edge it would have
|
|
372
|
+
// drawn, and rules that judge the whole graph (circularity, lazy loading)
|
|
373
|
+
// would answer over a gap. The explanation still reports — status
|
|
374
|
+
// no-verdict — naming the site in `coverage.blindSpots`. The restatement
|
|
375
|
+
// this replaces carried two of the constructor's three axes; the third
|
|
376
|
+
// (`analyzed > 0`, #599) moves no byte here, because every lane below that
|
|
377
|
+
// builds an envelope already implies a read file — an import record or a
|
|
378
|
+
// positioned failure exists only for a file the run analyzed — so the axis
|
|
379
|
+
// is carried by composition, not changed by it.
|
|
380
|
+
const { notAnalyzed, blindSpots, complete, status, exitCode } = coverageVerdict(commandContext);
|
|
384
381
|
|
|
385
382
|
// Find the import record at this site.
|
|
386
383
|
const record = findSite(parsed, commandContext.analysis.imports);
|
|
@@ -432,7 +429,7 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
432
429
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
433
430
|
imports: commandContext.analysis.imports.length,
|
|
434
431
|
notAnalyzed,
|
|
435
|
-
blindSpots
|
|
432
|
+
blindSpots,
|
|
436
433
|
notes: [],
|
|
437
434
|
};
|
|
438
435
|
|
|
@@ -447,7 +444,7 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
447
444
|
command: "explain",
|
|
448
445
|
context,
|
|
449
446
|
status,
|
|
450
|
-
exitCode
|
|
447
|
+
exitCode,
|
|
451
448
|
coverage,
|
|
452
449
|
result,
|
|
453
450
|
});
|
|
@@ -579,7 +576,7 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
579
576
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
580
577
|
imports: commandContext.analysis.imports.length,
|
|
581
578
|
notAnalyzed,
|
|
582
|
-
blindSpots
|
|
579
|
+
blindSpots,
|
|
583
580
|
notes: [],
|
|
584
581
|
};
|
|
585
582
|
|
package/src/commands/graph.mjs
CHANGED
|
@@ -180,6 +180,13 @@ export function computePolicyFingerprint(config) {
|
|
|
180
180
|
suppressions: config.suppressions ?? [],
|
|
181
181
|
...(config.fitness === undefined ? {} : { fitness: config.fitness }),
|
|
182
182
|
...(config.customRules === undefined ? {} : { customRules: config.customRules }),
|
|
183
|
+
// The document track is law the same way the two blocks above are: it
|
|
184
|
+
// decides what this run judges, so a policy that adds or edits a
|
|
185
|
+
// `markdown` block must not share a fingerprint with one that does not —
|
|
186
|
+
// `diff`'s policy-changed warning reads this hash. Conditional, like the
|
|
187
|
+
// two above, so a policy declaring no block hashes exactly as it did
|
|
188
|
+
// before this key existed.
|
|
189
|
+
...(config.markdown === undefined ? {} : { markdown: config.markdown }),
|
|
183
190
|
};
|
|
184
191
|
// Canonicalise: sort object keys at every depth so insertion order does not
|
|
185
192
|
// affect the hash. Semantic equality, not construction order, is the claim —
|
package/src/commands/health.mjs
CHANGED
|
@@ -112,10 +112,14 @@ export function healthCommand(commandContext, io = {}) {
|
|
|
112
112
|
const edges = buildDependencies(graph.dependencies);
|
|
113
113
|
|
|
114
114
|
// The run's coverage facts, the same shape every command's envelope carries.
|
|
115
|
+
// A run that analyzed nothing judged nothing (#599, #694): judging nothing
|
|
116
|
+
// is not finding nothing, so it defeats completeness the way a whole-file
|
|
117
|
+
// failure does.
|
|
115
118
|
// An unresolvable site is a fact the run saw but never judged (#595) —
|
|
116
119
|
// metrics measured over it would read precision the run does not have,
|
|
117
120
|
// so it defeats file completeness the way a whole-file failure does.
|
|
118
121
|
const fileComplete =
|
|
122
|
+
analysis.analyzed > 0 &&
|
|
119
123
|
analysis.failures.filter(isWholeFileFailure).length === 0 &&
|
|
120
124
|
unresolvableLiteralCount(analysis.failures) === 0;
|
|
121
125
|
// The graph is complete only when the files are AND the graph actually sees
|
|
@@ -501,10 +501,14 @@ export async function planContextCommand(
|
|
|
501
501
|
(a.messageId < b.messageId ? -1 : a.messageId > b.messageId ? 1 : 0),
|
|
502
502
|
);
|
|
503
503
|
|
|
504
|
+
// A run that analyzed nothing judged nothing (#599, #694): judging nothing
|
|
505
|
+
// is not finding nothing, so it defeats completeness the way a whole-file
|
|
506
|
+
// failure does.
|
|
504
507
|
// An unresolvable literal site is work the run saw but never judged
|
|
505
508
|
// (#595, narrowed): a plan over it would present edges the run does not
|
|
506
509
|
// hold, so it defeats completeness the way a whole-file failure does.
|
|
507
|
-
const complete =
|
|
510
|
+
const complete =
|
|
511
|
+
wholeTree.analyzed > 0 && notAnalyzed.length === 0 && unresolvableLiteralCount(failures) === 0;
|
|
508
512
|
const status = complete ? "ok" : "no-verdict";
|
|
509
513
|
const exitCode = complete ? 0 : 3;
|
|
510
514
|
|
package/src/commands/policy.mjs
CHANGED
|
@@ -86,10 +86,10 @@ export function hasProfiles(options) {
|
|
|
86
86
|
* @param {string} cwd The process's working directory a relative `--config`
|
|
87
87
|
* resolves against — kept separate from the workspace root for the reason
|
|
88
88
|
* above.
|
|
89
|
-
* @returns {Promise<{config: {depConstraints: object[], options: object, suppressions: object[], fitness?: object[], customRules?: object[], coverage?: object, notes?: string[]}|null, profile: string|null, source: string|null}>}
|
|
90
|
-
* `fitness` and `
|
|
91
|
-
* declares them — an absent key is the workspace's decision not to
|
|
92
|
-
* that law, never an empty one (`../config.mjs`'s `policyFrom`).
|
|
89
|
+
* @returns {Promise<{config: {depConstraints: object[], options: object, suppressions: object[], fitness?: object[], customRules?: object[], coverage?: object, markdown?: {include: string[], markers: {pattern: string, edge: string}[]}, notes?: string[]}|null, profile: string|null, source: string|null}>}
|
|
90
|
+
* `fitness`, `customRules` and `markdown` are present only when the resolved
|
|
91
|
+
* policy declares them — an absent key is the workspace's decision not to
|
|
92
|
+
* declare that law, never an empty one (`../config.mjs`'s `policyFrom`).
|
|
93
93
|
* @throws {Error} when a named profile, a `--config` file, or an inline
|
|
94
94
|
* policy cannot be resolved or is malformed — every arm's existing failure
|
|
95
95
|
* mode, unchanged by the extraction.
|
|
@@ -114,8 +114,14 @@ export function resolveProvenance(root) {
|
|
|
114
114
|
|
|
115
115
|
// Dirty: any uncommitted change to tracked files means the working tree
|
|
116
116
|
// does not match the commit. A baseline from a dirty tree is not a
|
|
117
|
-
// reproducible claim about that commit.
|
|
118
|
-
|
|
117
|
+
// reproducible claim about that commit. `--untracked-files=no` is what
|
|
118
|
+
// makes the code agree with that sentence: bare `--porcelain` includes
|
|
119
|
+
// untracked paths, and an untracked file is not an uncommitted change to a
|
|
120
|
+
// tracked file — the analysis reads `git ls-files`-tracked files only, so a
|
|
121
|
+
// tree whose only dirt is an editor swap, a scratch file, or an unignored
|
|
122
|
+
// build output has an unchanged analyzed input and must produce an
|
|
123
|
+
// unchanged envelope (#683).
|
|
124
|
+
const status = runProcess("git", ["status", "--porcelain", "--untracked-files=no"], root).trim();
|
|
119
125
|
const dirty = status.length > 0;
|
|
120
126
|
|
|
121
127
|
return { commit, remote, dirty };
|