@ecoma-io/archkeep 0.13.0 → 0.15.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 +9 -3
- package/cli.mjs +599 -55
- package/commands.mjs +51 -0
- package/package.json +3 -1
- package/src/analysis/typescript.mjs +2 -1
- package/src/commands/README.md +70 -1
- package/src/commands/change-intent.mjs +461 -0
- package/src/commands/change.mjs +612 -0
- package/src/commands/check.mjs +84 -17
- package/src/commands/context.mjs +92 -16
- package/src/commands/coverage-acceptance.mjs +113 -0
- package/src/commands/custom-rules.mjs +286 -2
- package/src/commands/delta-classify.mjs +664 -0
- package/src/commands/delta-snapshot.mjs +672 -0
- package/src/commands/delta.mjs +606 -0
- package/src/commands/diff.mjs +41 -13
- package/src/commands/evolution.mjs +473 -0
- package/src/commands/explain.mjs +39 -0
- package/src/commands/history.mjs +130 -103
- package/src/commands/policy.mjs +93 -1
- package/src/commands/trajectory.mjs +437 -0
- package/src/commands/waivers.mjs +53 -3
- package/src/config.mjs +129 -11
- package/src/lsp/boundary-config.mjs +9 -4
- package/src/path-util.mjs +40 -0
- package/src/providers/native/model.mjs +17 -0
- package/src/report/change-text.mjs +148 -0
- package/src/report/delta-text.mjs +264 -0
- package/src/report/evolution-text.mjs +83 -0
- package/src/report/explain-text.mjs +27 -0
- package/src/report/history-text.mjs +4 -114
- package/src/report/sarif.mjs +280 -0
- package/src/report/snapshot-text.mjs +123 -0
- package/src/report/text.mjs +36 -0
- package/src/report/trajectory-text.mjs +143 -0
- package/src/report/waivers-text.mjs +35 -2
- package/src/tsconfig-paths.mjs +3 -2
package/src/commands/check.mjs
CHANGED
|
@@ -14,10 +14,12 @@ import { join } from "node:path";
|
|
|
14
14
|
|
|
15
15
|
import { fileFailure, isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
16
16
|
import { tsconfigPathsFacts } from "../analysis/typescript.mjs";
|
|
17
|
+
import { stripTrailingSlashes } from "../path-util.mjs";
|
|
17
18
|
import { suppressionCovers } from "../config.mjs";
|
|
18
19
|
import { referenceTime } from "../governance/clock.mjs";
|
|
19
20
|
import { suppressionFate } from "../governance/waiver.mjs";
|
|
20
21
|
import { resolveCommandContext, unownedGapWithoutRunConfiguration } from "./context.mjs";
|
|
22
|
+
import { partitionUnownedCoverage } from "./coverage-acceptance.mjs";
|
|
21
23
|
import { readAdrContext } from "./adr.mjs";
|
|
22
24
|
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
23
25
|
import { declaredEdgeViolationsForCheck } from "./edge-constraints.mjs";
|
|
@@ -154,7 +156,7 @@ function declaredEdgeManifest({ provider, graph }, sourceProject) {
|
|
|
154
156
|
// spelling) — and `./project.json` is a different string from
|
|
155
157
|
// `project.json` to every consumer that compares paths, this file's own
|
|
156
158
|
// SARIF `uri` included.
|
|
157
|
-
const scoped = typeof root === "string" ? root
|
|
159
|
+
const scoped = typeof root === "string" ? stripTrailingSlashes(root) : "";
|
|
158
160
|
return scoped === "" || scoped === "." ? site.file : `${scoped}/${site.file}`;
|
|
159
161
|
}
|
|
160
162
|
/**
|
|
@@ -248,6 +250,51 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
248
250
|
}
|
|
249
251
|
: null;
|
|
250
252
|
|
|
253
|
+
// The unowned-file question, answered once and BEFORE any verdict below
|
|
254
|
+
// reads `failures`: the tolerated TS/JS/Vue gap (`./context.mjs`'s
|
|
255
|
+
// `unownedGap`), minus the files this run read as its own configuration —
|
|
256
|
+
// the law that actually governed THIS run, which `policySource` names
|
|
257
|
+
// (`--config` override and resolved profile included), never the declared
|
|
258
|
+
// name alone. Subtracting here rather than inside `resolveCommandContext`
|
|
259
|
+
// is forced: `resolvePolicy` takes the context as an argument, so it cannot
|
|
260
|
+
// run before it, and until it has run nothing knows which law governed.
|
|
261
|
+
// `tsConfig` joins it because it is configuration by the same test, though
|
|
262
|
+
// every spelling of it is `.json` today and so never reaches the list.
|
|
263
|
+
const unownedGap = unownedGapWithoutRunConfiguration(commandContext.unownedGap, [
|
|
264
|
+
policySource,
|
|
265
|
+
commandContext.options.tsConfig,
|
|
266
|
+
]);
|
|
267
|
+
// Then the acceptance channel: the policy's `coverage.unowned` rows,
|
|
268
|
+
// matched against BOTH unowned sets — this gap and the Go/Rust/Python
|
|
269
|
+
// unclaimed list — and nothing else (`./coverage-acceptance.mjs` owns the
|
|
270
|
+
// guarantee that an owned file is unreachable). Unowned-ness is decided
|
|
271
|
+
// exactly as before this channel existed; the rows only partition the
|
|
272
|
+
// result into accepted and uncovered.
|
|
273
|
+
const coverageRows = config?.coverage?.unowned ?? [];
|
|
274
|
+
const unownedCoverage = partitionUnownedCoverage({
|
|
275
|
+
rows: coverageRows,
|
|
276
|
+
unownedGap,
|
|
277
|
+
unclaimedFiles: commandContext.unclaimedGap.files,
|
|
278
|
+
tracked,
|
|
279
|
+
});
|
|
280
|
+
// A covered unclaimed file's whole-file failure is withdrawn here: the file
|
|
281
|
+
// is still unowned and still unanalyzed, but its state is a RECORDED
|
|
282
|
+
// acceptance now — stated below as the `"accepted-unowned-files"` coverage
|
|
283
|
+
// gap, never silently — rather than the exit-3 refusal an unanswered
|
|
284
|
+
// orphan earns. Uncovered unclaimed files keep their failures, and with
|
|
285
|
+
// them the exit code, byte-identical to before the channel existed. An
|
|
286
|
+
// unclaimed file carries exactly one failure (it is unowned, so no
|
|
287
|
+
// analyzer ever read it), so filtering by file cannot drop an unrelated
|
|
288
|
+
// read failure.
|
|
289
|
+
const acceptedUnclaimed = new Set(
|
|
290
|
+
commandContext.unclaimedGap.files.filter((file) => unownedCoverage.acceptedFiles.has(file)),
|
|
291
|
+
);
|
|
292
|
+
if (acceptedUnclaimed.size > 0) {
|
|
293
|
+
for (let at = failures.length - 1; at >= 0; at -= 1) {
|
|
294
|
+
if (acceptedUnclaimed.has(failures[at].sourceFile)) failures.splice(at, 1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
251
298
|
// The go.work drift check, keyed off the manifest's presence the way every
|
|
252
299
|
// resolver keys off its language's manifest: no tracked root go.work, no
|
|
253
300
|
// check and no mention. It ignores `options.paths` on purpose — two
|
|
@@ -622,9 +669,22 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
622
669
|
config !== null &&
|
|
623
670
|
options.paths.length === 0 &&
|
|
624
671
|
failures.length === 0 &&
|
|
625
|
-
(config.suppressions.length > 0 || config.depConstraints.length > 0)
|
|
672
|
+
(config.suppressions.length > 0 || config.depConstraints.length > 0 || coverageRows.length > 0)
|
|
626
673
|
) {
|
|
627
674
|
const deadRows = [];
|
|
675
|
+
// The third dead table: a `coverage.unowned` row matching no unowned file
|
|
676
|
+
// across BOTH sets (`./coverage-acceptance.mjs`) accepts nothing — the
|
|
677
|
+
// files it covered are owned now, or the path was never right — and it is
|
|
678
|
+
// refused in the same sentence shape the native provider's stale
|
|
679
|
+
// `coverage.exempt` row has always gotten
|
|
680
|
+
// (`../providers/native/index.mjs`), under the same gate as its two
|
|
681
|
+
// siblings above this comment's block.
|
|
682
|
+
for (const { path, index } of unownedCoverage.dead) {
|
|
683
|
+
deadRows.push(
|
|
684
|
+
`coverage.unowned[${index}]: '${path}' matches no unowned file this run judged — either ` +
|
|
685
|
+
`the files it accepted are owned by a project now, or the path was never right`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
628
688
|
config.suppressions.forEach((row, index) => {
|
|
629
689
|
const fate = suppressionFate(row, now);
|
|
630
690
|
if (fate === "waive") return;
|
|
@@ -739,25 +799,16 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
739
799
|
// `coverage.complete` untouched — those belong to `unchecked`, and moving
|
|
740
800
|
// this state into them would turn `check` red on trees whose only sin is a
|
|
741
801
|
// root-level tooling script.
|
|
742
|
-
// The
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
// inside `resolveCommandContext` is forced — `resolvePolicy` takes the
|
|
746
|
-
// context as an argument, so it cannot run before it. `tsConfig` joins it
|
|
747
|
-
// because it is configuration by the same test, though every spelling of it
|
|
748
|
-
// is `.json` today and so never reaches the list.
|
|
749
|
-
const unownedGap = unownedGapWithoutRunConfiguration(commandContext.unownedGap, [
|
|
750
|
-
policySource,
|
|
751
|
-
commandContext.options.tsConfig,
|
|
752
|
-
]);
|
|
753
|
-
|
|
802
|
+
// The subtraction of the run's own configuration, and the acceptance
|
|
803
|
+
// partition, both happened beside the policy above — `unownedCoverage` is
|
|
804
|
+
// that one computation's result, read here rather than recomputed.
|
|
754
805
|
const coverageGaps = [
|
|
755
806
|
...(commandContext.provider === "nx" &&
|
|
756
807
|
!commandContext.pluginGap.registered &&
|
|
757
808
|
commandContext.pluginGap.manifests.length > 0
|
|
758
809
|
? [{ kind: "unregistered-plugin", manifests: commandContext.pluginGap.manifests }]
|
|
759
810
|
: []),
|
|
760
|
-
...(
|
|
811
|
+
...(unownedCoverage.uncoveredUnowned.files.length > 0
|
|
761
812
|
? [
|
|
762
813
|
{
|
|
763
814
|
kind: "unowned-files",
|
|
@@ -766,8 +817,24 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
766
817
|
// `moon.yml`), and the faces that render this carry no other way
|
|
767
818
|
// to know which tree they are describing.
|
|
768
819
|
provider: commandContext.provider,
|
|
769
|
-
languages:
|
|
770
|
-
files:
|
|
820
|
+
languages: unownedCoverage.uncoveredUnowned.languages,
|
|
821
|
+
files: unownedCoverage.uncoveredUnowned.files,
|
|
822
|
+
},
|
|
823
|
+
]
|
|
824
|
+
: []),
|
|
825
|
+
// The accepted half of both unowned sets, stated every run the
|
|
826
|
+
// acceptance is in force: an accepted hole is loud, never invisible —
|
|
827
|
+
// the report keeps naming the files, and `archkeep waivers` names each
|
|
828
|
+
// accepting row with its reason. Only the permanent unanswerable
|
|
829
|
+
// question (the warning above) and the unclaimed exit 3 are gone for
|
|
830
|
+
// these files.
|
|
831
|
+
...(unownedCoverage.accepted.files.length > 0
|
|
832
|
+
? [
|
|
833
|
+
{
|
|
834
|
+
kind: "accepted-unowned-files",
|
|
835
|
+
provider: commandContext.provider,
|
|
836
|
+
languages: unownedCoverage.accepted.languages,
|
|
837
|
+
files: unownedCoverage.accepted.files,
|
|
771
838
|
},
|
|
772
839
|
]
|
|
773
840
|
: []),
|
package/src/commands/context.mjs
CHANGED
|
@@ -228,6 +228,15 @@ export const WORKSPACE_MARKERS = [NX_CONFIG_FILE, ARCHKEEP_MODEL_FILE, MOON_DIR,
|
|
|
228
228
|
* failure (`../providers/native/coverage.mjs`'s `judgeCoverage`) and so
|
|
229
229
|
* already refuses the run with exit 3 — a gap beside it would be a second,
|
|
230
230
|
* quieter voice for a state that is answered loudly.
|
|
231
|
+
* @property {{files: string[]}} unclaimedGap The tracked Go, Rust or Python
|
|
232
|
+
* files no project owns — the SAME files whose whole-file failures
|
|
233
|
+
* `unclaimedFileFailures` already put in `analysis.failures`, listed a
|
|
234
|
+
* second time as data so `./check.mjs` and `./waivers.mjs` can match the
|
|
235
|
+
* policy's `coverage.unowned` acceptance rows against them
|
|
236
|
+
* (`./coverage-acceptance.mjs`) without parsing a failure's sentence back
|
|
237
|
+
* into a file list. Always `{files: []}` on a native workspace, whose own
|
|
238
|
+
* `coverage.exempt` channel makes the policy key unreachable there
|
|
239
|
+
* (`./policy.mjs`'s `resolvePolicy`).
|
|
231
240
|
* @property {{file: string, project: string}[]} owned Every tracked file that
|
|
232
241
|
* belongs to a project, paired with its owning project — the ownership map
|
|
233
242
|
* `createWorkspace` already built. A command that needs to know WHICH project
|
|
@@ -305,20 +314,37 @@ const UNCLAIMED_CHECK_LANGUAGES = new Set(["go", "rust", "python"]);
|
|
|
305
314
|
* tool reads — inventing one is out of scope here; this only detects and
|
|
306
315
|
* reports.
|
|
307
316
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
317
|
+
* The file list and the failures it becomes are two exports on purpose:
|
|
318
|
+
* `./check.mjs` and `./waivers.mjs` need the LIST a second time — the
|
|
319
|
+
* `coverage.unowned` acceptance channel (`./coverage-acceptance.mjs`) matches
|
|
320
|
+
* its rows against exactly this set, and deriving the set from the failures'
|
|
321
|
+
* wording would bind an acceptance decision to a sentence.
|
|
322
|
+
*
|
|
323
|
+
* @param {{tracked: string[], owned: {file: string, project: string}[]}} args
|
|
324
|
+
* @returns {string[]}
|
|
310
325
|
*/
|
|
311
|
-
function
|
|
326
|
+
function unclaimedAnalyzableFiles({ tracked, owned }) {
|
|
312
327
|
const ownedFiles = new Set(owned.map(({ file }) => file));
|
|
313
|
-
return tracked
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
328
|
+
return tracked.filter(
|
|
329
|
+
(file) => UNCLAIMED_CHECK_LANGUAGES.has(languageOf(file)) && !ownedFiles.has(file),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The whole-file failures for `unclaimedAnalyzableFiles`' list — the shape
|
|
335
|
+
* argued in the comment above the two functions.
|
|
336
|
+
*
|
|
337
|
+
* @param {{files: string[], providerLabel: string}} args
|
|
338
|
+
* @returns {object[]}
|
|
339
|
+
*/
|
|
340
|
+
function unclaimedFileFailures({ files, providerLabel }) {
|
|
341
|
+
return files.map((file) =>
|
|
342
|
+
fileFailure(
|
|
343
|
+
file,
|
|
344
|
+
`is not owned by any project in ${providerLabel} — every tracked Go, Rust or Python file ` +
|
|
345
|
+
`must belong to exactly one declared project, so its cross-project imports can be checked`,
|
|
346
|
+
),
|
|
347
|
+
);
|
|
322
348
|
}
|
|
323
349
|
|
|
324
350
|
/**
|
|
@@ -498,6 +524,7 @@ export function resolveCommandContext(
|
|
|
498
524
|
let analyzedFiles;
|
|
499
525
|
let pluginGap;
|
|
500
526
|
let unownedGap;
|
|
527
|
+
let unclaimedGap;
|
|
501
528
|
let exemptedFiles;
|
|
502
529
|
|
|
503
530
|
if (hasNative) {
|
|
@@ -607,6 +634,12 @@ export function resolveCommandContext(
|
|
|
607
634
|
// the reason `pluginGap` is: a reader must not have to tell "false" from
|
|
608
635
|
// "this branch forgot".
|
|
609
636
|
unownedGap = { files: [], languages: [] };
|
|
637
|
+
// Same statement one list over: native's unclaimed files are already
|
|
638
|
+
// whole-file failures in `discovered.failures` above, and the policy's
|
|
639
|
+
// `coverage.unowned` channel is refused outright on this provider
|
|
640
|
+
// (`./policy.mjs`'s `resolvePolicy`), so there is nothing here for that
|
|
641
|
+
// channel to match against.
|
|
642
|
+
unclaimedGap = { files: [] };
|
|
610
643
|
} else if (hasMoon) {
|
|
611
644
|
// Moon provider — reads graph from `moon project-graph --json`, the same
|
|
612
645
|
// one-call contract as the Nx path: Moon already resolved projects, tags
|
|
@@ -683,11 +716,13 @@ export function resolveCommandContext(
|
|
|
683
716
|
// native's own `discovered.failures` has (this branch's header already
|
|
684
717
|
// analyzes the whole tree before `paths` narrows anything, for the same
|
|
685
718
|
// reason).
|
|
719
|
+
const unclaimedFiles = unclaimedAnalyzableFiles({ tracked, owned });
|
|
686
720
|
failures = [
|
|
687
721
|
...wholeTreeAnalysis.failures.filter((failure) => selectedFiles.has(failure.sourceFile)),
|
|
688
|
-
...unclaimedFileFailures({
|
|
722
|
+
...unclaimedFileFailures({ files: unclaimedFiles, providerLabel: "the Moon project graph" }),
|
|
689
723
|
...pythonUnmodelledFailures(workspace),
|
|
690
724
|
];
|
|
725
|
+
unclaimedGap = { files: unclaimedFiles };
|
|
691
726
|
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
692
727
|
analyzed = analyzedFiles.length;
|
|
693
728
|
// `coverage.exempt` is a native-only key (`../providers/native/coverage.mjs`'s
|
|
@@ -742,11 +777,13 @@ export function resolveCommandContext(
|
|
|
742
777
|
// unconditionally, the same workspace-wide posture native's own
|
|
743
778
|
// `discovered.failures` has, so a scoped `check <path>` cannot hide an
|
|
744
779
|
// orphan file elsewhere in the tree by naming a path that excludes it.
|
|
780
|
+
const unclaimedFiles = unclaimedAnalyzableFiles({ tracked, owned });
|
|
745
781
|
failures = [
|
|
746
782
|
...failures,
|
|
747
|
-
...unclaimedFileFailures({
|
|
783
|
+
...unclaimedFileFailures({ files: unclaimedFiles, providerLabel: "the Nx project graph" }),
|
|
748
784
|
...pythonUnmodelledFailures(workspace),
|
|
749
785
|
];
|
|
786
|
+
unclaimedGap = { files: unclaimedFiles };
|
|
750
787
|
// Same reason as the Moon branch above: `coverage.exempt` is a native-only
|
|
751
788
|
// concept, so an Nx workspace has nothing to report here.
|
|
752
789
|
exemptedFiles = [];
|
|
@@ -765,8 +802,7 @@ export function resolveCommandContext(
|
|
|
765
802
|
// can name it correctly.
|
|
766
803
|
return {
|
|
767
804
|
root,
|
|
768
|
-
|
|
769
|
-
marker: hasMoon ? moonMarker : hasNative ? ARCHKEEP_MODEL_FILE : NX_CONFIG_FILE,
|
|
805
|
+
...workspaceNames(hasMoon, hasNative, moonMarker),
|
|
770
806
|
graph,
|
|
771
807
|
workspace,
|
|
772
808
|
tracked,
|
|
@@ -774,6 +810,7 @@ export function resolveCommandContext(
|
|
|
774
810
|
options,
|
|
775
811
|
pluginGap,
|
|
776
812
|
unownedGap,
|
|
813
|
+
unclaimedGap,
|
|
777
814
|
// Every tracked file that belongs to a project, paired with its project —
|
|
778
815
|
// the ownership map `createWorkspace` already built (`own ./workspace.mjs`).
|
|
779
816
|
// A command that needs to know WHICH project owns a file (the planning
|
|
@@ -783,6 +820,45 @@ export function resolveCommandContext(
|
|
|
783
820
|
};
|
|
784
821
|
}
|
|
785
822
|
|
|
823
|
+
/**
|
|
824
|
+
* The provider/marker pair a workspace root carries, derived from marker
|
|
825
|
+
* presence through the one mapping every envelope header must agree on.
|
|
826
|
+
* `resolveCommandContext` reads it for its own context; a caller that needs
|
|
827
|
+
* the identity WITHOUT judging the tree — an envelope header over a run whose
|
|
828
|
+
* analyzed revisions each carry their own contexts (`./evolution.mjs`) —
|
|
829
|
+
* calls `describeWorkspaceRoot`, so the vocabulary ("nx"/"native"/"moon" and
|
|
830
|
+
* the marker that decided it) has exactly one home.
|
|
831
|
+
*
|
|
832
|
+
* @param {boolean} hasMoon Whether a Moon directory marks the root.
|
|
833
|
+
* @param {boolean} hasNative Whether `archkeep.json` marks the root.
|
|
834
|
+
* @param {string|null} moonMarker Which Moon directory is present.
|
|
835
|
+
* @returns {{provider: "nx" | "moon" | "native", marker: string}}
|
|
836
|
+
*/
|
|
837
|
+
function workspaceNames(hasMoon, hasNative, moonMarker) {
|
|
838
|
+
return {
|
|
839
|
+
provider: hasMoon ? "moon" : hasNative ? "native" : "nx",
|
|
840
|
+
marker: hasMoon ? moonMarker : hasNative ? ARCHKEEP_MODEL_FILE : NX_CONFIG_FILE,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The workspace identity of `root` — which project model governs it and which
|
|
846
|
+
* marker decided that — without reading one source file or building one graph.
|
|
847
|
+
* The single-project-model gate runs here exactly as it does in
|
|
848
|
+
* `resolveCommandContext`: both markers present is refused here for the same
|
|
849
|
+
* reason it is refused there, because a caller about to describe this
|
|
850
|
+
* workspace must not name a model the full read would have refused.
|
|
851
|
+
*
|
|
852
|
+
* @param {string} root Absolute path to the workspace root.
|
|
853
|
+
* @returns {{provider: "nx" | "moon" | "native", marker: string}}
|
|
854
|
+
* @throws {Error} when more than one project-model marker is present
|
|
855
|
+
* (`requireSingleProjectModel`).
|
|
856
|
+
*/
|
|
857
|
+
export function describeWorkspaceRoot(root) {
|
|
858
|
+
const { hasNative, moonMarker } = requireSingleProjectModel(root);
|
|
859
|
+
return workspaceNames(moonMarker !== null, hasNative, moonMarker);
|
|
860
|
+
}
|
|
861
|
+
|
|
786
862
|
// Re-exported so a caller that only needs "does this tree look like a
|
|
787
863
|
// workspace at all" (`../../cli.mjs`'s `optionsForUsage`) is not forced to
|
|
788
864
|
// duplicate the marker check a second time; `DEFAULT_OPTIONS` rides along for
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy's `coverage.unowned` acceptance channel, matched in ONE place
|
|
3
|
+
* against the two unowned-file sets a run establishes — so `check` and
|
|
4
|
+
* `waivers` cannot disagree about which files a row accepts.
|
|
5
|
+
*
|
|
6
|
+
* The channel exists for the Nx and Moon providers, whose project model has
|
|
7
|
+
* no home of its own for "this file is owned by no project, and we accept
|
|
8
|
+
* that, for this reason" — the decision `archkeep.json`'s `coverage.exempt`
|
|
9
|
+
* records on a native tree (`../providers/native/coverage.mjs`). Without it,
|
|
10
|
+
* the two unowned states those providers report are both permanent: the
|
|
11
|
+
* TS/JS/Vue gap (`./context.mjs`'s `unownedAnalyzableFiles`) warns on every
|
|
12
|
+
* run with no way to answer it, and a Go/Rust/Python unclaimed file
|
|
13
|
+
* (`unclaimedFileFailures` there) is a hard exit 3 with no accepted middle
|
|
14
|
+
* ground.
|
|
15
|
+
*
|
|
16
|
+
* A covered file is a RECORDED acceptance, never an invisible one: `check`
|
|
17
|
+
* still states every accepted file, as the `"accepted-unowned-files"`
|
|
18
|
+
* coverage-gap entry (`./check.mjs`), and `archkeep waivers` names each row
|
|
19
|
+
* with its reason and current coverage. What changes is only the permanent
|
|
20
|
+
* half — the warning stops re-asking a question someone answered, and the
|
|
21
|
+
* unclaimed exit 3 stops firing for a file whose acceptance is on record.
|
|
22
|
+
*
|
|
23
|
+
* ## Matching starts AFTER unowned-ness is decided
|
|
24
|
+
*
|
|
25
|
+
* The inputs here are lists something else already judged: `unownedGap` is
|
|
26
|
+
* the tolerated TS/JS/Vue list with the run's own configuration files
|
|
27
|
+
* subtracted (`./context.mjs`'s `unownedGapWithoutRunConfiguration`, applied
|
|
28
|
+
* by the caller), and `unclaimedFiles` is the Go/Rust/Python unclaimed list
|
|
29
|
+
* the same module computes. A row is matched against those lists ONLY —
|
|
30
|
+
* never against the tracked tree — so even a `**` row can never accept, or
|
|
31
|
+
* silence anything about, a file a project owns. That is the same guarantee
|
|
32
|
+
* the native provider's `coverage.exempt` states, kept by the same
|
|
33
|
+
* construction (`../providers/native/coverage.mjs`'s `judgeCoverage` filters
|
|
34
|
+
* `unowned` first and matches second).
|
|
35
|
+
*
|
|
36
|
+
* A row matching NOTHING across both sets is dead — the files it accepted
|
|
37
|
+
* are owned now, or the path was never right — and dead is a verdict the
|
|
38
|
+
* caller must refuse loudly, not a state to skip: `./check.mjs`'s dead-row
|
|
39
|
+
* block does, in the same sentence shape as the native stale-row refusal
|
|
40
|
+
* (`../providers/native/index.mjs`).
|
|
41
|
+
*/
|
|
42
|
+
import { languageOf } from "../analysis/registry.mjs";
|
|
43
|
+
import { safeMatchesGlob } from "../rules/match.mjs";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The languages a file list spans — sorted and distinct, derived beside the
|
|
47
|
+
* list it describes so no face can name a language the list does not contain
|
|
48
|
+
* (the same rule `./context.mjs`'s `unownedAnalyzableFiles` states).
|
|
49
|
+
*
|
|
50
|
+
* @param {string[]} files
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
function languagesOf(files) {
|
|
54
|
+
return [...new Set(files.map((file) => languageOf(file)))].sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Partitions a run's unowned files into accepted and uncovered under the
|
|
59
|
+
* policy's `coverage.unowned` rows.
|
|
60
|
+
*
|
|
61
|
+
* Everything is derived from the arguments — no filesystem, no policy load —
|
|
62
|
+
* so a test drives it directly and the two callers (`./check.mjs`,
|
|
63
|
+
* `./waivers.mjs`) provably run the identical judgment.
|
|
64
|
+
*
|
|
65
|
+
* @param {{
|
|
66
|
+
* rows: {path: string, reason: string}[],
|
|
67
|
+
* unownedGap: {files: string[], languages: string[]},
|
|
68
|
+
* unclaimedFiles: string[],
|
|
69
|
+
* tracked: string[],
|
|
70
|
+
* }} args `rows` is `config.coverage?.unowned ?? []` — the validated table
|
|
71
|
+
* (`../config.mjs`'s `findCoverageViolations`). `tracked` fixes the order
|
|
72
|
+
* accepted files are reported in: `git ls-files` order, the same order
|
|
73
|
+
* every other file list in a report keeps.
|
|
74
|
+
* @returns {{
|
|
75
|
+
* rows: {path: string, reason: string, index: number, files: string[]}[],
|
|
76
|
+
* dead: {path: string, reason: string, index: number}[],
|
|
77
|
+
* accepted: {files: string[], languages: string[]},
|
|
78
|
+
* acceptedFiles: Set<string>,
|
|
79
|
+
* uncoveredUnowned: {files: string[], languages: string[]},
|
|
80
|
+
* }} `rows` is every declared row with the unowned files it currently
|
|
81
|
+
* accepts; `dead` the subset accepting none. `accepted` is the union of
|
|
82
|
+
* both sets' covered files in tracked order; `uncoveredUnowned` is
|
|
83
|
+
* `unownedGap` minus the accepted files — the warning that survives.
|
|
84
|
+
* Uncovered UNCLAIMED files need no list of their own: their whole-file
|
|
85
|
+
* failures are already in the caller's hands, untouched.
|
|
86
|
+
*/
|
|
87
|
+
export function partitionUnownedCoverage({ rows, unownedGap, unclaimedFiles, tracked }) {
|
|
88
|
+
const candidates = [...unownedGap.files, ...unclaimedFiles];
|
|
89
|
+
const matched = rows.map((row, index) => ({
|
|
90
|
+
path: row.path,
|
|
91
|
+
reason: row.reason,
|
|
92
|
+
index,
|
|
93
|
+
files: candidates.filter((file) => safeMatchesGlob(file, row.path)),
|
|
94
|
+
}));
|
|
95
|
+
const acceptedFiles = new Set(matched.flatMap((entry) => entry.files));
|
|
96
|
+
const acceptedInTrackedOrder = tracked.filter((file) => acceptedFiles.has(file));
|
|
97
|
+
const uncoveredFiles = unownedGap.files.filter((file) => !acceptedFiles.has(file));
|
|
98
|
+
return {
|
|
99
|
+
rows: matched,
|
|
100
|
+
dead: matched
|
|
101
|
+
.filter((entry) => entry.files.length === 0)
|
|
102
|
+
.map(({ path, reason, index }) => ({ path, reason, index })),
|
|
103
|
+
accepted: {
|
|
104
|
+
files: acceptedInTrackedOrder,
|
|
105
|
+
languages: languagesOf(acceptedInTrackedOrder),
|
|
106
|
+
},
|
|
107
|
+
acceptedFiles,
|
|
108
|
+
uncoveredUnowned: {
|
|
109
|
+
files: uncoveredFiles,
|
|
110
|
+
languages: languagesOf(uncoveredFiles),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|