@ecoma-io/archkeep 0.21.0 → 0.22.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/cli.mjs +156 -66
- package/gate-attestation.mjs +23 -0
- package/package.json +3 -1
- package/src/analysis/analyze.mjs +6 -0
- package/src/analysis/contract.md +32 -5
- package/src/analysis/csharp.mjs +18 -0
- package/src/analysis/go.mjs +18 -0
- package/src/analysis/java.mjs +15 -0
- package/src/analysis/kotlin.mjs +15 -0
- package/src/analysis/python.mjs +25 -3
- package/src/analysis/rust.mjs +18 -0
- package/src/analysis/source-util.mjs +113 -0
- package/src/analysis/typescript.mjs +86 -5
- package/src/canonical.mjs +43 -25
- package/src/commands/README.md +63 -12
- package/src/commands/change-intent.mjs +25 -1
- package/src/commands/change.mjs +90 -40
- package/src/commands/check.mjs +65 -26
- package/src/commands/completeness.mjs +126 -19
- package/src/commands/context-command.mjs +13 -5
- package/src/commands/context.mjs +31 -4
- package/src/commands/coverage-verdict.mjs +191 -0
- package/src/commands/debt.mjs +18 -15
- package/src/commands/delta-classify.mjs +13 -18
- package/src/commands/delta-snapshot.mjs +13 -5
- package/src/commands/delta.mjs +95 -33
- package/src/commands/diff.mjs +31 -24
- package/src/commands/discover.mjs +70 -29
- package/src/commands/drift.mjs +21 -21
- package/src/commands/edge-constraints.mjs +47 -1
- package/src/commands/evaluation-primitives.mjs +194 -2
- package/src/commands/evolution.mjs +27 -10
- package/src/commands/explain.mjs +14 -13
- package/src/commands/fitness.mjs +20 -19
- package/src/commands/graph.mjs +29 -11
- package/src/commands/health.mjs +12 -5
- package/src/commands/history.mjs +41 -26
- package/src/commands/impact.mjs +17 -18
- package/src/commands/plan-context-command.mjs +10 -5
- package/src/commands/reconcile.mjs +14 -17
- package/src/commands/scenario-evaluation.mjs +93 -16
- package/src/commands/scenario.mjs +28 -18
- package/src/commands/waivers.mjs +36 -28
- package/src/governance/evolution-event.mjs +96 -9
- package/src/intent/intent-manifest.json +83 -39
- package/src/lsp/diagnose.mjs +12 -3
- package/src/report/discover-text.mjs +31 -9
- package/src/report/graph-text.mjs +25 -5
- package/src/report/json.mjs +32 -5
- package/src/report/text.mjs +82 -12
- package/src/verdict.mjs +78 -36
- package/src/verify-gate-attestation.mjs +323 -0
- package/src/workspace.mjs +126 -2
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
* than explaining constraints from a graph whose edges silently under-represent
|
|
33
33
|
* the real architecture.
|
|
34
34
|
*/
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
blindSpotRows,
|
|
37
|
+
isWholeFileFailure,
|
|
38
|
+
unresolvableLiteralCount,
|
|
39
|
+
} from "../analysis/source-util.mjs";
|
|
36
40
|
import { UsageError } from "../errors.mjs";
|
|
37
41
|
import { judgeEdge } from "./edge-constraints.mjs";
|
|
38
42
|
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
@@ -150,7 +154,13 @@ export function contextCommand(projectName, commandContext, config) {
|
|
|
150
154
|
.filter(isWholeFileFailure)
|
|
151
155
|
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
// The same completeness `check` claims (#595, #599): unjudged sites and
|
|
158
|
+
// a zero-analyzed run defeat it here exactly as they do there, so a
|
|
159
|
+
// context report cannot look complete over a tree the run could not
|
|
160
|
+
// fully read.
|
|
161
|
+
const blindSpotCount = unresolvableLiteralCount(commandContext.analysis.failures);
|
|
162
|
+
const complete =
|
|
163
|
+
notAnalyzed.length === 0 && blindSpotCount === 0 && commandContext.analysis.analyzed > 0;
|
|
154
164
|
const status = complete ? "ok" : "no-verdict";
|
|
155
165
|
const exitCode = complete ? 0 : 3;
|
|
156
166
|
|
|
@@ -160,9 +170,7 @@ export function contextCommand(projectName, commandContext, config) {
|
|
|
160
170
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
161
171
|
imports: commandContext.analysis.imports.length,
|
|
162
172
|
notAnalyzed,
|
|
163
|
-
blindSpots: commandContext.analysis.failures
|
|
164
|
-
.filter((f) => !isWholeFileFailure(f))
|
|
165
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
173
|
+
blindSpots: blindSpotRows(commandContext.analysis.failures),
|
|
166
174
|
notes: [
|
|
167
175
|
"per-edge violations cover only depConstraints (3 of 15 violation types). " +
|
|
168
176
|
"A dependency with no violations here may still violate npm-ban, circular-dependency, " +
|
package/src/commands/context.mjs
CHANGED
|
@@ -214,11 +214,14 @@ export const WORKSPACE_MARKERS = [
|
|
|
214
214
|
* `createWorkspace` returns.
|
|
215
215
|
* @property {string[]} tracked Every tracked file, from `listFiles(root)`.
|
|
216
216
|
* @property {{imports: object[], failures: object[], analyzed: number,
|
|
217
|
-
* analyzedFiles: string[], exemptedFiles: string[]
|
|
217
|
+
* analyzedFiles: string[], exemptedFiles: string[],
|
|
218
|
+
* unsupportedLanguageFiles: string[]}} analysis The
|
|
218
219
|
* whole-tree-then-scoped (native) or scoped-then-analyzed (nx/moon) result —
|
|
219
220
|
* see the branches below for why the order is not the same on all three.
|
|
220
221
|
* `exemptedFiles` is always `[]` on nx/moon: `coverage.exempt` is a
|
|
221
|
-
* native-only `archkeep.json` key.
|
|
222
|
+
* native-only `archkeep.json` key. `unsupportedLanguageFiles` is scoped to
|
|
223
|
+
* the selected files on every branch (#601): a gap row names only files the
|
|
224
|
+
* selected scope could have judged.
|
|
222
225
|
* @property {{boundaryConfig: string|object, tsConfig: object|undefined,
|
|
223
226
|
* boundaryConfigDeclared: boolean, profiles?: string, inline?: boolean}} options
|
|
224
227
|
* What this workspace names its boundary law, its shared tsconfig, and —
|
|
@@ -549,6 +552,7 @@ export function resolveCommandContext(
|
|
|
549
552
|
let failures;
|
|
550
553
|
let analyzed;
|
|
551
554
|
let analyzedFiles;
|
|
555
|
+
let unsupportedLanguageFiles;
|
|
552
556
|
let pluginGap;
|
|
553
557
|
let unownedGap;
|
|
554
558
|
let unclaimedGap;
|
|
@@ -664,6 +668,12 @@ export function resolveCommandContext(
|
|
|
664
668
|
...jvmIndexFailures(workspace),
|
|
665
669
|
]);
|
|
666
670
|
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
671
|
+
// Scoped exactly like `analyzedFiles` above (#601): a gap row must name
|
|
672
|
+
// only files the selected scope could have judged, and leaving this
|
|
673
|
+
// undefined here would crash `check`'s gap assembly instead of reporting.
|
|
674
|
+
unsupportedLanguageFiles = wholeTreeAnalysis.unsupportedLanguageFiles.filter((file) =>
|
|
675
|
+
selectedFiles.has(file),
|
|
676
|
+
);
|
|
667
677
|
analyzed = analyzedFiles.length;
|
|
668
678
|
// Unaffected by `paths`: an exempted file is by definition unowned by any
|
|
669
679
|
// project, so it was never a candidate for `owned`/`selected` in the
|
|
@@ -787,6 +797,13 @@ export function resolveCommandContext(
|
|
|
787
797
|
]);
|
|
788
798
|
unclaimedGap = { files: unclaimedFiles };
|
|
789
799
|
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
800
|
+
// Scoped exactly like the native branch's own filter above (#601): a gap
|
|
801
|
+
// row must name only files the selected scope could have judged, and
|
|
802
|
+
// leaving this undefined here would crash `check`'s gap assembly instead
|
|
803
|
+
// of reporting.
|
|
804
|
+
unsupportedLanguageFiles = wholeTreeAnalysis.unsupportedLanguageFiles.filter((file) =>
|
|
805
|
+
selectedFiles.has(file),
|
|
806
|
+
);
|
|
790
807
|
analyzed = analyzedFiles.length;
|
|
791
808
|
// `coverage.exempt` is a native-only key (`../providers/native/coverage.mjs`'s
|
|
792
809
|
// header: "Nx has no equivalent question") — Moon carries no such list.
|
|
@@ -835,7 +852,10 @@ export function resolveCommandContext(
|
|
|
835
852
|
paths,
|
|
836
853
|
{ root, cwd, tracked },
|
|
837
854
|
);
|
|
838
|
-
({ imports, failures, analyzed, analyzedFiles } = analyzeWorkspace(
|
|
855
|
+
({ imports, failures, analyzed, analyzedFiles, unsupportedLanguageFiles } = analyzeWorkspace(
|
|
856
|
+
workspace,
|
|
857
|
+
selected,
|
|
858
|
+
));
|
|
839
859
|
// Unclaimed analyzable files — `unclaimedFileFailures` above — join
|
|
840
860
|
// unconditionally, the same workspace-wide posture native's own
|
|
841
861
|
// `discovered.failures` has, so a scoped `check <path>` cannot hide an
|
|
@@ -876,7 +896,14 @@ export function resolveCommandContext(
|
|
|
876
896
|
graph,
|
|
877
897
|
workspace,
|
|
878
898
|
tracked,
|
|
879
|
-
analysis: {
|
|
899
|
+
analysis: {
|
|
900
|
+
imports,
|
|
901
|
+
failures,
|
|
902
|
+
analyzed,
|
|
903
|
+
analyzedFiles,
|
|
904
|
+
exemptedFiles,
|
|
905
|
+
unsupportedLanguageFiles,
|
|
906
|
+
},
|
|
880
907
|
options,
|
|
881
908
|
pluginGap,
|
|
882
909
|
unownedGap,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE incomplete-coverage refusal contract, and the one completeness
|
|
3
|
+
* every command's verdict is a claim about (#608).
|
|
4
|
+
*
|
|
5
|
+
* #602 gave the graph family (`check`, `graph`, `discover`, `explain`,
|
|
6
|
+
* `context`) a structured refusal: `status: "no-verdict"`, exit 3, a
|
|
7
|
+
* `coverage` block naming what the run could not read — in the envelope,
|
|
8
|
+
* where a parser and `--output` can read it. Every command here that refuses
|
|
9
|
+
* the same condition refused it by throwing: a stderr sentence, an empty
|
|
10
|
+
* stdout, no envelope, nothing under `--output`. Same refusal class, two
|
|
11
|
+
* machine contracts — while `docs/reference/exit-codes.md` already documents
|
|
12
|
+
* the structured one for every descriptive command. The functions here are
|
|
13
|
+
* the shared construction the throw family now goes through, so the two
|
|
14
|
+
* families cannot drift again:
|
|
15
|
+
*
|
|
16
|
+
* - `coverageVerdict` computes the one completeness over three axes — no
|
|
17
|
+
* whole-file failure, no unjudged blind spot, at least one file analyzed —
|
|
18
|
+
* and the status/exit pair that completeness implies. The first two axes
|
|
19
|
+
* ride the envelope law (`../report/json.mjs`), and there only in ONE
|
|
20
|
+
* direction: the envelope refuses `complete: true` over a non-empty
|
|
21
|
+
* `notAnalyzed` list or an unjudged blind-spot row, and deliberately
|
|
22
|
+
* allows `complete: false` with both lists empty, so a run that judged
|
|
23
|
+
* nothing is able to say so. The third axis — `analyzed > 0` — is the one
|
|
24
|
+
* this constructor adds on top (#599, #612, #619): the envelope has no
|
|
25
|
+
* zero-analysis refusal by design, which is WHY this constructor exists.
|
|
26
|
+
* The failure classes are read through
|
|
27
|
+
* `../analysis/source-util.mjs`'s classifiers, never an inline filter: the
|
|
28
|
+
* class line is theirs alone to own.
|
|
29
|
+
* - `coverageRefusal` builds the refusal result itself: the envelope through
|
|
30
|
+
* `jsonEnvelope` (so the envelope invariants hold by construction, and a
|
|
31
|
+
* refusal cannot accidentally claim `complete: true`), and a text face
|
|
32
|
+
* whose coverage lines come from `coverageIncompleteReasons` — the same
|
|
33
|
+
* clauses `check`'s text report renders — so a terminal reader is told the
|
|
34
|
+
* same thing the envelope tells a parser.
|
|
35
|
+
*
|
|
36
|
+
* What still throws: the plugin-gap refusal (a graph that under-represents
|
|
37
|
+
* the workspace is a different refusal, kept loud in every command that has
|
|
38
|
+
* one — `drift.mjs`'s `refuseIncompleteGraph` and `delta.mjs`'s
|
|
39
|
+
* `refusePluginGapHead` are the two shared guards), input refusals (a
|
|
40
|
+
* malformed baseline file, a missing intent), and the capture modes
|
|
41
|
+
* (`delta --capture`, `history --capture`) whose product is a snapshot file
|
|
42
|
+
* with no envelope contract to refuse through.
|
|
43
|
+
*
|
|
44
|
+
* @module
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
blindSpotRows,
|
|
49
|
+
isWholeFileFailure,
|
|
50
|
+
unresolvableLiteralCount,
|
|
51
|
+
} from "../analysis/source-util.mjs";
|
|
52
|
+
import { EXIT, coverageIncompleteReasons } from "../verdict.mjs";
|
|
53
|
+
import { buildDecision } from "../report/evidence.mjs";
|
|
54
|
+
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
55
|
+
import { formatCoverageIncomplete } from "../report/text.mjs";
|
|
56
|
+
import { resolveProvenance } from "./provenance.mjs";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {object} CoverageVerdict
|
|
60
|
+
* @property {{file: string, reason: string}[]} notAnalyzed One row per
|
|
61
|
+
* whole-file failure — the files the run never read.
|
|
62
|
+
* @property {object[]} blindSpots Every positioned failure, all permanent
|
|
63
|
+
* classes — the run's disclosure of every site it saw and did not judge.
|
|
64
|
+
* @property {number} blindSpotCount The count of sites that WITHHOLD the
|
|
65
|
+
* verdict (unresolvable literals referencing the workspace; dynamic and
|
|
66
|
+
* external sites disclosed but exit-neutral).
|
|
67
|
+
* @property {boolean} complete Whether the run judged everything in scope.
|
|
68
|
+
* @property {"ok"|"no-verdict"} status The verdict the completeness implies.
|
|
69
|
+
* @property {0|3} exitCode The exit code the status implies.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The one completeness verdict over a command context's analysis.
|
|
74
|
+
*
|
|
75
|
+
* The three completeness axes are exactly the envelope law's: a whole-file
|
|
76
|
+
* failure is a file no verdict was reached on; an unresolvable literal site
|
|
77
|
+
* is a question the resolver was asked and could not answer; a zero-analyzed
|
|
78
|
+
* run judged nothing (#599). Dynamic and external sites are declared limits —
|
|
79
|
+
* disclosed in `blindSpots`, never withholding — because
|
|
80
|
+
* `unresolvableLiteralCount` draws that class line once, and this function
|
|
81
|
+
* reads the count rather than re-deriving it.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} commandContext From `resolveCommandContext`.
|
|
84
|
+
* @param {{acceptedFiles?: Set<string>}} [io] `acceptedFiles` names whole-
|
|
85
|
+
* file failures a `coverage.unowned` row already accepts (`waivers` and
|
|
86
|
+
* `check` withdraw those first): the state is a recorded acceptance the
|
|
87
|
+
* report itself names, not a hole the run failed to look at. Absent ⇒ no
|
|
88
|
+
* withdrawal, which is every command but `waivers`.
|
|
89
|
+
* @returns {CoverageVerdict}
|
|
90
|
+
*/
|
|
91
|
+
export function coverageVerdict(commandContext, { acceptedFiles } = {}) {
|
|
92
|
+
const { analysis } = commandContext;
|
|
93
|
+
const notAnalyzed = analysis.failures
|
|
94
|
+
.filter(isWholeFileFailure)
|
|
95
|
+
.filter(({ sourceFile }) => !(acceptedFiles?.has(sourceFile) ?? false))
|
|
96
|
+
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
97
|
+
const blindSpots = blindSpotRows(analysis.failures);
|
|
98
|
+
const blindSpotCount = unresolvableLiteralCount(analysis.failures);
|
|
99
|
+
const complete = notAnalyzed.length === 0 && blindSpotCount === 0 && analysis.analyzed > 0;
|
|
100
|
+
return {
|
|
101
|
+
notAnalyzed,
|
|
102
|
+
blindSpots,
|
|
103
|
+
blindSpotCount,
|
|
104
|
+
complete,
|
|
105
|
+
status: complete ? "ok" : "no-verdict",
|
|
106
|
+
exitCode: complete ? EXIT.ok : EXIT.error,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The structured refusal itself: what a command returns over an analysis it
|
|
112
|
+
* could not fully read, instead of throwing.
|
|
113
|
+
*
|
|
114
|
+
* The envelope carries no `result`: the verdict was withheld, so there is no
|
|
115
|
+
* verdict payload to ride beside the refusal — the `coverage` block IS the
|
|
116
|
+
* payload, and `jsonEnvelope`'s law would refuse any `ok`-shaped claim this
|
|
117
|
+
* object might otherwise smuggle through. `decision` is accepted for the two
|
|
118
|
+
* commands whose envelopes already carry one (`delta`, `change`); an
|
|
119
|
+
* "unknown" decision must name its reason (I3), so callers pass `true` and
|
|
120
|
+
* the reason is joined from the same clause list the text face renders.
|
|
121
|
+
*
|
|
122
|
+
* @param {{command: string, commandContext: object, what: string,
|
|
123
|
+
* acceptedFiles?: Set<string>, decision?: boolean}} input
|
|
124
|
+
* `command` is the envelope's command name, `what` names the question the
|
|
125
|
+
* command was asking (the text face's one free clause), `acceptedFiles`
|
|
126
|
+
* forwards to `coverageVerdict` (see above), and `decision: true` attaches
|
|
127
|
+
* the canonical `unknown` decision.
|
|
128
|
+
* @returns {{status: "no-verdict", coverage: object,
|
|
129
|
+
* report: {text: string, json: string}}}
|
|
130
|
+
*/
|
|
131
|
+
export function coverageRefusal({ command, commandContext, what, acceptedFiles, decision }) {
|
|
132
|
+
const verdict = coverageVerdict(commandContext, { acceptedFiles });
|
|
133
|
+
// A refusal over a complete coverage would be a `no-verdict` claim about a
|
|
134
|
+
// run that looked at everything — the silent direction in the other dress.
|
|
135
|
+
// Every caller gates on `coverageVerdict(...).complete` first; this is the
|
|
136
|
+
// one place that refuses to build the lie if a caller stops gating.
|
|
137
|
+
if (verdict.complete) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`coverageRefusal: ${command}'s coverage is complete — there is nothing to refuse`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
/** @type {"no-verdict"} The completeness is false, so this is the status. */
|
|
143
|
+
const status = "no-verdict";
|
|
144
|
+
const reasons = coverageIncompleteReasons({
|
|
145
|
+
unchecked: verdict.notAnalyzed.length,
|
|
146
|
+
blindSpots: verdict.blindSpotCount,
|
|
147
|
+
analyzed: commandContext.analysis.analyzed,
|
|
148
|
+
});
|
|
149
|
+
const coverage = {
|
|
150
|
+
complete: false,
|
|
151
|
+
projects: Object.keys(commandContext.graph.nodes).length,
|
|
152
|
+
analyzedFiles: commandContext.analysis.analyzed,
|
|
153
|
+
imports: commandContext.analysis.imports.length,
|
|
154
|
+
notAnalyzed: verdict.notAnalyzed,
|
|
155
|
+
blindSpots: verdict.blindSpots,
|
|
156
|
+
notes: [],
|
|
157
|
+
};
|
|
158
|
+
const envelope = jsonEnvelope({
|
|
159
|
+
command,
|
|
160
|
+
context: {
|
|
161
|
+
root: commandContext.root,
|
|
162
|
+
provider: commandContext.provider,
|
|
163
|
+
marker: commandContext.marker,
|
|
164
|
+
provenance: resolveProvenance(commandContext.root),
|
|
165
|
+
},
|
|
166
|
+
status,
|
|
167
|
+
exitCode: verdict.exitCode,
|
|
168
|
+
coverage,
|
|
169
|
+
result: undefined,
|
|
170
|
+
...(decision === true
|
|
171
|
+
? {
|
|
172
|
+
decision: buildDecision({
|
|
173
|
+
status,
|
|
174
|
+
coverageComplete: false,
|
|
175
|
+
findings: 0,
|
|
176
|
+
reason: reasons.join("; "),
|
|
177
|
+
}),
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
status,
|
|
183
|
+
coverage,
|
|
184
|
+
report: {
|
|
185
|
+
text:
|
|
186
|
+
`${command}: no verdict — ${what} needs a workspace this run could fully read\n` +
|
|
187
|
+
formatCoverageIncomplete(reasons),
|
|
188
|
+
json: renderJson(envelope),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
package/src/commands/debt.mjs
CHANGED
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
* when the ledger was taken; the aging itself is snapshot-relative
|
|
46
46
|
* (`../governance/debt-ledger.mjs`).
|
|
47
47
|
*/
|
|
48
|
-
import {
|
|
48
|
+
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
49
49
|
import { judgeIntent } from "../architecture-intent/judge.mjs";
|
|
50
50
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
51
51
|
import { computeDebtLedger } from "../governance/debt-ledger.mjs";
|
|
52
52
|
import { readEvents } from "../governance/evolution-store.mjs";
|
|
53
53
|
import { formatDebtReport } from "../report/debt-text.mjs";
|
|
54
|
+
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
54
55
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
55
56
|
import { resolveProvenance } from "./provenance.mjs";
|
|
56
57
|
import { buildObserved, refuseIncompleteGraph } from "./drift.mjs";
|
|
@@ -75,9 +76,12 @@ import { readSnapshots } from "./history.mjs";
|
|
|
75
76
|
* the `history`/`drift` pattern. `events` is the event-store directory the
|
|
76
77
|
* `--events <dir>` CLI flag resolves; absent ⇒ no lifecycle refs (the
|
|
77
78
|
* ledger says so in `lifecycle.note`), never guessed.
|
|
78
|
-
* @returns {Promise<{status: "ok", ledger
|
|
79
|
+
* @returns {Promise<{status: "ok"|"no-verdict", ledger?: object, coverage: object,
|
|
79
80
|
* report: {text: string, json: string}}>}
|
|
80
|
-
*
|
|
81
|
+
* `status: "no-verdict"` carries no `ledger` payload — the verdict was
|
|
82
|
+
* withheld, and the envelope's `coverage` block is the whole answer (#608).
|
|
83
|
+
* @throws {Error} on every condition the header lists except the coverage one,
|
|
84
|
+
* which returns instead of throwing.
|
|
81
85
|
*/
|
|
82
86
|
export async function debtCommand(dir, commandContext, options = {}) {
|
|
83
87
|
const { root, provider, marker, analysis } = commandContext;
|
|
@@ -86,15 +90,16 @@ export async function debtCommand(dir, commandContext, options = {}) {
|
|
|
86
90
|
|
|
87
91
|
refuseIncompleteGraph(commandContext);
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
// The ledger is a claim about the tree the run read, refused through the one
|
|
94
|
+
// structured contract `./coverage-verdict.mjs` builds (#608). This gate used
|
|
95
|
+
// to check whole-file failures only and then claim `coverage.complete: true`
|
|
96
|
+
// beside `blindSpots` that could carry an unjudged site — which the envelope
|
|
97
|
+
// law refuses as a programming error, so a blind-spot-only tree crashed
|
|
98
|
+
// here. The unified completeness refuses it as the verdict-withholding state
|
|
99
|
+
// it is, before the snapshot directory is read.
|
|
100
|
+
const completeness = coverageVerdict(commandContext);
|
|
101
|
+
if (!completeness.complete) {
|
|
102
|
+
return coverageRefusal({ command: "debt", commandContext, what: "aging the debt ledger" });
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
// The snapshot directory is the sole source of truth for aging; a directory
|
|
@@ -173,9 +178,7 @@ export async function debtCommand(dir, commandContext, options = {}) {
|
|
|
173
178
|
analyzedFiles: analysis.analyzed,
|
|
174
179
|
imports: analysis.imports.length,
|
|
175
180
|
notAnalyzed: [],
|
|
176
|
-
blindSpots: analysis.failures
|
|
177
|
-
.filter((failure) => !isWholeFileFailure(failure))
|
|
178
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
181
|
+
blindSpots: blindSpotRows(analysis.failures),
|
|
179
182
|
// The ledger names what it could not age — a reader can tell "aged" from
|
|
180
183
|
// "observed, not yet aged". The second note discloses `sampleTime`'s own
|
|
181
184
|
// nature: it reflects the wall clock at the moment of THIS run, not the
|
|
@@ -593,20 +593,11 @@ export function deltaFindings(delta) {
|
|
|
593
593
|
};
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
* use — a second spelling somewhere would be a second definition of "same
|
|
602
|
-
* edge", and two definitions drift.
|
|
603
|
-
*
|
|
604
|
-
* @param {{source: string, target: string, type: string}} edge
|
|
605
|
-
* @returns {string}
|
|
606
|
-
*/
|
|
607
|
-
export function edgeEvolutionIdentity({ source, target, type }) {
|
|
608
|
-
return `${source}>${target}:${type}`;
|
|
609
|
-
}
|
|
596
|
+
// The identity spelling's ONE home is `../governance/evolution-event.mjs` —
|
|
597
|
+
// `classifyEvolution` owns it and maps every `observed.edges` entry through
|
|
598
|
+
// it. This re-export keeps delta's importers on the same name; it defines
|
|
599
|
+
// nothing.
|
|
600
|
+
export { edgeEvolutionIdentity } from "../governance/evolution-event.mjs";
|
|
610
601
|
|
|
611
602
|
/**
|
|
612
603
|
* The delta event's per-constraint verdict deltas (design §1 `fitness`),
|
|
@@ -675,12 +666,16 @@ export function deltaVerdictDeltas(delta) {
|
|
|
675
666
|
*
|
|
676
667
|
* @param {object} delta The `deltaCommand` result payload.
|
|
677
668
|
* @param {{projects?: {added: string[], removed: string[], changed: string[]},
|
|
678
|
-
* edges?: {added: string
|
|
669
|
+
* edges?: {added: {source: string, target: string, type: string}[],
|
|
670
|
+
* removed: {source: string, target: string, type: string}[]},
|
|
671
|
+
* codeDrift?: boolean}} [signals]
|
|
679
672
|
* The structural-change and drift signals a delta run derives from the two
|
|
680
673
|
* graphs it holds (the graph diff is `diff`'s vocabulary, shared here, never
|
|
681
|
-
* re-derived) — `
|
|
682
|
-
*
|
|
683
|
-
*
|
|
674
|
+
* re-derived) — `edges` carries the raw `{source, target, type}` triples,
|
|
675
|
+
* which `classifyEvolution` maps through `edgeEvolutionIdentity` itself (the
|
|
676
|
+
* identity string is its output spelling, never an input), and `codeDrift`
|
|
677
|
+
* is the delta's computed "provenance advanced, no arch/policy change" fact.
|
|
678
|
+
* Absent signals are empty, so a delta that
|
|
684
679
|
* computed none reads as a violation-only mapping.
|
|
685
680
|
* @returns {{classifications: string[], disposition: "accepted"|"rejected"|"no-verdict",
|
|
686
681
|
* notes: string[], affected: {projects: string[], boundaries: string[],
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import { readFileSync } from "node:fs";
|
|
49
49
|
|
|
50
|
+
import { canonicalJsonReplacer } from "../canonical.mjs";
|
|
50
51
|
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
51
52
|
|
|
52
53
|
/** The only snapshot schemaVersion this module writes and reads. */
|
|
@@ -262,16 +263,23 @@ export function buildEvidenceSnapshot({
|
|
|
262
263
|
/**
|
|
263
264
|
* Renders the snapshot as deterministic JSON text.
|
|
264
265
|
*
|
|
265
|
-
* Deterministic
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
266
|
+
* Deterministic by mechanism, not by constructor discipline: the text is
|
|
267
|
+
* produced through `../canonical.mjs`'s `canonicalJsonReplacer`, which sorts
|
|
268
|
+
* plain-object keys at every depth — the same canonicalizer the graph-snapshot
|
|
269
|
+
* family's `snapshotIdentity` hashes with. That is load-bearing because two of
|
|
270
|
+
* the stored inputs (`records`, `graph.workspaceLayout`) arrive verbatim from
|
|
271
|
+
* upstream code that owns their nested key order; sorting at serialize time is
|
|
272
|
+
* what makes the bytes a function of what the snapshot MEANS. Array element
|
|
273
|
+
* order is the only order the format keeps, and `buildEvidenceSnapshot` sorts
|
|
274
|
+
* every array whose source does not guarantee it. Two captures over one
|
|
275
|
+
* unchanged tree produce byte-identical files, which is what makes a plain
|
|
276
|
+
* `diff` of two baselines meaningful.
|
|
269
277
|
*
|
|
270
278
|
* @param {object} snapshot From `buildEvidenceSnapshot`.
|
|
271
279
|
* @returns {string} The JSON text, newline-terminated.
|
|
272
280
|
*/
|
|
273
281
|
export function serializeEvidenceSnapshot(snapshot) {
|
|
274
|
-
return `${JSON.stringify(snapshot,
|
|
282
|
+
return `${JSON.stringify(snapshot, canonicalJsonReplacer, 2)}\n`;
|
|
275
283
|
}
|
|
276
284
|
|
|
277
285
|
/**
|