@ecoma-io/archkeep 0.14.0 → 0.16.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/README.md +11 -5
- package/cli.mjs +571 -61
- package/commands.mjs +57 -0
- package/lsp.mjs +15 -2
- package/package.json +8 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +485 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +289 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +11 -3
- package/src/commands/README.md +52 -1
- package/src/commands/change-intent.mjs +461 -0
- package/src/commands/change.mjs +612 -0
- package/src/commands/check.mjs +2 -1
- package/src/commands/context.mjs +124 -16
- package/src/commands/custom-rules.mjs +286 -2
- package/src/commands/delta-classify.mjs +195 -33
- package/src/commands/delta-snapshot.mjs +156 -1
- package/src/commands/delta.mjs +142 -17
- package/src/commands/diff.mjs +41 -13
- package/src/commands/evolution.mjs +473 -0
- package/src/commands/history.mjs +130 -103
- package/src/commands/policy.mjs +57 -0
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/commands/trajectory.mjs +437 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/path-util.mjs +40 -0
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/report/change-text.mjs +148 -0
- package/src/report/delta-text.mjs +82 -1
- package/src/report/evolution-text.mjs +83 -0
- package/src/report/history-text.mjs +4 -114
- package/src/report/sarif.mjs +255 -0
- package/src/report/snapshot-text.mjs +123 -0
- package/src/report/trajectory-text.mjs +143 -0
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/tsconfig-paths.mjs +3 -2
- package/src/workspace.mjs +115 -23
package/src/commands/delta.mjs
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* Unlike `diff` — which compares two GRAPH snapshots edge by edge and never
|
|
20
20
|
* exits 1 — `delta` is a gate: a non-waived introduced violation is a finding
|
|
21
21
|
* (exit 1), which is the whole point of carrying re-judgeable evidence rather
|
|
22
|
-
* than a graph. That
|
|
23
|
-
* exit 1, beside `check` and `fitness
|
|
22
|
+
* than a graph. That made `delta` the third verb whose verdict carries
|
|
23
|
+
* exit 1, beside `check` and `fitness`; `./change.mjs` later became the
|
|
24
|
+
* fourth, over a different question — declared intent versus observed delta.
|
|
24
25
|
*
|
|
25
26
|
* Refusals (each a throw, exit 3 upstream — a delta that could not honestly
|
|
26
27
|
* classify must never read as "no change"):
|
|
@@ -48,12 +49,15 @@
|
|
|
48
49
|
import { createRequire } from "node:module";
|
|
49
50
|
|
|
50
51
|
import { isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
52
|
+
import { stripTrailingSlashes } from "../path-util.mjs";
|
|
51
53
|
import { referenceTime } from "../governance/clock.mjs";
|
|
52
54
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
53
55
|
import { buildDecision } from "../report/evidence.mjs";
|
|
54
56
|
import { formatDeltaReport } from "../report/delta-text.mjs";
|
|
57
|
+
import { formatDeltaSarif } from "../report/sarif.mjs";
|
|
55
58
|
import { evaluateRun } from "../rules/index.mjs";
|
|
56
|
-
import {
|
|
59
|
+
import { customRulesForDelta, declaresCustomRules } from "./custom-rules.mjs";
|
|
60
|
+
import { classifyCustomFindings, classifyDelta } from "./delta-classify.mjs";
|
|
57
61
|
import {
|
|
58
62
|
buildEvidenceSnapshot,
|
|
59
63
|
providerMismatch,
|
|
@@ -72,11 +76,17 @@ const { name: TOOL_NAME, version: TOOL_VERSION } = require("../../package.json")
|
|
|
72
76
|
* Refuses the two head states no delta side may be built over, shared by both
|
|
73
77
|
* modes: the unregistered-plugin graph and incomplete analysis coverage.
|
|
74
78
|
*
|
|
79
|
+
* Exported since `change` arrived because that command builds its comparison
|
|
80
|
+
* over the same two head states — a graph that under-represents the tree and
|
|
81
|
+
* an analysis with holes would reconcile a declaration against architecture
|
|
82
|
+
* nobody observed — and a second copy of the refusal is where the two
|
|
83
|
+
* commands would drift into answering "may this head be judged?" differently.
|
|
84
|
+
*
|
|
75
85
|
* @param {object} commandContext From `resolveCommandContext`.
|
|
76
86
|
* @param {string} activity Which mode is refusing, for the message.
|
|
77
87
|
* @throws {Error} on either condition.
|
|
78
88
|
*/
|
|
79
|
-
function refuseUnjudgeableHead(commandContext, activity) {
|
|
89
|
+
export function refuseUnjudgeableHead(commandContext, activity) {
|
|
80
90
|
const { provider, pluginGap } = commandContext;
|
|
81
91
|
if (provider === "nx" && !pluginGap.registered && pluginGap.manifests.length > 0) {
|
|
82
92
|
throw new Error(
|
|
@@ -123,6 +133,12 @@ export function captureDelta(commandContext, { config }) {
|
|
|
123
133
|
}
|
|
124
134
|
const { root, provider, graph, analysis } = commandContext;
|
|
125
135
|
const snapshot = buildEvidenceSnapshot({
|
|
136
|
+
// The two optional custom-rule blocks, stored exactly when the capturing
|
|
137
|
+
// policy declares rules — an undeclaring workspace's snapshot stays
|
|
138
|
+
// byte-identical (`./delta-snapshot.mjs`, the optional-blocks section).
|
|
139
|
+
...(declaresCustomRules(config)
|
|
140
|
+
? { customRules: config.customRules, owned: commandContext.owned }
|
|
141
|
+
: {}),
|
|
126
142
|
tool: { name: TOOL_NAME, version: TOOL_VERSION },
|
|
127
143
|
provenance: resolveProvenance(root),
|
|
128
144
|
provider,
|
|
@@ -223,11 +239,16 @@ export function evidenceGraphToProjectGraph(storedGraph) {
|
|
|
223
239
|
* head does not already claim — so a base-side record living in a directory
|
|
224
240
|
* the head no longer has still attributes to the project that owned it.
|
|
225
241
|
*
|
|
242
|
+
* Exported for `./change.mjs`, which classifies the same two evidence sets
|
|
243
|
+
* through `./delta-classify.mjs` and must attribute unresolvable records the
|
|
244
|
+
* same way a delta does — a second attribution rule beside this one is how
|
|
245
|
+
* the two commands would disagree about which project carried a site.
|
|
246
|
+
*
|
|
226
247
|
* @param {object} headGraph The current run's graph (`nodes` map).
|
|
227
248
|
* @param {object[]} baselineProjects The snapshot's stored project rows.
|
|
228
249
|
* @returns {(record: object) => string|null}
|
|
229
250
|
*/
|
|
230
|
-
function sourceProjectAttributor(headGraph, baselineProjects) {
|
|
251
|
+
export function sourceProjectAttributor(headGraph, baselineProjects) {
|
|
231
252
|
/** @type {Map<string, string>} root → project name, head winning ties. */
|
|
232
253
|
const byRoot = new Map();
|
|
233
254
|
for (const node of Object.values(headGraph.nodes ?? {})) {
|
|
@@ -240,7 +261,7 @@ function sourceProjectAttributor(headGraph, baselineProjects) {
|
|
|
240
261
|
}
|
|
241
262
|
}
|
|
242
263
|
const entries = [...byRoot.entries()]
|
|
243
|
-
.map(([root, name]) => [root
|
|
264
|
+
.map(([root, name]) => [stripTrailingSlashes(root), name])
|
|
244
265
|
.sort((a, b) => b[0].length - a[0].length);
|
|
245
266
|
return (record) => {
|
|
246
267
|
const file = record?.sourceFile;
|
|
@@ -273,20 +294,36 @@ const short = (fingerprint) =>
|
|
|
273
294
|
* a tracked acceptance, not a fix — but do not fail the gate, which is what
|
|
274
295
|
* a waiver is for.
|
|
275
296
|
*
|
|
297
|
+
* Custom-rule (wasm) findings join the classification when either side
|
|
298
|
+
* declares them: `./custom-rules.mjs`'s `customRulesForDelta` judges every
|
|
299
|
+
* head-declared rule over both evidence sets, `./delta-classify.mjs`'s
|
|
300
|
+
* `classifyCustomFindings` buckets the findings, and the result rides the
|
|
301
|
+
* envelope as `result.customRules` — a block that is ABSENT (never `null`)
|
|
302
|
+
* when neither side declares any, so an undeclaring workspace's envelope
|
|
303
|
+
* stays byte-identical. An introduced custom finding gates exactly as an
|
|
304
|
+
* introduced violation does, with no waiver lane by construction
|
|
305
|
+
* (suppressions key on a `messageId` custom findings do not have); an
|
|
306
|
+
* unclassifiable one is a no-verdict. This is also why the function is async:
|
|
307
|
+
* the wasm host is.
|
|
308
|
+
*
|
|
276
309
|
* @param {string} baselinePath Absolute path to the evidence snapshot.
|
|
277
310
|
* @param {object} commandContext From `resolveCommandContext`.
|
|
278
311
|
* @param {{config: object|null, readBaseline?: (path: string) => object,
|
|
279
|
-
* now?: string
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
312
|
+
* now?: string, readArtifact?: (artifact: string) => Uint8Array|null,
|
|
313
|
+
* timeoutMs?: number}} io The resolved boundary config (required
|
|
314
|
+
* — both sides are re-judged under it), an injectable baseline reader, the
|
|
315
|
+
* one shared reference instant (defaults to the shared governance clock),
|
|
316
|
+
* and the custom-rule host's two injectable seams, passed through to
|
|
317
|
+
* `customRulesForDelta`.
|
|
318
|
+
* @returns {Promise<{status: "ok"|"findings"|"no-verdict", delta: object,
|
|
319
|
+
* coverage: object, report: {text: string, json: string, sarif: string}}>}
|
|
320
|
+
* @throws {Error} on every refusal the module header lists, and on a
|
|
321
|
+
* custom-rule LOAD failure (`./custom-rules.mjs` argues the split).
|
|
285
322
|
*/
|
|
286
|
-
export function deltaCommand(
|
|
323
|
+
export async function deltaCommand(
|
|
287
324
|
baselinePath,
|
|
288
325
|
commandContext,
|
|
289
|
-
{ config, readBaseline = readEvidenceSnapshot, now = referenceTime() },
|
|
326
|
+
{ config, readBaseline = readEvidenceSnapshot, now = referenceTime(), ...customRuleIo },
|
|
290
327
|
) {
|
|
291
328
|
const { root, provider, marker, graph, analysis } = commandContext;
|
|
292
329
|
|
|
@@ -378,23 +415,93 @@ export function deltaCommand(
|
|
|
378
415
|
);
|
|
379
416
|
}
|
|
380
417
|
|
|
418
|
+
// The custom-rule half, present exactly when a side declares rules: judged
|
|
419
|
+
// two-sided where the law is identical, `unknown` with a mandatory reason
|
|
420
|
+
// everywhere else (`./custom-rules.mjs`'s `customRulesForDelta` owns the
|
|
421
|
+
// routes). `null` when NEITHER side declares any — the envelope block and
|
|
422
|
+
// the summary key are then absent, and an undeclaring workspace's envelope
|
|
423
|
+
// stays byte-identical (`../../../../AGENTS.md`, "a change to what is
|
|
424
|
+
// reported on an unchanged workspace is a breaking change").
|
|
425
|
+
/** @type {{judged: object[], skipped: object[], removed: string[],
|
|
426
|
+
* findings: {introduced: object[], resolved: object[], unchanged: object[],
|
|
427
|
+
* unknown: object[]}}|null} */
|
|
428
|
+
let custom = null;
|
|
429
|
+
// The head-declared finding catalogue, held for the SARIF face alone: the
|
|
430
|
+
// envelope deliberately does not carry it (the JSON contract predates the
|
|
431
|
+
// SARIF face and must stay byte-identical), while `sarifRules` needs it so
|
|
432
|
+
// an introduced custom finding's `ruleId` resolves to a descriptor.
|
|
433
|
+
/** @type {{ruleId: string, rule: string, findingId: string, message: string}[]} */
|
|
434
|
+
let customCatalogue = [];
|
|
435
|
+
if (declaresCustomRules(config)) {
|
|
436
|
+
const twoSided = await customRulesForDelta(commandContext, {
|
|
437
|
+
rows: config.customRules,
|
|
438
|
+
policy: config,
|
|
439
|
+
baseline,
|
|
440
|
+
...customRuleIo,
|
|
441
|
+
});
|
|
442
|
+
customCatalogue = twoSided.catalogue;
|
|
443
|
+
custom = {
|
|
444
|
+
judged: twoSided.judged.map(({ name, sha256, notes: ruleNotes }) => ({
|
|
445
|
+
name,
|
|
446
|
+
sha256,
|
|
447
|
+
...(ruleNotes === undefined ? {} : { notes: ruleNotes }),
|
|
448
|
+
})),
|
|
449
|
+
skipped: twoSided.unknownRules,
|
|
450
|
+
removed: twoSided.removedRules,
|
|
451
|
+
findings: classifyCustomFindings({
|
|
452
|
+
judged: twoSided.judged,
|
|
453
|
+
unknownRules: twoSided.unknownRules,
|
|
454
|
+
}),
|
|
455
|
+
};
|
|
456
|
+
} else if (baseline.customRules !== undefined) {
|
|
457
|
+
// The head declares nothing, so there is no law to judge either side
|
|
458
|
+
// under — every baseline rule is a removal, disclosed rather than judged.
|
|
459
|
+
custom = {
|
|
460
|
+
judged: [],
|
|
461
|
+
skipped: [],
|
|
462
|
+
removed: baseline.customRules.map((row) => row.name),
|
|
463
|
+
findings: { introduced: [], resolved: [], unchanged: [], unknown: [] },
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (custom !== null) {
|
|
467
|
+
for (const skipped of custom.skipped) {
|
|
468
|
+
notes.push(`custom rule "${skipped.name}" was not classified — ${skipped.reason}`);
|
|
469
|
+
}
|
|
470
|
+
for (const name of custom.removed) {
|
|
471
|
+
notes.push(
|
|
472
|
+
`custom rule "${name}" is declared in the baseline but not by the current policy — ` +
|
|
473
|
+
`nothing was judged for it, so its base-side findings are not classified as resolved`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
for (const rule of custom.judged) {
|
|
477
|
+
for (const note of rule.notes ?? []) {
|
|
478
|
+
notes.push(`custom rule "${rule.name}": ${note}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
381
483
|
const { violations, unresolvable } = classification;
|
|
382
484
|
const introducedWaived = violations.introduced.filter((entry) => entry.waived === true).length;
|
|
383
485
|
const introducedNotWaived = violations.introduced.length - introducedWaived;
|
|
384
|
-
|
|
486
|
+
// Custom findings have no waiver lane (`./delta-classify.mjs`'s
|
|
487
|
+
// `classifyCustomFindings` argues the by-construction absence), so every
|
|
488
|
+
// introduced one gates.
|
|
489
|
+
const customIntroduced = custom === null ? 0 : custom.findings.introduced.length;
|
|
490
|
+
const customUnknown = custom === null ? 0 : custom.findings.unknown.length;
|
|
491
|
+
const unknownCount = violations.unknown.length + unresolvable.unknown.length + customUnknown;
|
|
385
492
|
|
|
386
493
|
/** @type {"ok"|"findings"|"no-verdict"} */
|
|
387
494
|
let status;
|
|
388
495
|
/** @type {0|1|3} */
|
|
389
496
|
let exitCode;
|
|
390
497
|
let decision;
|
|
391
|
-
if (introducedNotWaived > 0) {
|
|
498
|
+
if (introducedNotWaived + customIntroduced > 0) {
|
|
392
499
|
status = "findings";
|
|
393
500
|
exitCode = 1;
|
|
394
501
|
decision = buildDecision({
|
|
395
502
|
status,
|
|
396
503
|
coverageComplete: true,
|
|
397
|
-
findings: introducedNotWaived,
|
|
504
|
+
findings: introducedNotWaived + customIntroduced,
|
|
398
505
|
});
|
|
399
506
|
} else if (unknownCount > 0) {
|
|
400
507
|
status = "no-verdict";
|
|
@@ -405,6 +512,9 @@ export function deltaCommand(
|
|
|
405
512
|
findings: 0,
|
|
406
513
|
reason:
|
|
407
514
|
`${unknownCount} delta item${unknownCount === 1 ? "" : "s"} could not be classified — ` +
|
|
515
|
+
(customUnknown > 0
|
|
516
|
+
? `${customUnknown} of them custom-rule item${customUnknown === 1 ? "" : "s"} — `
|
|
517
|
+
: "") +
|
|
408
518
|
`an item whose identity cannot be stated is never guessed into a bucket`,
|
|
409
519
|
});
|
|
410
520
|
} else {
|
|
@@ -454,9 +564,20 @@ export function deltaCommand(
|
|
|
454
564
|
unchanged: unresolvable.unchanged.length,
|
|
455
565
|
unknown: unresolvable.unknown.length,
|
|
456
566
|
},
|
|
567
|
+
...(custom === null
|
|
568
|
+
? {}
|
|
569
|
+
: {
|
|
570
|
+
customFindings: {
|
|
571
|
+
introduced: custom.findings.introduced.length,
|
|
572
|
+
resolved: custom.findings.resolved.length,
|
|
573
|
+
unchanged: custom.findings.unchanged.length,
|
|
574
|
+
unknown: custom.findings.unknown.length,
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
457
577
|
},
|
|
458
578
|
violations,
|
|
459
579
|
unresolvable,
|
|
580
|
+
...(custom === null ? {} : { customRules: custom }),
|
|
460
581
|
};
|
|
461
582
|
|
|
462
583
|
const envelope = jsonEnvelope({
|
|
@@ -476,6 +597,10 @@ export function deltaCommand(
|
|
|
476
597
|
report: {
|
|
477
598
|
text: formatDeltaReport({ delta: result, coverage }),
|
|
478
599
|
json: renderJson(envelope),
|
|
600
|
+
// Eager beside the other two faces: the render is pure and cheap, and a
|
|
601
|
+
// lazy face is one a caller can forget to build — the SARIF is the same
|
|
602
|
+
// verdict, ready whichever face `--format` selects.
|
|
603
|
+
sarif: formatDeltaSarif({ delta: result, coverage, customCatalogue }),
|
|
479
604
|
},
|
|
480
605
|
};
|
|
481
606
|
}
|
package/src/commands/diff.mjs
CHANGED
|
@@ -216,13 +216,29 @@ function buildHeadSnapshot(commandContext) {
|
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The identity key of one dependency record: the `(source, target, type)`
|
|
221
|
+
* triple joined with `\0`. Extracted from `computeDiff`'s index maps so a
|
|
222
|
+
* second consumer (`./trajectory.mjs`'s persistence sets) keys edges the SAME
|
|
223
|
+
* way the diff does — a second spelling of this string would be a second
|
|
224
|
+
* definition of "same edge", and two definitions drift.
|
|
225
|
+
*
|
|
226
|
+
* A `static` edge becoming `dynamic` is therefore a removed edge under the
|
|
227
|
+
* old type and an added edge under the new one, which is exactly what a
|
|
228
|
+
* consumer wants to see: it is a real architectural event, not an
|
|
229
|
+
* implementation detail.
|
|
230
|
+
*
|
|
231
|
+
* @param {{source: string, target: string, type: string}} edge
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
export function edgeIdentityKey({ source, target, type }) {
|
|
235
|
+
return `${source}\0${target}\0${type}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
219
238
|
/**
|
|
220
239
|
* Computes the diff between two graph snapshots.
|
|
221
240
|
*
|
|
222
|
-
* Edge identity is `(source, target, type)` —
|
|
223
|
-
* `dynamic` is an added edge under the new type and a removed edge under the
|
|
224
|
-
* old one, which is exactly what a consumer wants to see: it is a real
|
|
225
|
-
* architectural event, not an implementation detail.
|
|
241
|
+
* Edge identity is `(source, target, type)` — see `edgeIdentityKey` above.
|
|
226
242
|
*
|
|
227
243
|
* @param {{projects: object[], dependencies: object[]}} baseline
|
|
228
244
|
* @param {{projects: object[], dependencies: object[]}} head
|
|
@@ -282,13 +298,9 @@ export function computeDiff(baseline, head) {
|
|
|
282
298
|
removedProjects.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
283
299
|
changedProjects.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
284
300
|
|
|
285
|
-
// Index edges by the
|
|
286
|
-
const baselineEdges = new Map(
|
|
287
|
-
|
|
288
|
-
);
|
|
289
|
-
const headEdges = new Map(
|
|
290
|
-
head.dependencies.map((e) => [`${e.source}\0${e.target}\0${e.type}`, e]),
|
|
291
|
-
);
|
|
301
|
+
// Index edges by the identity key — `edgeIdentityKey` above owns the triple.
|
|
302
|
+
const baselineEdges = new Map(baseline.dependencies.map((e) => [edgeIdentityKey(e), e]));
|
|
303
|
+
const headEdges = new Map(head.dependencies.map((e) => [edgeIdentityKey(e), e]));
|
|
292
304
|
|
|
293
305
|
const addedEdges = [];
|
|
294
306
|
const removedEdges = [];
|
|
@@ -410,8 +422,11 @@ export function diffCommand(
|
|
|
410
422
|
// classifies as changes, compared here through the shared
|
|
411
423
|
// `./snapshot-meta.mjs` so the two commands cannot disagree about them.
|
|
412
424
|
// Each mismatch becomes a `coverage.notes` warning rather than a refusal:
|
|
413
|
-
// a provider migration, a cross-repository diff,
|
|
414
|
-
// baseline and head are all legitimate states a
|
|
425
|
+
// a provider migration, a cross-repository diff, a dirty-tree side, or a
|
|
426
|
+
// policy change between baseline and head are all legitimate states a
|
|
427
|
+
// consumer must be told about. The dirty-tree wording is the one `delta` and
|
|
428
|
+
// `change` emit over the same metadata — three commands, one sentence each,
|
|
429
|
+
// so a consumer reading any report reads the same caveat.
|
|
415
430
|
const headProvenance = resolveProvenance(root);
|
|
416
431
|
const headFingerprint = config ? computePolicyFingerprint(config) : null;
|
|
417
432
|
const meta = compareSnapshotMetadata({
|
|
@@ -445,6 +460,19 @@ export function diffCommand(
|
|
|
445
460
|
);
|
|
446
461
|
}
|
|
447
462
|
|
|
463
|
+
if (meta.dirtyBaseline) {
|
|
464
|
+
coverage.notes.push(
|
|
465
|
+
"the baseline was captured from a dirty working tree — its evidence is not a reproducible " +
|
|
466
|
+
"claim about the commit it names",
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (meta.dirtyHead) {
|
|
470
|
+
coverage.notes.push(
|
|
471
|
+
"this run's working tree is dirty — the head side describes uncommitted state, not the " +
|
|
472
|
+
"commit HEAD names",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
448
476
|
if (meta.policyChanged === true) {
|
|
449
477
|
// A policy change between baseline and head means every "introduced" or
|
|
450
478
|
// "resolved" violation in the rule-impact analysis may be an artefact of
|