@goodbones/cli 0.1.0-beta.17 → 0.1.0-beta.18
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/campaigns/host.d.ts +51 -0
- package/build/dts/campaigns/host.d.ts.map +1 -0
- package/build/dts/campaigns/live.d.ts +3 -0
- package/build/dts/campaigns/live.d.ts.map +1 -0
- package/build/dts/campaigns/none.d.ts +7 -0
- package/build/dts/campaigns/none.d.ts.map +1 -0
- package/build/dts/campaigns/verbs.d.ts +6 -0
- package/build/dts/campaigns/verbs.d.ts.map +1 -0
- package/build/dts/config-loader.d.ts +7 -5
- package/build/dts/config-loader.d.ts.map +1 -1
- package/build/dts/output.d.ts +9 -0
- package/build/dts/output.d.ts.map +1 -0
- package/build/dts/run.d.ts +3 -18
- package/build/dts/run.d.ts.map +1 -1
- package/build/esm/campaigns/host.js +2 -0
- package/build/esm/campaigns/host.js.map +1 -0
- package/build/esm/campaigns/live.js +106 -0
- package/build/esm/campaigns/live.js.map +1 -0
- package/build/esm/campaigns/none.js +34 -0
- package/build/esm/campaigns/none.js.map +1 -0
- package/build/esm/campaigns/verbs.js +489 -0
- package/build/esm/campaigns/verbs.js.map +1 -0
- package/build/esm/config-loader.js +40 -25
- package/build/esm/config-loader.js.map +1 -1
- package/build/esm/infer.js +3 -3
- package/build/esm/infer.js.map +1 -1
- package/build/esm/output.js +8 -0
- package/build/esm/output.js.map +1 -0
- package/build/esm/run.js +39 -555
- package/build/esm/run.js.map +1 -1
- package/package.json +18 -6
- package/src/campaigns/host.ts +120 -0
- package/src/campaigns/live.ts +136 -0
- package/src/campaigns/none.ts +47 -0
- package/src/campaigns/verbs.ts +620 -0
- package/src/config-loader.ts +41 -26
- package/src/infer.ts +3 -3
- package/src/output.ts +18 -0
- package/src/run.ts +56 -726
package/src/run.ts
CHANGED
|
@@ -2,40 +2,6 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
attest,
|
|
7
|
-
authorOf,
|
|
8
|
-
baseSideAt,
|
|
9
|
-
type CampaignEvaluation,
|
|
10
|
-
campaignFailuresOf,
|
|
11
|
-
type CampaignReport,
|
|
12
|
-
campaignReportsOf,
|
|
13
|
-
campaignsOf,
|
|
14
|
-
campaignsSelecting,
|
|
15
|
-
clear,
|
|
16
|
-
concede,
|
|
17
|
-
evaluateCampaigns,
|
|
18
|
-
explainCampaignLines,
|
|
19
|
-
explainObjective,
|
|
20
|
-
historyOf,
|
|
21
|
-
hitsInWindow,
|
|
22
|
-
ledgeredFilter,
|
|
23
|
-
measureFile,
|
|
24
|
-
note,
|
|
25
|
-
nudgeOf,
|
|
26
|
-
readDiff,
|
|
27
|
-
renderCampaignReports,
|
|
28
|
-
renderCampaignRows,
|
|
29
|
-
renderHistory,
|
|
30
|
-
renderNudge,
|
|
31
|
-
renderSectorView,
|
|
32
|
-
reportSpecsOf,
|
|
33
|
-
sectorMovesOf,
|
|
34
|
-
sectorViewOf,
|
|
35
|
-
snapshotCampaignsOf,
|
|
36
|
-
valueOfParts,
|
|
37
|
-
widenedExtensions,
|
|
38
|
-
} from "@goodbones/campaigns";
|
|
39
5
|
import {
|
|
40
6
|
type Baseline,
|
|
41
7
|
baselineOf,
|
|
@@ -90,9 +56,11 @@ import {
|
|
|
90
56
|
import * as Effect from "effect/Effect";
|
|
91
57
|
import * as Result from "effect/Result";
|
|
92
58
|
|
|
59
|
+
import type { CampaignEvaluation, CampaignReport, VerbContext } from "./campaigns/host.js";
|
|
93
60
|
import { type LoadedPolicy, loadPolicyFromFile, manifestPathOf } from "./config-loader.js";
|
|
94
61
|
import { buildGraph } from "./graph.js";
|
|
95
62
|
import { infer } from "./infer.js";
|
|
63
|
+
import { type CliFailure, count, fail, report } from "./output.js";
|
|
96
64
|
import { sourceFactsOf } from "./source-facts.js";
|
|
97
65
|
|
|
98
66
|
// The policy, run with no linter in the loop.
|
|
@@ -107,9 +75,7 @@ import { sourceFactsOf } from "./source-facts.js";
|
|
|
107
75
|
// same vocabulary — a specifier, a binding, a member site — so they answer to
|
|
108
76
|
// the same core rather than to each other.
|
|
109
77
|
|
|
110
|
-
export type CliFailure
|
|
111
|
-
|
|
112
|
-
const fail = (message: string): CliFailure => ({ _tag: "CliFailure", message });
|
|
78
|
+
export type { CliFailure } from "./output.js";
|
|
113
79
|
|
|
114
80
|
// An edge the resolver could not turn into a file. It is reported on its own,
|
|
115
81
|
// since every import rule about it enforces nothing.
|
|
@@ -153,7 +119,7 @@ export const collectFindings = (
|
|
|
153
119
|
policy.repoRoot,
|
|
154
120
|
roots,
|
|
155
121
|
policy.languages,
|
|
156
|
-
widenedExtensions(policy),
|
|
122
|
+
policy.campaigns.widenedExtensions(policy),
|
|
157
123
|
);
|
|
158
124
|
const known = new Set(policy.languages.flatMap((one) => one.extensions));
|
|
159
125
|
const files = walked.filter((file) => known.has(path.extname(file)));
|
|
@@ -191,7 +157,7 @@ export const collectFindings = (
|
|
|
191
157
|
}
|
|
192
158
|
|
|
193
159
|
// The campaigns, over every walked file, through the same caches.
|
|
194
|
-
const campaigns =
|
|
160
|
+
const campaigns = policy.campaigns.evaluate(policy, roots, walked, { textOf, factsOf });
|
|
195
161
|
|
|
196
162
|
for (const file of files) {
|
|
197
163
|
for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
|
|
@@ -270,11 +236,6 @@ const readBaseline = (policy: LoadedPolicy): Baseline => {
|
|
|
270
236
|
}
|
|
271
237
|
};
|
|
272
238
|
|
|
273
|
-
const report = (lines: ReadonlyArray<string>): Effect.Effect<void> =>
|
|
274
|
-
Effect.sync(() => {
|
|
275
|
-
for (const line of lines) process.stdout.write(`${line}\n`);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
239
|
const describe = (violation: Violation): string =>
|
|
279
240
|
` ${violation.file}\n ${formatMessage(violation)}`;
|
|
280
241
|
|
|
@@ -424,7 +385,6 @@ const reportOf = (
|
|
|
424
385
|
const baseline = readBaseline(policy);
|
|
425
386
|
const stale = staleEntriesOf(baseline, findings.violations);
|
|
426
387
|
const { isBaselined } = makeBaselineFilter(baseline);
|
|
427
|
-
const isLedgered = ledgeredFilter(policy, findings.campaigns);
|
|
428
388
|
const violations: Array<ReportedViolation> = [
|
|
429
389
|
...findings.violations.map((violation) => ({
|
|
430
390
|
...violation,
|
|
@@ -434,19 +394,17 @@ const reportOf = (
|
|
|
434
394
|
})),
|
|
435
395
|
// The hits that count: those whose objective is in window for the
|
|
436
396
|
// sector they fall in.
|
|
437
|
-
...findings.campaigns.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
})),
|
|
447
|
-
),
|
|
397
|
+
...policy.campaigns.hitsOf(policy, findings.campaigns).map((hit) => ({
|
|
398
|
+
...hit.violation,
|
|
399
|
+
fingerprint: fingerprintOf(hit.violation),
|
|
400
|
+
baselined: false,
|
|
401
|
+
ledgered: hit.ledgered,
|
|
402
|
+
objective: hit.objective,
|
|
403
|
+
sector: hit.sector,
|
|
404
|
+
entry: hit.entry,
|
|
405
|
+
})),
|
|
448
406
|
];
|
|
449
|
-
const campaigns =
|
|
407
|
+
const campaigns = policy.campaigns.reportsOf(policy, findings.campaigns);
|
|
450
408
|
|
|
451
409
|
// The floors. A policy states how much of the tree it reaches, per
|
|
452
410
|
// family; falling under is a policy that quietly stopped covering files.
|
|
@@ -504,7 +462,7 @@ const reportOf = (
|
|
|
504
462
|
shortfalls.length === 0 &&
|
|
505
463
|
overruns.length === 0 &&
|
|
506
464
|
excesses.length === 0 &&
|
|
507
|
-
|
|
465
|
+
policy.campaigns.failuresOf(campaigns).length === 0,
|
|
508
466
|
manifest: {
|
|
509
467
|
path: path.relative(policy.repoRoot, manifestPath).replaceAll(path.sep, "/"),
|
|
510
468
|
sha256: sha256Of(manifestPath),
|
|
@@ -577,11 +535,12 @@ const excessesOf = (conformance: ConformanceReport): ReadonlyArray<Excess> =>
|
|
|
577
535
|
});
|
|
578
536
|
|
|
579
537
|
const failureOf = (
|
|
538
|
+
policy: LoadedPolicy,
|
|
580
539
|
report: CheckReport,
|
|
581
540
|
shortfalls: ReadonlyArray<Shortfall>,
|
|
582
541
|
): CliFailure | null => {
|
|
583
542
|
if (report.stale.length > 0) return fail("stale baseline entries");
|
|
584
|
-
const [campaignFailure] =
|
|
543
|
+
const [campaignFailure] = policy.campaigns.failuresOf(report.campaigns);
|
|
585
544
|
if (campaignFailure !== undefined) return fail(campaignFailure);
|
|
586
545
|
if (shortfalls.length > 0) return fail("coverage below floor");
|
|
587
546
|
if (overrunsOf(report.coverage).length > 0) return fail("unreached above ceiling");
|
|
@@ -606,8 +565,8 @@ const describeExcess = (one: Excess): string => {
|
|
|
606
565
|
return ` ${one.measure}: ${count(one.count, singular, plural)}, ceiling ${String(one.ceiling)}`;
|
|
607
566
|
};
|
|
608
567
|
|
|
609
|
-
const renderCampaigns = (report: CheckReport): ReadonlyArray<string> =>
|
|
610
|
-
|
|
568
|
+
const renderCampaigns = (policy: LoadedPolicy, report: CheckReport): ReadonlyArray<string> =>
|
|
569
|
+
policy.campaigns.renderReports(
|
|
611
570
|
report.campaigns,
|
|
612
571
|
report.violations.flatMap((one) =>
|
|
613
572
|
one.kind === "campaign" &&
|
|
@@ -627,7 +586,7 @@ const renderCampaigns = (report: CheckReport): ReadonlyArray<string> =>
|
|
|
627
586
|
),
|
|
628
587
|
);
|
|
629
588
|
|
|
630
|
-
const renderText = (report: CheckReport): ReadonlyArray<string> => {
|
|
589
|
+
const renderText = (policy: LoadedPolicy, report: CheckReport): ReadonlyArray<string> => {
|
|
631
590
|
const reportable = report.violations.filter((one) => one.kind !== "campaign" && !one.baselined);
|
|
632
591
|
const carried =
|
|
633
592
|
report.violations.filter((one) => one.kind !== "campaign").length - reportable.length;
|
|
@@ -685,7 +644,7 @@ const renderText = (report: CheckReport): ReadonlyArray<string> => {
|
|
|
685
644
|
"",
|
|
686
645
|
" architecture conformance # which files, nodes and allowances",
|
|
687
646
|
]),
|
|
688
|
-
...renderCampaigns(report),
|
|
647
|
+
...renderCampaigns(policy, report),
|
|
689
648
|
];
|
|
690
649
|
};
|
|
691
650
|
|
|
@@ -704,17 +663,14 @@ export const check = (
|
|
|
704
663
|
// JSON is one object on stdout and nothing else there; the failure, when
|
|
705
664
|
// there is one, is a sentence on stderr and the exit code, as in text.
|
|
706
665
|
yield* report(
|
|
707
|
-
options.format === "json" ? [JSON.stringify(report_, null, 2)] : renderText(report_),
|
|
666
|
+
options.format === "json" ? [JSON.stringify(report_, null, 2)] : renderText(policy, report_),
|
|
708
667
|
);
|
|
709
|
-
const failure = failureOf(report_, shortfallsOf(report_.coverage));
|
|
668
|
+
const failure = failureOf(policy, report_, shortfallsOf(report_.coverage));
|
|
710
669
|
if (failure !== null) return yield* Effect.fail(failure);
|
|
711
670
|
});
|
|
712
671
|
|
|
713
672
|
const percent = (fraction: number): string => `${String(Math.floor(fraction * 100))}%`;
|
|
714
673
|
|
|
715
|
-
const count = (n: number, noun: string, plural = `${noun}s`): string =>
|
|
716
|
-
`${String(n)} ${n === 1 ? noun : plural}`;
|
|
717
|
-
|
|
718
674
|
// The conformance snapshot: `check`'s report grown with what no family
|
|
719
675
|
// reaches, what the allowlists permit and nothing uses, the cycle count and
|
|
720
676
|
// the size of the debt — the whole distance between the tree and the
|
|
@@ -742,7 +698,7 @@ export const snapshotOf = (
|
|
|
742
698
|
return byHeight !== 0 ? byHeight : left.fingerprint.localeCompare(right.fingerprint);
|
|
743
699
|
});
|
|
744
700
|
|
|
745
|
-
const campaigns =
|
|
701
|
+
const campaigns = policy.campaigns.snapshotOf(policy, findings.campaigns);
|
|
746
702
|
|
|
747
703
|
return {
|
|
748
704
|
version: SNAPSHOT_VERSION,
|
|
@@ -779,7 +735,7 @@ const unreachedMark = (covered: number, total: number, ceiling: number | undefin
|
|
|
779
735
|
const outsideNote = (outside: number): string =>
|
|
780
736
|
outside === 0 ? "" : `, ${count(outside, "file")} outside the limits`;
|
|
781
737
|
|
|
782
|
-
const renderSnapshot = (snapshot: Snapshot): ReadonlyArray<string> => {
|
|
738
|
+
const renderSnapshot = (policy: LoadedPolicy, snapshot: Snapshot): ReadonlyArray<string> => {
|
|
783
739
|
const reportable = snapshot.violations.filter((one) => !one.baselined && !one.ledgered);
|
|
784
740
|
const carried = snapshot.violations.length - reportable.length;
|
|
785
741
|
const row = (family: CoverageFamily): string => {
|
|
@@ -880,7 +836,7 @@ const renderSnapshot = (snapshot: Snapshot): ReadonlyArray<string> => {
|
|
|
880
836
|
(snapshot.campaigns.some((one) => one.complete && one.ledgered)
|
|
881
837
|
? `, ${count(snapshot.campaigns.filter((one) => one.complete && one.ledgered).length, "complete", "complete")}`
|
|
882
838
|
: ""),
|
|
883
|
-
|
|
839
|
+
policy.campaigns.renderRows(snapshot.campaigns),
|
|
884
840
|
)),
|
|
885
841
|
"",
|
|
886
842
|
`cycles: ${String(snapshot.cycles)}`,
|
|
@@ -903,7 +859,9 @@ export const conformance = (
|
|
|
903
859
|
Effect.gen(function* () {
|
|
904
860
|
const snapshot = snapshotOf(policy, roots, options.manifestPath);
|
|
905
861
|
yield* report(
|
|
906
|
-
options.format === "json"
|
|
862
|
+
options.format === "json"
|
|
863
|
+
? [JSON.stringify(snapshot, null, 2)]
|
|
864
|
+
: renderSnapshot(policy, snapshot),
|
|
907
865
|
);
|
|
908
866
|
});
|
|
909
867
|
|
|
@@ -1036,59 +994,13 @@ export const explain = (
|
|
|
1036
994
|
const section = (title: string, lines: ReadonlyArray<string>): ReadonlyArray<string> =>
|
|
1037
995
|
lines.length === 0 ? [] : ["", title, ...lines];
|
|
1038
996
|
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const selectedCampaigns = campaignsSelecting(campaignsOf(policy).campaignRules, relative);
|
|
1047
|
-
const evaluations =
|
|
1048
|
-
selectedCampaigns.length === 0 ? [] : collectFindings(policy, roots).campaigns;
|
|
1049
|
-
const campaignLines = selectedCampaigns.flatMap((rule) => {
|
|
1050
|
-
const at = path.join(policy.repoRoot, relative);
|
|
1051
|
-
const text = existsSync(at) ? readFileSync(at, "utf8") : "";
|
|
1052
|
-
const input = {
|
|
1053
|
-
file: relative,
|
|
1054
|
-
text,
|
|
1055
|
-
facts: policy.extractor.factsOf(relative, text),
|
|
1056
|
-
resolver: policy.resolver,
|
|
1057
|
-
fileSystem: policy.fileSystem,
|
|
1058
|
-
syntax: policy.syntax.parse(relative, text),
|
|
1059
|
-
functions: campaignsOf(policy).functions,
|
|
1060
|
-
reports: campaignsOf(policy).reports,
|
|
1061
|
-
};
|
|
1062
|
-
const evaluation = evaluations.find((one) => one.rule.id === rule.id);
|
|
1063
|
-
return [
|
|
1064
|
-
...(evaluation === undefined ? [] : explainCampaignLines(policy, evaluation, relative)),
|
|
1065
|
-
...rule.objectives.flatMap((objective) => {
|
|
1066
|
-
const measure = objective.measure;
|
|
1067
|
-
if (measure !== null) {
|
|
1068
|
-
// A scalar: what this file adds to its sector's number.
|
|
1069
|
-
if (measure.kind === "command") return [];
|
|
1070
|
-
const parts = measureFile(measure, input);
|
|
1071
|
-
const here =
|
|
1072
|
-
measure.kind === "ratio"
|
|
1073
|
-
? `${String(parts.of)} of ${String(parts.per)} here`
|
|
1074
|
-
: `${String(valueOfParts(measure, parts))} here`;
|
|
1075
|
-
return [
|
|
1076
|
-
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (measure, ${objective.direction}; ${here})`,
|
|
1077
|
-
];
|
|
1078
|
-
}
|
|
1079
|
-
const table = explainObjective(objective, input);
|
|
1080
|
-
if (table.length === 0) return [];
|
|
1081
|
-
const fired = table.length > 0 && table.every((line) => line.answer);
|
|
1082
|
-
return [
|
|
1083
|
-
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (${objective.holdout}; ${fired ? "fires here" : "no hit"})`,
|
|
1084
|
-
...table.map(
|
|
1085
|
-
(line) =>
|
|
1086
|
-
` ${line.answer ? "✓" : "✗"} ${line.term}${line.count === undefined ? "" : ` (${String(line.count)})`}`,
|
|
1087
|
-
),
|
|
1088
|
-
];
|
|
1089
|
-
}),
|
|
1090
|
-
];
|
|
1091
|
-
});
|
|
997
|
+
// The campaigns selecting the file, evaluated over their files only
|
|
998
|
+
// when one does: a sector's phase is derived from all of them.
|
|
999
|
+
const campaignLines = policy.campaigns.explainLines(
|
|
1000
|
+
policy,
|
|
1001
|
+
relative,
|
|
1002
|
+
() => collectFindings(policy, roots).campaigns,
|
|
1003
|
+
);
|
|
1092
1004
|
|
|
1093
1005
|
yield* report([
|
|
1094
1006
|
relative,
|
|
@@ -1337,382 +1249,31 @@ export const migrate = (
|
|
|
1337
1249
|
]);
|
|
1338
1250
|
});
|
|
1339
1251
|
|
|
1340
|
-
// The campaign commands
|
|
1341
|
-
//
|
|
1342
|
-
// `
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
const flagOf = (argv: ReadonlyArray<string>, flag: string): string | undefined => {
|
|
1348
|
-
const at = argv.indexOf(flag);
|
|
1349
|
-
const value = at === -1 ? undefined : argv[at + 1];
|
|
1350
|
-
return value === undefined || value.startsWith("--") ? undefined : value;
|
|
1351
|
-
};
|
|
1352
|
-
|
|
1353
|
-
const CAMPAIGN_SUBCOMMANDS = ["status", "attest", "note", "history", "clear", "concede"] as const;
|
|
1354
|
-
const OBJECTIVE_SUBCOMMANDS = ["clear", "concede"] as const;
|
|
1355
|
-
const VALUE_FLAGS = [
|
|
1356
|
-
"--reason",
|
|
1357
|
-
"--by",
|
|
1358
|
-
"--holdouts",
|
|
1359
|
-
"--entries",
|
|
1360
|
-
"--sector",
|
|
1361
|
-
"--campaign",
|
|
1362
|
-
"--evidence",
|
|
1363
|
-
"--base",
|
|
1364
|
-
"--since",
|
|
1365
|
-
"--hotfix",
|
|
1366
|
-
] as const;
|
|
1367
|
-
|
|
1368
|
-
// The verbs the family shipped with, refused by name: each has a new name
|
|
1369
|
-
// or no place.
|
|
1370
|
-
const RETIRED: Readonly<Record<string, string>> = {
|
|
1371
|
-
init: "`campaigns init` is gone: `objectives clear <campaign>` writes a first ledger, recording each sector's initial.",
|
|
1372
|
-
prune: "`campaigns prune` is now `objectives clear`.",
|
|
1373
|
-
allow: "`campaigns allow` is now `objectives concede`.",
|
|
1374
|
-
};
|
|
1375
|
-
|
|
1376
|
-
// The positionals after the subcommand, with the value flags and their
|
|
1377
|
-
// values stepped over: whatever is left names the roots to walk, as it
|
|
1378
|
-
// does for every other command.
|
|
1379
|
-
const positionalsOf = (argv: ReadonlyArray<string>): ReadonlyArray<string> => {
|
|
1380
|
-
const positional: Array<string> = [];
|
|
1381
|
-
for (let at = 0; at < argv.length; at += 1) {
|
|
1382
|
-
const one = argv[at] ?? "";
|
|
1383
|
-
if ((VALUE_FLAGS as ReadonlyArray<string>).includes(one)) {
|
|
1384
|
-
at += 1;
|
|
1385
|
-
continue;
|
|
1386
|
-
}
|
|
1387
|
-
if (!one.startsWith("--")) positional.push(one);
|
|
1388
|
-
}
|
|
1389
|
-
return positional;
|
|
1390
|
-
};
|
|
1391
|
-
|
|
1392
|
-
// `campaigns [status | attest <sector> <phase> | note <sector> "<text>" |
|
|
1393
|
-
// history [<campaign>]] [roots…]`: the subcommand and its arguments come
|
|
1394
|
-
// first.
|
|
1395
|
-
export const campaignArgsOf = (
|
|
1396
|
-
argv: ReadonlyArray<string>,
|
|
1397
|
-
ids: ReadonlyArray<string>,
|
|
1398
|
-
): {
|
|
1399
|
-
readonly subcommand: string | undefined;
|
|
1400
|
-
readonly args: ReadonlyArray<string>;
|
|
1401
|
-
readonly roots: ReadonlyArray<string>;
|
|
1402
|
-
} => {
|
|
1403
|
-
const positional = positionalsOf(argv);
|
|
1404
|
-
const [first, ...rest] = positional;
|
|
1405
|
-
if (first === undefined) return { subcommand: undefined, args: [], roots: [] };
|
|
1406
|
-
if (first in RETIRED) return { subcommand: first, args: [], roots: rest };
|
|
1407
|
-
if (!(CAMPAIGN_SUBCOMMANDS as ReadonlyArray<string>).includes(first)) {
|
|
1408
|
-
return { subcommand: undefined, args: [], roots: positional };
|
|
1409
|
-
}
|
|
1410
|
-
switch (first) {
|
|
1411
|
-
case "attest":
|
|
1412
|
-
return { subcommand: first, args: rest.slice(0, 2), roots: rest.slice(2) };
|
|
1413
|
-
case "note":
|
|
1414
|
-
return { subcommand: first, args: rest.slice(0, 2), roots: rest.slice(2) };
|
|
1415
|
-
case "history": {
|
|
1416
|
-
const [second] = rest;
|
|
1417
|
-
return second !== undefined && ids.includes(second)
|
|
1418
|
-
? { subcommand: first, args: [second], roots: rest.slice(1) }
|
|
1419
|
-
: { subcommand: first, args: [], roots: rest };
|
|
1420
|
-
}
|
|
1421
|
-
case "clear":
|
|
1422
|
-
case "concede": {
|
|
1423
|
-
const [second] = rest;
|
|
1424
|
-
return second !== undefined && ids.includes(second.split("/")[0] ?? "")
|
|
1425
|
-
? { subcommand: first, args: [second], roots: rest.slice(1) }
|
|
1426
|
-
: { subcommand: first, args: [], roots: rest };
|
|
1427
|
-
}
|
|
1428
|
-
default:
|
|
1429
|
-
return { subcommand: first, args: [], roots: rest };
|
|
1430
|
-
}
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
// `objectives [clear [<campaign>[/<objective>]] | concede <campaign>[/<objective>]
|
|
1434
|
-
// --reason <text>] [roots…]`.
|
|
1435
|
-
export const objectiveArgsOf = (
|
|
1436
|
-
argv: ReadonlyArray<string>,
|
|
1437
|
-
ids: ReadonlyArray<string>,
|
|
1438
|
-
): {
|
|
1439
|
-
readonly subcommand: string | undefined;
|
|
1440
|
-
readonly target: { campaign: string; objective: string | null } | null;
|
|
1441
|
-
readonly roots: ReadonlyArray<string>;
|
|
1442
|
-
} => {
|
|
1443
|
-
const positional = positionalsOf(argv);
|
|
1444
|
-
const [first, second, ...rest] = positional;
|
|
1445
|
-
if (first === undefined || !(OBJECTIVE_SUBCOMMANDS as ReadonlyArray<string>).includes(first)) {
|
|
1446
|
-
return { subcommand: first, target: null, roots: positional.slice(1) };
|
|
1447
|
-
}
|
|
1448
|
-
const [campaign = "", objective] = (second ?? "").split("/");
|
|
1449
|
-
const named = second !== undefined && ids.includes(campaign);
|
|
1450
|
-
return {
|
|
1451
|
-
subcommand: first,
|
|
1452
|
-
target: named ? { campaign, objective: objective ?? null } : null,
|
|
1453
|
-
roots: named ? rest : second === undefined ? [] : [second, ...rest],
|
|
1454
|
-
};
|
|
1455
|
-
};
|
|
1456
|
-
|
|
1457
|
-
// The one campaign, when there is one, else the one `--campaign` names.
|
|
1458
|
-
const campaignFor = (
|
|
1252
|
+
// The campaign commands, answered by the family the host was composed with:
|
|
1253
|
+
// `campaigns` alone is the status table, `status --changed` the nudge,
|
|
1254
|
+
// `attest` and `note` write a sector's record, `history` replays the ledgers;
|
|
1255
|
+
// `objectives clear` and `objectives concede` write the ledgers. Without
|
|
1256
|
+
// `@goodbones/campaigns` installed, each says what to install.
|
|
1257
|
+
const verbContext = (
|
|
1459
1258
|
policy: LoadedPolicy,
|
|
1259
|
+
defaultRoots: ReadonlyArray<string>,
|
|
1460
1260
|
argv: ReadonlyArray<string>,
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
if (campaignsOf(policy).campaignRules.length === 1 && only !== undefined)
|
|
1471
|
-
return Result.succeed(only);
|
|
1472
|
-
return Result.fail(
|
|
1473
|
-
`this policy declares ${String(campaignsOf(policy).campaignRules.length)} campaigns; say which with --campaign <id>.`,
|
|
1474
|
-
);
|
|
1475
|
-
};
|
|
1476
|
-
|
|
1477
|
-
const objectiveFor = (
|
|
1478
|
-
rule: CampaignEvaluation["rule"],
|
|
1479
|
-
objective: string | null,
|
|
1480
|
-
): Result.Result<string, string> => {
|
|
1481
|
-
if (objective !== null) {
|
|
1482
|
-
return rule.objectives.some((one) => one.id === objective)
|
|
1483
|
-
? Result.succeed(objective)
|
|
1484
|
-
: Result.fail(`no objective of ${rule.id} is named "${objective}"`);
|
|
1485
|
-
}
|
|
1486
|
-
const [only] = rule.objectives;
|
|
1487
|
-
if (rule.objectives.length === 1 && only !== undefined) return Result.succeed(only.id);
|
|
1488
|
-
return Result.fail(
|
|
1489
|
-
`campaign ${rule.id} declares ${String(rule.objectives.length)} objectives; say which as ${rule.id}/<objective>.`,
|
|
1490
|
-
);
|
|
1491
|
-
};
|
|
1261
|
+
configFilename?: string,
|
|
1262
|
+
): VerbContext => ({
|
|
1263
|
+
policy,
|
|
1264
|
+
defaultRoots,
|
|
1265
|
+
argv,
|
|
1266
|
+
evaluate: (roots) => collectFindings(policy, roots).campaigns,
|
|
1267
|
+
reload: (repoRoot) => loadPolicyFromFile(repoRoot, configFilename),
|
|
1268
|
+
manifestPath: manifestPathOf(policy.repoRoot, configFilename),
|
|
1269
|
+
});
|
|
1492
1270
|
|
|
1493
1271
|
export const objectives = (
|
|
1494
1272
|
policy: LoadedPolicy,
|
|
1495
1273
|
defaultRoots: ReadonlyArray<string>,
|
|
1496
1274
|
argv: ReadonlyArray<string>,
|
|
1497
1275
|
): Effect.Effect<void, CliFailure> =>
|
|
1498
|
-
|
|
1499
|
-
const parsed = objectiveArgsOf(
|
|
1500
|
-
argv,
|
|
1501
|
-
campaignsOf(policy).campaignRules.map((rule) => rule.id),
|
|
1502
|
-
);
|
|
1503
|
-
const roots = parsed.roots.length > 0 ? parsed.roots : defaultRoots;
|
|
1504
|
-
if (campaignsOf(policy).campaignRules.length === 0) {
|
|
1505
|
-
return yield* report(["this policy declares no campaigns."]);
|
|
1506
|
-
}
|
|
1507
|
-
const evaluations = (): ReadonlyArray<CampaignEvaluation> =>
|
|
1508
|
-
collectFindings(policy, roots).campaigns;
|
|
1509
|
-
|
|
1510
|
-
switch (parsed.subcommand) {
|
|
1511
|
-
case "clear": {
|
|
1512
|
-
const targets =
|
|
1513
|
-
parsed.target === null
|
|
1514
|
-
? campaignsOf(policy).campaignRules
|
|
1515
|
-
: campaignsOf(policy).campaignRules.filter(
|
|
1516
|
-
(rule) => rule.id === parsed.target?.campaign,
|
|
1517
|
-
);
|
|
1518
|
-
const by = authorOf(flagOf(argv, "--by")) ?? "unknown";
|
|
1519
|
-
const all = evaluations();
|
|
1520
|
-
const lines: Array<string> = [];
|
|
1521
|
-
for (const rule of targets) {
|
|
1522
|
-
const evaluation = all.find((one) => one.rule.id === rule.id);
|
|
1523
|
-
if (evaluation === undefined) continue;
|
|
1524
|
-
const only = parsed.target?.objective ?? null;
|
|
1525
|
-
if (only !== null && !rule.objectives.some((one) => one.id === only)) {
|
|
1526
|
-
return yield* Effect.fail(fail(`no objective of ${rule.id} is named "${only}"`));
|
|
1527
|
-
}
|
|
1528
|
-
const outcomes = yield* Effect.try({
|
|
1529
|
-
try: () => clear(policy, evaluation, only, by),
|
|
1530
|
-
catch: (cause) => fail(String(cause)),
|
|
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) : [];
|
|
1535
|
-
for (const outcome of outcomes) {
|
|
1536
|
-
const parts = [
|
|
1537
|
-
...(outcome.entered.length > 0
|
|
1538
|
-
? [
|
|
1539
|
-
`${count(outcome.entered.length, "sector")} entered (${outcome.entered.join(", ")})`,
|
|
1540
|
-
]
|
|
1541
|
-
: []),
|
|
1542
|
-
...(outcome.cleared > 0 ? [`${count(outcome.cleared, "holdout")} cleared`] : []),
|
|
1543
|
-
...(outcome.rewritten > 0
|
|
1544
|
-
? [`${count(outcome.rewritten, "holdout")} rewritten`]
|
|
1545
|
-
: []),
|
|
1546
|
-
...(outcome.closed > 0 ? [`${count(outcome.closed, "holdout")} closed`] : []),
|
|
1547
|
-
...(outcome.rebaselined.length > 0
|
|
1548
|
-
? [
|
|
1549
|
-
`${count(outcome.rebaselined.length, "sector")} re-baselined (${outcome.rebaselined.join(", ")})`,
|
|
1550
|
-
]
|
|
1551
|
-
: []),
|
|
1552
|
-
];
|
|
1553
|
-
if (outcome.measure !== undefined) {
|
|
1554
|
-
const moved = [
|
|
1555
|
-
...(outcome.entered.length > 0
|
|
1556
|
-
? [
|
|
1557
|
-
`${count(outcome.entered.length, "sector")} entered (${outcome.entered.join(", ")})`,
|
|
1558
|
-
]
|
|
1559
|
-
: []),
|
|
1560
|
-
...(outcome.measure.improved.length > 0
|
|
1561
|
-
? [
|
|
1562
|
-
`${count(outcome.measure.improved.length, "sector")} improved (${outcome.measure.improved
|
|
1563
|
-
.map((one) => `${one.sector} ${String(one.from)} → ${String(one.to)}`)
|
|
1564
|
-
.join(", ")})`,
|
|
1565
|
-
]
|
|
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
|
-
: []),
|
|
1577
|
-
...(outcome.closed > 0 ? [`${count(outcome.closed, "sector")} closed`] : []),
|
|
1578
|
-
...(outcome.rebaselined.length > 0
|
|
1579
|
-
? [
|
|
1580
|
-
`${count(outcome.rebaselined.length, "sector")} re-baselined (${outcome.rebaselined.join(", ")})`,
|
|
1581
|
-
]
|
|
1582
|
-
: []),
|
|
1583
|
-
];
|
|
1584
|
-
lines.push(
|
|
1585
|
-
`${outcome.campaign}/${outcome.objective}: ${moved.length === 0 ? "nothing to clear" : moved.join(", ")}; held to ${String(outcome.measure.recorded)}.`,
|
|
1586
|
-
);
|
|
1587
|
-
continue;
|
|
1588
|
-
}
|
|
1589
|
-
lines.push(
|
|
1590
|
-
`${outcome.campaign}/${outcome.objective}: ${parts.length === 0 ? "nothing to clear" : parts.join(", ")}; ${count(outcome.left, "holdout")} left.`,
|
|
1591
|
-
);
|
|
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
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
return yield* report(lines);
|
|
1603
|
-
}
|
|
1604
|
-
case "concede": {
|
|
1605
|
-
if (parsed.target === null) {
|
|
1606
|
-
return yield* Effect.fail(
|
|
1607
|
-
fail(
|
|
1608
|
-
"objectives concede needs a campaign: `objectives concede <campaign>[/<objective>] --reason <text>`",
|
|
1609
|
-
),
|
|
1610
|
-
);
|
|
1611
|
-
}
|
|
1612
|
-
const rule = campaignsOf(policy).campaignRules.find(
|
|
1613
|
-
(one) => one.id === parsed.target?.campaign,
|
|
1614
|
-
);
|
|
1615
|
-
if (rule === undefined)
|
|
1616
|
-
return yield* Effect.fail(fail(`no campaign is named "${parsed.target.campaign}"`));
|
|
1617
|
-
const objective = objectiveFor(rule, parsed.target.objective);
|
|
1618
|
-
if (Result.isFailure(objective)) return yield* Effect.fail(fail(objective.failure));
|
|
1619
|
-
const reason = flagOf(argv, "--reason");
|
|
1620
|
-
if (reason === undefined) {
|
|
1621
|
-
return yield* Effect.fail(
|
|
1622
|
-
fail(
|
|
1623
|
-
"objectives concede needs --reason <text>: growth is recorded with why, or not at all.",
|
|
1624
|
-
),
|
|
1625
|
-
);
|
|
1626
|
-
}
|
|
1627
|
-
const by = authorOf(flagOf(argv, "--by"));
|
|
1628
|
-
if (by === null) {
|
|
1629
|
-
return yield* Effect.fail(
|
|
1630
|
-
fail("objectives concede needs an author: pass --by <email>, or set git's user.email."),
|
|
1631
|
-
);
|
|
1632
|
-
}
|
|
1633
|
-
const evaluation = evaluations().find((one) => one.rule.id === rule.id);
|
|
1634
|
-
if (evaluation === undefined)
|
|
1635
|
-
return yield* Effect.fail(fail(`campaign ${rule.id} was not evaluated`));
|
|
1636
|
-
// `--holdouts` concedes a subset and refuses the rest: a pull
|
|
1637
|
-
// request that legitimately adds one hit while another is an
|
|
1638
|
-
// accident.
|
|
1639
|
-
const chosen =
|
|
1640
|
-
(flagOf(argv, "--holdouts") ?? flagOf(argv, "--entries"))
|
|
1641
|
-
?.split(",")
|
|
1642
|
-
.map((one) => one.trim()) ?? null;
|
|
1643
|
-
const outcome = concede(
|
|
1644
|
-
policy,
|
|
1645
|
-
evaluation,
|
|
1646
|
-
objective.success,
|
|
1647
|
-
chosen,
|
|
1648
|
-
flagOf(argv, "--sector") ?? null,
|
|
1649
|
-
{
|
|
1650
|
-
at: policy.now,
|
|
1651
|
-
by,
|
|
1652
|
-
reason,
|
|
1653
|
-
},
|
|
1654
|
-
);
|
|
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
|
-
]);
|
|
1671
|
-
const scalar =
|
|
1672
|
-
rule.objectives.find((one) => one.id === objective.success)?.measure !== null;
|
|
1673
|
-
if (scalar) {
|
|
1674
|
-
if (outcome.success.conceded.length === 0) {
|
|
1675
|
-
return yield* report([
|
|
1676
|
-
`${rule.id}/${objective.success}: nothing to concede; no sector measures past its record.`,
|
|
1677
|
-
]);
|
|
1678
|
-
}
|
|
1679
|
-
return yield* report([
|
|
1680
|
-
`${rule.id}/${objective.success}: a rise conceded in ${count(outcome.success.conceded.length, "sector")}, recorded by ${by}.`,
|
|
1681
|
-
...outcome.success.conceded.map((one) => ` ${one.sector} · ${one.entry}`),
|
|
1682
|
-
...(outcome.success.left.length === 0
|
|
1683
|
-
? []
|
|
1684
|
-
: [
|
|
1685
|
-
"",
|
|
1686
|
-
`${count(outcome.success.left.length, "sector")} left past the record; check still fails on them.`,
|
|
1687
|
-
]),
|
|
1688
|
-
...sentBack,
|
|
1689
|
-
]);
|
|
1690
|
-
}
|
|
1691
|
-
if (outcome.success.conceded.length === 0) {
|
|
1692
|
-
return yield* report([
|
|
1693
|
-
`${rule.id}/${objective.success}: nothing to concede; every hit is in the ledger.`,
|
|
1694
|
-
]);
|
|
1695
|
-
}
|
|
1696
|
-
return yield* report([
|
|
1697
|
-
`${rule.id}/${objective.success}: ${count(outcome.success.conceded.length, "holdout")} conceded, recorded by ${by}.`,
|
|
1698
|
-
...outcome.success.conceded.map((one) => ` ${one.sector} · ${one.entry}`),
|
|
1699
|
-
...(outcome.success.left.length === 0
|
|
1700
|
-
? []
|
|
1701
|
-
: [
|
|
1702
|
-
"",
|
|
1703
|
-
`${count(outcome.success.left.length, "hit")} left unrecorded; check still fails on them.`,
|
|
1704
|
-
]),
|
|
1705
|
-
...sentBack,
|
|
1706
|
-
]);
|
|
1707
|
-
}
|
|
1708
|
-
default:
|
|
1709
|
-
return yield* Effect.fail(
|
|
1710
|
-
fail(
|
|
1711
|
-
`unknown objectives subcommand "${parsed.subcommand ?? ""}". Try: objectives clear [<campaign>[/<objective>]] | objectives concede <campaign>[/<objective>] --reason <text> [--by <email>] [--sector <name>] [--holdouts a,b]`,
|
|
1712
|
-
),
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
|
-
});
|
|
1276
|
+
policy.campaigns.objectives(verbContext(policy, defaultRoots, argv));
|
|
1716
1277
|
|
|
1717
1278
|
export const campaigns = (
|
|
1718
1279
|
policy: LoadedPolicy,
|
|
@@ -1720,233 +1281,7 @@ export const campaigns = (
|
|
|
1720
1281
|
argv: ReadonlyArray<string>,
|
|
1721
1282
|
configFilename?: string,
|
|
1722
1283
|
): Effect.Effect<void, CliFailure> =>
|
|
1723
|
-
|
|
1724
|
-
const parsed = campaignArgsOf(
|
|
1725
|
-
argv,
|
|
1726
|
-
campaignsOf(policy).campaignRules.map((rule) => rule.id),
|
|
1727
|
-
);
|
|
1728
|
-
const roots = parsed.roots.length > 0 ? parsed.roots : defaultRoots;
|
|
1729
|
-
if (campaignsOf(policy).campaignRules.length === 0) {
|
|
1730
|
-
return yield* report(["this policy declares no campaigns."]);
|
|
1731
|
-
}
|
|
1732
|
-
const json = argv.includes("--json");
|
|
1733
|
-
const retired = parsed.subcommand === undefined ? undefined : RETIRED[parsed.subcommand];
|
|
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
|
-
}
|
|
1770
|
-
|
|
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) {
|
|
1776
|
-
case undefined: {
|
|
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
|
-
}
|
|
1785
|
-
return yield* report([
|
|
1786
|
-
`${count(snapshot.campaigns.length, "campaign")} under ${roots.join(", ")}`,
|
|
1787
|
-
"",
|
|
1788
|
-
...renderCampaignRows(snapshot.campaigns),
|
|
1789
|
-
"",
|
|
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",
|
|
1792
|
-
" architecture objectives clear [<campaign>[/<objective>]] # reconcile the ledgers with the code",
|
|
1793
|
-
' architecture objectives concede <campaign>[/<objective>] --reason "<why>" # record why a count may rise',
|
|
1794
|
-
' architecture campaigns attest <sector> <phase> --reason "<why>" [--evidence <url>]',
|
|
1795
|
-
' architecture campaigns note <sector> "<text>"',
|
|
1796
|
-
" architecture campaigns history [<campaign>] [--since <ref>]",
|
|
1797
|
-
]);
|
|
1798
|
-
}
|
|
1799
|
-
case "status": {
|
|
1800
|
-
const base = flagOf(argv, "--base") ?? null;
|
|
1801
|
-
const diff = yield* Effect.try({
|
|
1802
|
-
try: () => readDiff(policy.repoRoot, base),
|
|
1803
|
-
catch: (cause) => fail(`could not read the diff: ${String(cause)}`),
|
|
1804
|
-
});
|
|
1805
|
-
const current = collectFindings(policy, roots).campaigns;
|
|
1806
|
-
const baseSide =
|
|
1807
|
-
base === null
|
|
1808
|
-
? null
|
|
1809
|
-
: yield* Effect.tryPromise({
|
|
1810
|
-
try: () =>
|
|
1811
|
-
baseSideAt(policy, base, roots, (repoRoot) =>
|
|
1812
|
-
loadPolicyFromFile(repoRoot, configFilename),
|
|
1813
|
-
),
|
|
1814
|
-
catch: (cause) =>
|
|
1815
|
-
fail(`could not evaluate the base tree at ${base}: ${String(cause)}`),
|
|
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
|
-
});
|
|
1830
|
-
const hotfix = flagOf(argv, "--hotfix") ?? null;
|
|
1831
|
-
const nudge = nudgeOf(
|
|
1832
|
-
policy,
|
|
1833
|
-
current,
|
|
1834
|
-
diff,
|
|
1835
|
-
baseSide,
|
|
1836
|
-
hotfix,
|
|
1837
|
-
hotfix === null ? null : authorOf(flagOf(argv, "--by")),
|
|
1838
|
-
headTree,
|
|
1839
|
-
);
|
|
1840
|
-
yield* report(json ? [JSON.stringify(nudge, null, 2)] : renderNudge(nudge, policy.now));
|
|
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
|
-
}
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
case "attest": {
|
|
1858
|
-
const [sector, phase] = parsed.args;
|
|
1859
|
-
if (sector === undefined || phase === undefined) {
|
|
1860
|
-
return yield* Effect.fail(
|
|
1861
|
-
fail(
|
|
1862
|
-
'campaigns attest needs a sector and a phase: `campaigns attest <sector> <phase> --reason "<why>"`',
|
|
1863
|
-
),
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
const reason = flagOf(argv, "--reason");
|
|
1867
|
-
if (reason === undefined)
|
|
1868
|
-
return yield* Effect.fail(fail("campaigns attest needs --reason <text>."));
|
|
1869
|
-
const by = authorOf(flagOf(argv, "--by"));
|
|
1870
|
-
if (by === null)
|
|
1871
|
-
return yield* Effect.fail(
|
|
1872
|
-
fail("campaigns attest needs an author: pass --by <email>, or set git's user.email."),
|
|
1873
|
-
);
|
|
1874
|
-
const rule = campaignFor(policy, argv);
|
|
1875
|
-
if (Result.isFailure(rule)) return yield* Effect.fail(fail(rule.failure));
|
|
1876
|
-
const evaluation = collectFindings(policy, roots).campaigns.find(
|
|
1877
|
-
(one) => one.rule.id === rule.success.id,
|
|
1878
|
-
);
|
|
1879
|
-
if (evaluation === undefined)
|
|
1880
|
-
return yield* Effect.fail(fail(`campaign ${rule.success.id} was not evaluated`));
|
|
1881
|
-
const written = attest(policy, evaluation, sector, phase, {
|
|
1882
|
-
reason,
|
|
1883
|
-
evidence: flagOf(argv, "--evidence"),
|
|
1884
|
-
by,
|
|
1885
|
-
});
|
|
1886
|
-
if (Result.isFailure(written)) return yield* Effect.fail(fail(written.failure));
|
|
1887
|
-
return yield* report([
|
|
1888
|
-
`${rule.success.id}: sector ${sector} attested at ${phase} by ${by}, in ${written.success}.`,
|
|
1889
|
-
"Run `objectives clear` to move it on.",
|
|
1890
|
-
]);
|
|
1891
|
-
}
|
|
1892
|
-
case "note": {
|
|
1893
|
-
const [sector, text] = parsed.args;
|
|
1894
|
-
if (sector === undefined || text === undefined) {
|
|
1895
|
-
return yield* Effect.fail(
|
|
1896
|
-
fail('campaigns note needs a sector and a text: `campaigns note <sector> "<text>"`'),
|
|
1897
|
-
);
|
|
1898
|
-
}
|
|
1899
|
-
const by = authorOf(flagOf(argv, "--by"));
|
|
1900
|
-
if (by === null)
|
|
1901
|
-
return yield* Effect.fail(
|
|
1902
|
-
fail("campaigns note needs an author: pass --by <email>, or set git's user.email."),
|
|
1903
|
-
);
|
|
1904
|
-
const rule = campaignFor(policy, argv);
|
|
1905
|
-
if (Result.isFailure(rule)) return yield* Effect.fail(fail(rule.failure));
|
|
1906
|
-
const evaluation = collectFindings(policy, roots).campaigns.find(
|
|
1907
|
-
(one) => one.rule.id === rule.success.id,
|
|
1908
|
-
);
|
|
1909
|
-
if (evaluation === undefined)
|
|
1910
|
-
return yield* Effect.fail(fail(`campaign ${rule.success.id} was not evaluated`));
|
|
1911
|
-
const written = note(policy, evaluation, sector, text, by);
|
|
1912
|
-
if (Result.isFailure(written)) return yield* Effect.fail(fail(written.failure));
|
|
1913
|
-
return yield* report([
|
|
1914
|
-
`${rule.success.id}: note left on ${sector}, in ${written.success}.`,
|
|
1915
|
-
]);
|
|
1916
|
-
}
|
|
1917
|
-
case "history": {
|
|
1918
|
-
const [named] = parsed.args;
|
|
1919
|
-
const targets =
|
|
1920
|
-
named === undefined
|
|
1921
|
-
? campaignsOf(policy).campaignRules
|
|
1922
|
-
: campaignsOf(policy).campaignRules.filter((rule) => rule.id === named);
|
|
1923
|
-
const manifestPath = path
|
|
1924
|
-
.relative(policy.repoRoot, manifestPathOf(policy.repoRoot, configFilename))
|
|
1925
|
-
.replaceAll(path.sep, "/");
|
|
1926
|
-
const lines: Array<string> = [];
|
|
1927
|
-
for (const rule of targets) {
|
|
1928
|
-
const rows = historyOf(policy, rule, flagOf(argv, "--since") ?? null, [manifestPath]);
|
|
1929
|
-
if (json) {
|
|
1930
|
-
lines.push(JSON.stringify({ campaign: rule.id, rows }, null, 2));
|
|
1931
|
-
continue;
|
|
1932
|
-
}
|
|
1933
|
-
if (lines.length > 0) lines.push("");
|
|
1934
|
-
for (const line of renderHistory(rule, rows)) lines.push(line);
|
|
1935
|
-
}
|
|
1936
|
-
return yield* report(lines);
|
|
1937
|
-
}
|
|
1938
|
-
case "clear":
|
|
1939
|
-
case "concede":
|
|
1940
|
-
// The ledger verbs answer under `objectives`; accepted here too.
|
|
1941
|
-
return yield* objectives(policy, defaultRoots, argv);
|
|
1942
|
-
default:
|
|
1943
|
-
return yield* Effect.fail(
|
|
1944
|
-
fail(
|
|
1945
|
-
`unknown campaigns subcommand "${parsed.subcommand}". Try: campaigns | campaigns status --changed | campaigns attest <sector> <phase> --reason <text> | campaigns note <sector> "<text>" | campaigns history [<campaign>]`,
|
|
1946
|
-
),
|
|
1947
|
-
);
|
|
1948
|
-
}
|
|
1949
|
-
});
|
|
1284
|
+
policy.campaigns.campaigns(verbContext(policy, defaultRoots, argv, configFilename));
|
|
1950
1285
|
|
|
1951
1286
|
// The commands that judge a campaign, and so ask its report source.
|
|
1952
1287
|
const READS_REPORTS: ReadonlySet<string> = new Set([
|
|
@@ -2005,12 +1340,7 @@ export const run = (
|
|
|
2005
1340
|
// for the campaign to report; a report that does not parse is refused.
|
|
2006
1341
|
if (READS_REPORTS.has(command)) {
|
|
2007
1342
|
yield* Effect.tryPromise({
|
|
2008
|
-
try: () =>
|
|
2009
|
-
Promise.all(
|
|
2010
|
-
reportSpecsOf(campaignsOf(policy).campaignRules).map((spec) =>
|
|
2011
|
-
campaignsOf(policy).reports.read?.(spec),
|
|
2012
|
-
),
|
|
2013
|
-
),
|
|
1343
|
+
try: () => policy.campaigns.readReports(policy),
|
|
2014
1344
|
catch: (cause) => fail(String(cause)),
|
|
2015
1345
|
});
|
|
2016
1346
|
}
|