@goodbones/cli 0.1.0-beta.14 → 0.1.0-beta.16
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/build/dts/run.d.ts +2 -0
- package/build/dts/run.d.ts.map +1 -1
- package/build/esm/run.js +150 -19
- package/build/esm/run.js.map +1 -1
- package/package.json +5 -5
- package/src/run.ts +223 -23
package/src/run.ts
CHANGED
|
@@ -28,7 +28,10 @@ import {
|
|
|
28
28
|
renderCampaignRows,
|
|
29
29
|
renderHistory,
|
|
30
30
|
renderNudge,
|
|
31
|
+
renderSectorView,
|
|
31
32
|
reportSpecsOf,
|
|
33
|
+
sectorMovesOf,
|
|
34
|
+
sectorViewOf,
|
|
32
35
|
snapshotCampaignsOf,
|
|
33
36
|
valueOfParts,
|
|
34
37
|
widenedExtensions,
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
breaches,
|
|
40
43
|
CONFORMANCE_MEASURES,
|
|
41
44
|
type ConformanceMeasure,
|
|
45
|
+
countedFiles,
|
|
42
46
|
type CoverageFamily,
|
|
43
47
|
coverageOf,
|
|
44
48
|
cyclesIn,
|
|
@@ -51,12 +55,15 @@ import {
|
|
|
51
55
|
evaluateSelectedBindings,
|
|
52
56
|
evaluateStructure,
|
|
53
57
|
evaluateSurface,
|
|
58
|
+
expandAliases,
|
|
54
59
|
exportRulesSelecting,
|
|
55
60
|
findManifestFile,
|
|
56
61
|
fingerprintOf,
|
|
57
62
|
formatManifestYaml,
|
|
58
63
|
formatMessage,
|
|
59
64
|
fractionsOf,
|
|
65
|
+
globsOf,
|
|
66
|
+
globToRegExp,
|
|
60
67
|
type Graph,
|
|
61
68
|
hasGraphRules,
|
|
62
69
|
heightOf,
|
|
@@ -287,10 +294,18 @@ export type ReportedViolation = Violation & {
|
|
|
287
294
|
readonly entry?: string;
|
|
288
295
|
};
|
|
289
296
|
|
|
297
|
+
// Per family: the files it reaches of those the limits count, the floor the
|
|
298
|
+
// manifest's `limits.coverage` states as a fraction, and the ceiling its
|
|
299
|
+
// `limits.unreached` states on the files left over.
|
|
290
300
|
export type CoverageReport = Readonly<
|
|
291
301
|
Record<
|
|
292
302
|
CoverageFamily,
|
|
293
|
-
{
|
|
303
|
+
{
|
|
304
|
+
readonly covered: number;
|
|
305
|
+
readonly total: number;
|
|
306
|
+
readonly floor?: number;
|
|
307
|
+
readonly ceiling?: number;
|
|
308
|
+
}
|
|
294
309
|
>
|
|
295
310
|
>;
|
|
296
311
|
|
|
@@ -304,6 +319,9 @@ export type ConformanceReport = Readonly<
|
|
|
304
319
|
export type CheckReport = {
|
|
305
320
|
readonly version: 1;
|
|
306
321
|
readonly files: number;
|
|
322
|
+
// Walked files under the manifest's `limits.outside`: judged by every
|
|
323
|
+
// rule, and left out of `coverage` and of the residue.
|
|
324
|
+
readonly outside: number;
|
|
307
325
|
readonly roots: ReadonlyArray<string>;
|
|
308
326
|
readonly ok: boolean;
|
|
309
327
|
// The file the policy was read from, repo-relative, and a hash of its
|
|
@@ -355,6 +373,13 @@ type Measures = {
|
|
|
355
373
|
readonly concentration: ReturnType<typeof slackOf>["concentration"];
|
|
356
374
|
};
|
|
357
375
|
|
|
376
|
+
// The patterns the policy's `limits.outside` names, alias-expanded, each a
|
|
377
|
+
// subtree.
|
|
378
|
+
const outsideOf = (policy: LoadedPolicy): ReadonlyArray<RegExp> =>
|
|
379
|
+
globsOf(policy.config.limits?.outside ?? []).map((glob) =>
|
|
380
|
+
globToRegExp(expandAliases(glob, policy.config.aliases ?? {})),
|
|
381
|
+
);
|
|
382
|
+
|
|
358
383
|
const measuresOf = (
|
|
359
384
|
policy: LoadedPolicy,
|
|
360
385
|
files: ReadonlyArray<string>,
|
|
@@ -365,7 +390,9 @@ const measuresOf = (
|
|
|
365
390
|
// that rather than as lines nobody needs.
|
|
366
391
|
const { concentration, slack } = slackOf(policy.importRules, edges, files);
|
|
367
392
|
return {
|
|
368
|
-
|
|
393
|
+
// Residue is a count of files, and counts the ones the limits count. A
|
|
394
|
+
// node is live or vacant by every file under it, counted or not.
|
|
395
|
+
residue: residueOf(policy, countedFiles(files, outsideOf(policy))),
|
|
369
396
|
vacant: vacancyOf(policy.importRules, files),
|
|
370
397
|
slack,
|
|
371
398
|
concentration,
|
|
@@ -423,25 +450,32 @@ const reportOf = (
|
|
|
423
450
|
|
|
424
451
|
// The floors. A policy states how much of the tree it reaches, per
|
|
425
452
|
// family; falling under is a policy that quietly stopped covering files.
|
|
453
|
+
// Both are held over the files the limits count: what `limits.outside`
|
|
454
|
+
// names is judged by every rule and left out of these numbers.
|
|
426
455
|
const floors = policy.config.limits?.coverage ?? {};
|
|
456
|
+
const unreached = policy.config.limits?.unreached ?? {};
|
|
427
457
|
const files = listSourceFiles(policy.repoRoot, roots, policy.languages);
|
|
428
|
-
const
|
|
458
|
+
const counted = countedFiles(files, outsideOf(policy));
|
|
459
|
+
const found = coverageOf(policy, counted);
|
|
429
460
|
const covered = (family: CoverageFamily): number =>
|
|
430
461
|
family === "structure" ? found.structure.enumerated : found[family].covered;
|
|
431
462
|
const coverage = Object.fromEntries(
|
|
432
463
|
COVERAGE_FAMILIES.map((family) => {
|
|
433
464
|
const floor = floors[family];
|
|
465
|
+
const ceiling = unreached[family];
|
|
434
466
|
return [
|
|
435
467
|
family,
|
|
436
468
|
{
|
|
437
469
|
covered: covered(family),
|
|
438
470
|
total: found.files,
|
|
439
471
|
...(floor === undefined ? {} : { floor }),
|
|
472
|
+
...(ceiling === undefined ? {} : { ceiling }),
|
|
440
473
|
},
|
|
441
474
|
];
|
|
442
475
|
}),
|
|
443
476
|
) as CoverageReport;
|
|
444
477
|
const shortfalls = shortfallsOf(coverage);
|
|
478
|
+
const overruns = overrunsOf(coverage);
|
|
445
479
|
|
|
446
480
|
// The ceilings. What no family reaches, what no file is under and what
|
|
447
481
|
// nothing imports through are each a count the policy may hold itself
|
|
@@ -461,12 +495,14 @@ const reportOf = (
|
|
|
461
495
|
const report: CheckReport = {
|
|
462
496
|
version: 1,
|
|
463
497
|
files: findings.files,
|
|
498
|
+
outside: files.length - counted.length,
|
|
464
499
|
roots,
|
|
465
500
|
ok:
|
|
466
501
|
reportable === 0 &&
|
|
467
502
|
findings.unresolved.length === 0 &&
|
|
468
503
|
stale.length === 0 &&
|
|
469
504
|
shortfalls.length === 0 &&
|
|
505
|
+
overruns.length === 0 &&
|
|
470
506
|
excesses.length === 0 &&
|
|
471
507
|
campaignFailuresOf(campaigns).length === 0,
|
|
472
508
|
manifest: {
|
|
@@ -505,6 +541,25 @@ const shortfallsOf = (coverage: CoverageReport): ReadonlyArray<Shortfall> =>
|
|
|
505
541
|
: [{ family, actual, floor }];
|
|
506
542
|
});
|
|
507
543
|
|
|
544
|
+
// A family leaving more files unreached than the ceiling the policy states
|
|
545
|
+
// for it. A count, so deleting files a rule reached changes nothing here,
|
|
546
|
+
// where it lowers the fraction a floor holds.
|
|
547
|
+
type Overrun = {
|
|
548
|
+
readonly family: CoverageFamily;
|
|
549
|
+
readonly unreached: number;
|
|
550
|
+
readonly ceiling: number;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const overrunsOf = (coverage: CoverageReport): ReadonlyArray<Overrun> =>
|
|
554
|
+
COVERAGE_FAMILIES.flatMap((family) => {
|
|
555
|
+
const { ceiling, covered, total } = coverage[family];
|
|
556
|
+
const unreached = total - covered;
|
|
557
|
+
return ceiling === undefined ||
|
|
558
|
+
!breaches({ direction: "down", limit: ceiling, tolerance: 0 }, unreached)
|
|
559
|
+
? []
|
|
560
|
+
: [{ family, unreached, ceiling }];
|
|
561
|
+
});
|
|
562
|
+
|
|
508
563
|
// A conformance measure over the ceiling the policy states for it.
|
|
509
564
|
type Excess = {
|
|
510
565
|
readonly measure: ConformanceMeasure;
|
|
@@ -529,6 +584,7 @@ const failureOf = (
|
|
|
529
584
|
const [campaignFailure] = campaignFailuresOf(report.campaigns);
|
|
530
585
|
if (campaignFailure !== undefined) return fail(campaignFailure);
|
|
531
586
|
if (shortfalls.length > 0) return fail("coverage below floor");
|
|
587
|
+
if (overrunsOf(report.coverage).length > 0) return fail("unreached above ceiling");
|
|
532
588
|
if (excessesOf(report.conformance).length > 0) return fail("conformance above ceiling");
|
|
533
589
|
if (report.ok) return null;
|
|
534
590
|
return fail("architecture violations");
|
|
@@ -576,6 +632,7 @@ const renderText = (report: CheckReport): ReadonlyArray<string> => {
|
|
|
576
632
|
const carried =
|
|
577
633
|
report.violations.filter((one) => one.kind !== "campaign").length - reportable.length;
|
|
578
634
|
const shortfalls = shortfallsOf(report.coverage);
|
|
635
|
+
const overruns = overrunsOf(report.coverage);
|
|
579
636
|
const excesses = excessesOf(report.conformance);
|
|
580
637
|
return [
|
|
581
638
|
...reportable.map(describe),
|
|
@@ -607,6 +664,18 @@ const renderText = (report: CheckReport): ReadonlyArray<string> => {
|
|
|
607
664
|
"",
|
|
608
665
|
" architecture coverage # which files no rule reaches",
|
|
609
666
|
]),
|
|
667
|
+
...(overruns.length === 0
|
|
668
|
+
? []
|
|
669
|
+
: [
|
|
670
|
+
"",
|
|
671
|
+
"more files are unreached than the ceiling the policy states for itself:",
|
|
672
|
+
...overruns.map(
|
|
673
|
+
(one) =>
|
|
674
|
+
` ${one.family}: ${count(one.unreached, "file")} unreached, ceiling ${String(one.ceiling)}`,
|
|
675
|
+
),
|
|
676
|
+
"",
|
|
677
|
+
" architecture coverage # which files no rule reaches",
|
|
678
|
+
]),
|
|
610
679
|
...(excesses.length === 0
|
|
611
680
|
? []
|
|
612
681
|
: [
|
|
@@ -680,6 +749,7 @@ export const snapshotOf = (
|
|
|
680
749
|
manifest: report_.manifest,
|
|
681
750
|
roots: report_.roots,
|
|
682
751
|
files: report_.files,
|
|
752
|
+
outside: report_.outside,
|
|
683
753
|
ok: report_.ok,
|
|
684
754
|
coverage: report_.coverage,
|
|
685
755
|
conformance: report_.conformance,
|
|
@@ -697,11 +767,23 @@ export const snapshotOf = (
|
|
|
697
767
|
};
|
|
698
768
|
};
|
|
699
769
|
|
|
770
|
+
// The unreached ceiling beside a family's row, and the ratchet's nudge when
|
|
771
|
+
// the count has fallen under it: a ceiling is lowered by hand.
|
|
772
|
+
const unreachedMark = (covered: number, total: number, ceiling: number | undefined): string => {
|
|
773
|
+
if (ceiling === undefined) return "";
|
|
774
|
+
const left = total - covered;
|
|
775
|
+
if (left > ceiling) return ` ${String(left)} unreached > ${String(ceiling)} ✗`;
|
|
776
|
+
return ` ${String(left)} unreached ≤ ${String(ceiling)} ✓${left < ceiling ? `, lower it to ${String(left)}` : ""}`;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const outsideNote = (outside: number): string =>
|
|
780
|
+
outside === 0 ? "" : `, ${count(outside, "file")} outside the limits`;
|
|
781
|
+
|
|
700
782
|
const renderSnapshot = (snapshot: Snapshot): ReadonlyArray<string> => {
|
|
701
783
|
const reportable = snapshot.violations.filter((one) => !one.baselined && !one.ledgered);
|
|
702
784
|
const carried = snapshot.violations.length - reportable.length;
|
|
703
785
|
const row = (family: CoverageFamily): string => {
|
|
704
|
-
const { covered, floor, total } = snapshot.coverage[family];
|
|
786
|
+
const { ceiling, covered, floor, total } = snapshot.coverage[family];
|
|
705
787
|
const fraction = total === 0 ? 1 : covered / total;
|
|
706
788
|
const mark =
|
|
707
789
|
floor === undefined
|
|
@@ -709,7 +791,7 @@ const renderSnapshot = (snapshot: Snapshot): ReadonlyArray<string> => {
|
|
|
709
791
|
: fraction >= floor
|
|
710
792
|
? ` ≥ ${percent(floor)} ✓`
|
|
711
793
|
: ` < ${percent(floor)} ✗`;
|
|
712
|
-
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(total)} ${percent(fraction).padStart(4)}${mark}`;
|
|
794
|
+
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(total)} ${percent(fraction).padStart(4)}${mark}${unreachedMark(covered, total, ceiling)}`;
|
|
713
795
|
};
|
|
714
796
|
const section = (title: string, lines: ReadonlyArray<string>): ReadonlyArray<string> => [
|
|
715
797
|
"",
|
|
@@ -730,7 +812,7 @@ const renderSnapshot = (snapshot: Snapshot): ReadonlyArray<string> => {
|
|
|
730
812
|
};
|
|
731
813
|
|
|
732
814
|
return [
|
|
733
|
-
`${String(snapshot.files)} files under ${snapshot.roots.join(", ")}, against ${snapshot.manifest.path}`,
|
|
815
|
+
`${String(snapshot.files)} files under ${snapshot.roots.join(", ")}, against ${snapshot.manifest.path}${outsideNote(snapshot.outside)}`,
|
|
734
816
|
...section("coverage", COVERAGE_FAMILIES.map(row)),
|
|
735
817
|
...section(
|
|
736
818
|
`residue: ${count(snapshot.residue.files.length, "file")} no family reaches` +
|
|
@@ -833,10 +915,14 @@ export const coverage = (
|
|
|
833
915
|
roots: ReadonlyArray<string>,
|
|
834
916
|
): Effect.Effect<void, CliFailure> =>
|
|
835
917
|
Effect.gen(function* () {
|
|
836
|
-
|
|
918
|
+
// The limits are held over the files they count, so that is what the
|
|
919
|
+
// rows are over: what `limits.outside` names is said, and left out.
|
|
920
|
+
const walked = listSourceFiles(policy.repoRoot, roots, policy.languages);
|
|
921
|
+
const files = countedFiles(walked, outsideOf(policy));
|
|
837
922
|
const found = coverageOf(policy, files);
|
|
838
923
|
const fractions = fractionsOf(found);
|
|
839
924
|
const floors = policy.config.limits?.coverage ?? {};
|
|
925
|
+
const ceilings = policy.config.limits?.unreached ?? {};
|
|
840
926
|
const row = (family: keyof typeof fractions, covered: number, note: string): string => {
|
|
841
927
|
const floor = floors[family];
|
|
842
928
|
const mark =
|
|
@@ -845,11 +931,11 @@ export const coverage = (
|
|
|
845
931
|
: fractions[family] >= floor
|
|
846
932
|
? ` ≥ ${percent(floor)} ✓`
|
|
847
933
|
: ` < ${percent(floor)} ✗`;
|
|
848
|
-
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(found.files)} ${percent(fractions[family]).padStart(4)} ${note}${mark}`;
|
|
934
|
+
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(found.files)} ${percent(fractions[family]).padStart(4)} ${note}${mark}${unreachedMark(covered, found.files, ceilings[family])}`;
|
|
849
935
|
};
|
|
850
936
|
|
|
851
937
|
yield* report([
|
|
852
|
-
`${String(
|
|
938
|
+
`${String(walked.length)} files under ${roots.join(", ")}${outsideNote(walked.length - files.length)}`,
|
|
853
939
|
"",
|
|
854
940
|
row("imports", found.imports.covered, "under an import allowlist"),
|
|
855
941
|
row(
|
|
@@ -987,14 +1073,14 @@ export const explain = (
|
|
|
987
1073
|
? `${String(parts.of)} of ${String(parts.per)} here`
|
|
988
1074
|
: `${String(valueOfParts(measure, parts))} here`;
|
|
989
1075
|
return [
|
|
990
|
-
` ${objective.name} — ${firstSentence(objective.
|
|
1076
|
+
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (measure, ${objective.direction}; ${here})`,
|
|
991
1077
|
];
|
|
992
1078
|
}
|
|
993
1079
|
const table = explainObjective(objective, input);
|
|
994
1080
|
if (table.length === 0) return [];
|
|
995
1081
|
const fired = table.length > 0 && table.every((line) => line.answer);
|
|
996
1082
|
return [
|
|
997
|
-
` ${objective.name} — ${firstSentence(objective.
|
|
1083
|
+
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (${objective.holdout}; ${fired ? "fires here" : "no hit"})`,
|
|
998
1084
|
...table.map(
|
|
999
1085
|
(line) =>
|
|
1000
1086
|
` ${line.answer ? "✓" : "✗"} ${line.term}${line.count === undefined ? "" : ` (${String(line.count)})`}`,
|
|
@@ -1156,6 +1242,7 @@ limits:
|
|
|
1156
1242
|
# perimeter: file
|
|
1157
1243
|
# objectives:
|
|
1158
1244
|
# is-ts:
|
|
1245
|
+
# intent: A JavaScript file is one the strict compiler never checks.
|
|
1159
1246
|
# holdout: file
|
|
1160
1247
|
# match: { path: { file: "\\\\.(js|jsx)$" } }
|
|
1161
1248
|
# probes: { fires: [{ path: src/legacy/util.js }], ignores: [{ path: src/util.ts }] }
|
|
@@ -1380,7 +1467,8 @@ const campaignFor = (
|
|
|
1380
1467
|
: Result.succeed(found);
|
|
1381
1468
|
}
|
|
1382
1469
|
const [only] = campaignsOf(policy).campaignRules;
|
|
1383
|
-
if (campaignsOf(policy).campaignRules.length === 1 && only !== undefined)
|
|
1470
|
+
if (campaignsOf(policy).campaignRules.length === 1 && only !== undefined)
|
|
1471
|
+
return Result.succeed(only);
|
|
1384
1472
|
return Result.fail(
|
|
1385
1473
|
`this policy declares ${String(campaignsOf(policy).campaignRules.length)} campaigns; say which with --campaign <id>.`,
|
|
1386
1474
|
);
|
|
@@ -1424,7 +1512,9 @@ export const objectives = (
|
|
|
1424
1512
|
const targets =
|
|
1425
1513
|
parsed.target === null
|
|
1426
1514
|
? campaignsOf(policy).campaignRules
|
|
1427
|
-
: campaignsOf(policy).campaignRules.filter(
|
|
1515
|
+
: campaignsOf(policy).campaignRules.filter(
|
|
1516
|
+
(rule) => rule.id === parsed.target?.campaign,
|
|
1517
|
+
);
|
|
1428
1518
|
const by = authorOf(flagOf(argv, "--by")) ?? "unknown";
|
|
1429
1519
|
const all = evaluations();
|
|
1430
1520
|
const lines: Array<string> = [];
|
|
@@ -1439,6 +1529,9 @@ export const objectives = (
|
|
|
1439
1529
|
try: () => clear(policy, evaluation, only, by),
|
|
1440
1530
|
catch: (cause) => fail(String(cause)),
|
|
1441
1531
|
});
|
|
1532
|
+
// Where each sector went, read off the ledgers as they stood: a
|
|
1533
|
+
// clear of one objective places no sector, so it says nothing.
|
|
1534
|
+
const moves = only === null ? sectorMovesOf(policy, evaluation) : [];
|
|
1442
1535
|
for (const outcome of outcomes) {
|
|
1443
1536
|
const parts = [
|
|
1444
1537
|
...(outcome.entered.length > 0
|
|
@@ -1471,6 +1564,16 @@ export const objectives = (
|
|
|
1471
1564
|
.join(", ")})`,
|
|
1472
1565
|
]
|
|
1473
1566
|
: []),
|
|
1567
|
+
...(outcome.measure.grown.length > 0
|
|
1568
|
+
? [
|
|
1569
|
+
`${count(outcome.measure.grown.length, "sector")} grown (${outcome.measure.grown
|
|
1570
|
+
.map(
|
|
1571
|
+
(one) =>
|
|
1572
|
+
`${one.sector} ${String(one.from)} → ${String(one.to)}, ${one.phase === null ? "measured, not held" : `as ${one.phase} expects`}`,
|
|
1573
|
+
)
|
|
1574
|
+
.join(", ")})`,
|
|
1575
|
+
]
|
|
1576
|
+
: []),
|
|
1474
1577
|
...(outcome.closed > 0 ? [`${count(outcome.closed, "sector")} closed`] : []),
|
|
1475
1578
|
...(outcome.rebaselined.length > 0
|
|
1476
1579
|
? [
|
|
@@ -1487,6 +1590,14 @@ export const objectives = (
|
|
|
1487
1590
|
`${outcome.campaign}/${outcome.objective}: ${parts.length === 0 ? "nothing to clear" : parts.join(", ")}; ${count(outcome.left, "holdout")} left.`,
|
|
1488
1591
|
);
|
|
1489
1592
|
}
|
|
1593
|
+
for (const move of moves) {
|
|
1594
|
+
lines.push(
|
|
1595
|
+
`${move.campaign}: ${move.sector} ${move.back ? "went back" : "moved"} ${move.from ?? "done"} → ${move.to ?? "done"}` +
|
|
1596
|
+
(move.passed.length === 0
|
|
1597
|
+
? "."
|
|
1598
|
+
: `, passing ${move.passed.join(", ")} in the same clear: nothing there was ever counted for it.`),
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1490
1601
|
}
|
|
1491
1602
|
return yield* report(lines);
|
|
1492
1603
|
}
|
|
@@ -1498,7 +1609,9 @@ export const objectives = (
|
|
|
1498
1609
|
),
|
|
1499
1610
|
);
|
|
1500
1611
|
}
|
|
1501
|
-
const rule = campaignsOf(policy).campaignRules.find(
|
|
1612
|
+
const rule = campaignsOf(policy).campaignRules.find(
|
|
1613
|
+
(one) => one.id === parsed.target?.campaign,
|
|
1614
|
+
);
|
|
1502
1615
|
if (rule === undefined)
|
|
1503
1616
|
return yield* Effect.fail(fail(`no campaign is named "${parsed.target.campaign}"`));
|
|
1504
1617
|
const objective = objectiveFor(rule, parsed.target.objective);
|
|
@@ -1540,6 +1653,21 @@ export const objectives = (
|
|
|
1540
1653
|
},
|
|
1541
1654
|
);
|
|
1542
1655
|
if (Result.isFailure(outcome)) return yield* Effect.fail(fail(outcome.failure));
|
|
1656
|
+
// A concession that sends a sector back a phase says so, and names
|
|
1657
|
+
// the attestations it revoked on the way.
|
|
1658
|
+
const sentBack = outcome.success.sentBack.flatMap((one) => [
|
|
1659
|
+
"",
|
|
1660
|
+
`this concession moves ${one.sector} back ${one.from ?? "done"} → ${one.to ?? "done"}.`,
|
|
1661
|
+
...(one.revoked.length === 0
|
|
1662
|
+
? []
|
|
1663
|
+
: [
|
|
1664
|
+
`It revokes ${one.sector}'s attestation of ${one.revoked.join(", ")}: what was attested no longer holds. Once it does again, attest it anew:`,
|
|
1665
|
+
...one.revoked.map(
|
|
1666
|
+
(phase) =>
|
|
1667
|
+
` architecture campaigns attest ${one.sector} ${phase} --reason "…" --campaign ${rule.id}`,
|
|
1668
|
+
),
|
|
1669
|
+
]),
|
|
1670
|
+
]);
|
|
1543
1671
|
const scalar =
|
|
1544
1672
|
rule.objectives.find((one) => one.id === objective.success)?.measure !== null;
|
|
1545
1673
|
if (scalar) {
|
|
@@ -1557,6 +1685,7 @@ export const objectives = (
|
|
|
1557
1685
|
"",
|
|
1558
1686
|
`${count(outcome.success.left.length, "sector")} left past the record; check still fails on them.`,
|
|
1559
1687
|
]),
|
|
1688
|
+
...sentBack,
|
|
1560
1689
|
]);
|
|
1561
1690
|
}
|
|
1562
1691
|
if (outcome.success.conceded.length === 0) {
|
|
@@ -1573,6 +1702,7 @@ export const objectives = (
|
|
|
1573
1702
|
"",
|
|
1574
1703
|
`${count(outcome.success.left.length, "hit")} left unrecorded; check still fails on them.`,
|
|
1575
1704
|
]),
|
|
1705
|
+
...sentBack,
|
|
1576
1706
|
]);
|
|
1577
1707
|
}
|
|
1578
1708
|
default:
|
|
@@ -1602,16 +1732,63 @@ export const campaigns = (
|
|
|
1602
1732
|
const json = argv.includes("--json");
|
|
1603
1733
|
const retired = parsed.subcommand === undefined ? undefined : RETIRED[parsed.subcommand];
|
|
1604
1734
|
if (retired !== undefined) return yield* Effect.fail(fail(retired));
|
|
1735
|
+
// A positional that is no path is refused, not walked: most often it is
|
|
1736
|
+
// a sector's name, and `--sector` is what asks about one.
|
|
1737
|
+
for (const root of parsed.roots) {
|
|
1738
|
+
if (existsSync(path.resolve(policy.repoRoot, root))) continue;
|
|
1739
|
+
const named = collectFindings(policy, defaultRoots).campaigns.some((one) =>
|
|
1740
|
+
one.sectors.has(root),
|
|
1741
|
+
);
|
|
1742
|
+
return yield* Effect.fail(
|
|
1743
|
+
fail(
|
|
1744
|
+
`campaigns: ${root} is not a path in the repository, so there is nothing to walk there.` +
|
|
1745
|
+
(named ? ` It names a sector: \`campaigns status --sector ${root}\`.` : ""),
|
|
1746
|
+
),
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
const sector = flagOf(argv, "--sector");
|
|
1750
|
+
if (parsed.subcommand === "status" && sector !== undefined) {
|
|
1751
|
+
const only = argv.includes("--campaign") ? campaignFor(policy, argv) : null;
|
|
1752
|
+
if (only !== null && Result.isFailure(only)) return yield* Effect.fail(fail(only.failure));
|
|
1753
|
+
const views = collectFindings(policy, roots)
|
|
1754
|
+
.campaigns.filter((one) => only === null || one.rule.id === only.success.id)
|
|
1755
|
+
.flatMap((one) => {
|
|
1756
|
+
const view = sectorViewOf(policy, one, sector);
|
|
1757
|
+
return view === null ? [] : [view];
|
|
1758
|
+
});
|
|
1759
|
+
if (views.length === 0) {
|
|
1760
|
+
return yield* Effect.fail(
|
|
1761
|
+
fail(`campaigns: no campaign under ${roots.join(", ")} has a sector named ${sector}.`),
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
return yield* report(
|
|
1765
|
+
json
|
|
1766
|
+
? [JSON.stringify({ version: 1, sectors: views }, null, 2)]
|
|
1767
|
+
: views.flatMap((view, i) => [...(i === 0 ? [] : [""]), ...renderSectorView(view)]),
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1605
1770
|
|
|
1606
|
-
|
|
1771
|
+
// `status` without `--changed` is the overview, in text or JSON: the
|
|
1772
|
+
// nudge alone is scoped to a diff.
|
|
1773
|
+
const subcommand =
|
|
1774
|
+
parsed.subcommand === "status" && !argv.includes("--changed") ? undefined : parsed.subcommand;
|
|
1775
|
+
switch (subcommand) {
|
|
1607
1776
|
case undefined: {
|
|
1608
1777
|
const snapshot = snapshotOf(policy, roots, manifestPathOf(policy.repoRoot, configFilename));
|
|
1778
|
+
if (json) {
|
|
1779
|
+
// Each campaign as the conformance snapshot carries it: its
|
|
1780
|
+
// burn-down, its ladder, and a row per sector.
|
|
1781
|
+
return yield* report([
|
|
1782
|
+
JSON.stringify({ version: 1, campaigns: snapshot.campaigns }, null, 2),
|
|
1783
|
+
]);
|
|
1784
|
+
}
|
|
1609
1785
|
return yield* report([
|
|
1610
1786
|
`${count(snapshot.campaigns.length, "campaign")} under ${roots.join(", ")}`,
|
|
1611
1787
|
"",
|
|
1612
1788
|
...renderCampaignRows(snapshot.campaigns),
|
|
1613
1789
|
"",
|
|
1614
1790
|
" architecture campaigns status --changed [--base <ref>] [--json] # what a diff touches, and what to do",
|
|
1791
|
+
" architecture campaigns status --sector <sector> [--json] # where one sector stands, and what holds it",
|
|
1615
1792
|
" architecture objectives clear [<campaign>[/<objective>]] # reconcile the ledgers with the code",
|
|
1616
1793
|
' architecture objectives concede <campaign>[/<objective>] --reason "<why>" # record why a count may rise',
|
|
1617
1794
|
' architecture campaigns attest <sector> <phase> --reason "<why>" [--evidence <url>]',
|
|
@@ -1620,11 +1797,6 @@ export const campaigns = (
|
|
|
1620
1797
|
]);
|
|
1621
1798
|
}
|
|
1622
1799
|
case "status": {
|
|
1623
|
-
if (!argv.includes("--changed")) {
|
|
1624
|
-
return yield* Effect.fail(
|
|
1625
|
-
fail("campaigns status takes --changed: the nudge is scoped to a diff."),
|
|
1626
|
-
);
|
|
1627
|
-
}
|
|
1628
1800
|
const base = flagOf(argv, "--base") ?? null;
|
|
1629
1801
|
const diff = yield* Effect.try({
|
|
1630
1802
|
try: () => readDiff(policy.repoRoot, base),
|
|
@@ -1642,6 +1814,19 @@ export const campaigns = (
|
|
|
1642
1814
|
catch: (cause) =>
|
|
1643
1815
|
fail(`could not evaluate the base tree at ${base}: ${String(cause)}`),
|
|
1644
1816
|
});
|
|
1817
|
+
// The ledger mode's "before" for work done ahead of the plan is the
|
|
1818
|
+
// HEAD tree, evaluated once per commit and cached: no ledger counts
|
|
1819
|
+
// the holdouts of a phase a sector has not entered.
|
|
1820
|
+
const headTree =
|
|
1821
|
+
base !== null || !current.some((one) => one.rule.phases.length > 0)
|
|
1822
|
+
? null
|
|
1823
|
+
: yield* Effect.tryPromise({
|
|
1824
|
+
try: () =>
|
|
1825
|
+
baseSideAt(policy, "HEAD", roots, (repoRoot) =>
|
|
1826
|
+
loadPolicyFromFile(repoRoot, configFilename),
|
|
1827
|
+
),
|
|
1828
|
+
catch: (cause) => fail(`could not evaluate the tree at HEAD: ${String(cause)}`),
|
|
1829
|
+
});
|
|
1645
1830
|
const hotfix = flagOf(argv, "--hotfix") ?? null;
|
|
1646
1831
|
const nudge = nudgeOf(
|
|
1647
1832
|
policy,
|
|
@@ -1650,10 +1835,23 @@ export const campaigns = (
|
|
|
1650
1835
|
baseSide,
|
|
1651
1836
|
hotfix,
|
|
1652
1837
|
hotfix === null ? null : authorOf(flagOf(argv, "--by")),
|
|
1838
|
+
headTree,
|
|
1653
1839
|
);
|
|
1654
1840
|
yield* report(json ? [JSON.stringify(nudge, null, 2)] : renderNudge(nudge, policy.now));
|
|
1655
|
-
if (!nudge.ok)
|
|
1656
|
-
|
|
1841
|
+
if (!nudge.ok) {
|
|
1842
|
+
const ahead = nudge.sectors.some((one) => one.verdict === "ahead");
|
|
1843
|
+
const back = nudge.sectors.some(
|
|
1844
|
+
(one) => one.verdict !== "ok" && one.verdict !== "ahead" && one.verdict !== "hotfix",
|
|
1845
|
+
);
|
|
1846
|
+
return yield* Effect.fail(
|
|
1847
|
+
fail(
|
|
1848
|
+
[
|
|
1849
|
+
...(back ? ["the diff sends a sector back under its onTouch"] : []),
|
|
1850
|
+
...(ahead ? ["the diff works ahead of a sector's phase under its onAhead"] : []),
|
|
1851
|
+
].join("; "),
|
|
1852
|
+
),
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1657
1855
|
return;
|
|
1658
1856
|
}
|
|
1659
1857
|
case "attest": {
|
|
@@ -1809,7 +2007,9 @@ export const run = (
|
|
|
1809
2007
|
yield* Effect.tryPromise({
|
|
1810
2008
|
try: () =>
|
|
1811
2009
|
Promise.all(
|
|
1812
|
-
reportSpecsOf(campaignsOf(policy).campaignRules).map((spec) =>
|
|
2010
|
+
reportSpecsOf(campaignsOf(policy).campaignRules).map((spec) =>
|
|
2011
|
+
campaignsOf(policy).reports.read?.(spec),
|
|
2012
|
+
),
|
|
1813
2013
|
),
|
|
1814
2014
|
catch: (cause) => fail(String(cause)),
|
|
1815
2015
|
});
|