@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/build/esm/run.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { attest, authorOf, baseSideAt, campaignFailuresOf, campaignReportsOf, campaignsOf, campaignsSelecting, clear, concede, evaluateCampaigns, explainCampaignLines, explainObjective, historyOf, hitsInWindow, ledgeredFilter, measureFile, note, nudgeOf, readDiff, renderCampaignReports, renderCampaignRows, renderHistory, renderNudge, renderSectorView, reportSpecsOf, sectorMovesOf, sectorViewOf, snapshotCampaignsOf, valueOfParts, widenedExtensions, } from "@goodbones/campaigns";
|
|
5
4
|
import { baselineOf, breaches, CONFORMANCE_MEASURES, countedFiles, coverageOf, cyclesIn, decodeBaseline, decodeManifest, EMPTY_BASELINE, evaluateGraph, evaluateMemberSite, evaluateResolvedEdge, evaluateSelectedBindings, evaluateStructure, evaluateSurface, expandAliases, exportRulesSelecting, findManifestFile, fingerprintOf, formatManifestYaml, formatMessage, fractionsOf, globsOf, globToRegExp, hasGraphRules, heightOf, listSourceFiles, makeBaselineFilter, MANIFEST_FILENAMES, MANIFEST_SCHEMA_ID, memberRulesSelecting, readManifestFile, requiredSiblingsOf, residueOf, rulesSelecting, serializeBaseline, slackOf, SNAPSHOT_VERSION, staleEntriesOf, surfaceRulesSelecting, vacancyOf, } from "@goodbones/core";
|
|
6
5
|
import * as Effect from "effect/Effect";
|
|
7
6
|
import * as Result from "effect/Result";
|
|
8
7
|
import { loadPolicyFromFile, manifestPathOf } from "./config-loader.js";
|
|
9
8
|
import { buildGraph } from "./graph.js";
|
|
10
9
|
import { infer } from "./infer.js";
|
|
10
|
+
import { count, fail, report } from "./output.js";
|
|
11
11
|
import { sourceFactsOf } from "./source-facts.js";
|
|
12
|
-
const fail = (message) => ({ _tag: "CliFailure", message });
|
|
13
12
|
export const collectFindings = (policy, roots, options = {}) => {
|
|
14
13
|
// A campaign may widen the walk past the packs' extensions; a file only a
|
|
15
14
|
// campaign asked for is seen by the campaigns and by no other family.
|
|
16
|
-
const walked = listSourceFiles(policy.repoRoot, roots, policy.languages, widenedExtensions(policy));
|
|
15
|
+
const walked = listSourceFiles(policy.repoRoot, roots, policy.languages, policy.campaigns.widenedExtensions(policy));
|
|
17
16
|
const known = new Set(policy.languages.flatMap((one) => one.extensions));
|
|
18
17
|
const files = walked.filter((file) => known.has(path.extname(file)));
|
|
19
18
|
const violations = [];
|
|
@@ -49,7 +48,7 @@ export const collectFindings = (policy, roots, options = {}) => {
|
|
|
49
48
|
violations.push(violation);
|
|
50
49
|
}
|
|
51
50
|
// The campaigns, over every walked file, through the same caches.
|
|
52
|
-
const campaigns =
|
|
51
|
+
const campaigns = policy.campaigns.evaluate(policy, roots, walked, { textOf, factsOf });
|
|
53
52
|
for (const file of files) {
|
|
54
53
|
for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
|
|
55
54
|
violations.push(violation);
|
|
@@ -119,10 +118,6 @@ const readBaseline = (policy) => {
|
|
|
119
118
|
return EMPTY_BASELINE;
|
|
120
119
|
}
|
|
121
120
|
};
|
|
122
|
-
const report = (lines) => Effect.sync(() => {
|
|
123
|
-
for (const line of lines)
|
|
124
|
-
process.stdout.write(`${line}\n`);
|
|
125
|
-
});
|
|
126
121
|
const describe = (violation) => ` ${violation.file}\n ${formatMessage(violation)}`;
|
|
127
122
|
const COVERAGE_FAMILIES = [
|
|
128
123
|
"imports",
|
|
@@ -170,7 +165,6 @@ const reportOf = (policy, roots, manifestPath, findings) => {
|
|
|
170
165
|
const baseline = readBaseline(policy);
|
|
171
166
|
const stale = staleEntriesOf(baseline, findings.violations);
|
|
172
167
|
const { isBaselined } = makeBaselineFilter(baseline);
|
|
173
|
-
const isLedgered = ledgeredFilter(policy, findings.campaigns);
|
|
174
168
|
const violations = [
|
|
175
169
|
...findings.violations.map((violation) => ({
|
|
176
170
|
...violation,
|
|
@@ -180,17 +174,17 @@ const reportOf = (policy, roots, manifestPath, findings) => {
|
|
|
180
174
|
})),
|
|
181
175
|
// The hits that count: those whose objective is in window for the
|
|
182
176
|
// sector they fall in.
|
|
183
|
-
...
|
|
177
|
+
...policy.campaigns.hitsOf(policy, findings.campaigns).map((hit) => ({
|
|
184
178
|
...hit.violation,
|
|
185
179
|
fingerprint: fingerprintOf(hit.violation),
|
|
186
180
|
baselined: false,
|
|
187
|
-
ledgered:
|
|
181
|
+
ledgered: hit.ledgered,
|
|
188
182
|
objective: hit.objective,
|
|
189
183
|
sector: hit.sector,
|
|
190
184
|
entry: hit.entry,
|
|
191
|
-
}))
|
|
185
|
+
})),
|
|
192
186
|
];
|
|
193
|
-
const campaigns =
|
|
187
|
+
const campaigns = policy.campaigns.reportsOf(policy, findings.campaigns);
|
|
194
188
|
// The floors. A policy states how much of the tree it reaches, per
|
|
195
189
|
// family; falling under is a policy that quietly stopped covering files.
|
|
196
190
|
// Both are held over the files the limits count: what `limits.outside`
|
|
@@ -239,7 +233,7 @@ const reportOf = (policy, roots, manifestPath, findings) => {
|
|
|
239
233
|
shortfalls.length === 0 &&
|
|
240
234
|
overruns.length === 0 &&
|
|
241
235
|
excesses.length === 0 &&
|
|
242
|
-
|
|
236
|
+
policy.campaigns.failuresOf(campaigns).length === 0,
|
|
243
237
|
manifest: {
|
|
244
238
|
path: path.relative(policy.repoRoot, manifestPath).replaceAll(path.sep, "/"),
|
|
245
239
|
sha256: sha256Of(manifestPath),
|
|
@@ -279,10 +273,10 @@ const excessesOf = (conformance) => CONFORMANCE_MEASURES.flatMap((measure) => {
|
|
|
279
273
|
? []
|
|
280
274
|
: [{ measure, count, ceiling }];
|
|
281
275
|
});
|
|
282
|
-
const failureOf = (report, shortfalls) => {
|
|
276
|
+
const failureOf = (policy, report, shortfalls) => {
|
|
283
277
|
if (report.stale.length > 0)
|
|
284
278
|
return fail("stale baseline entries");
|
|
285
|
-
const [campaignFailure] =
|
|
279
|
+
const [campaignFailure] = policy.campaigns.failuresOf(report.campaigns);
|
|
286
280
|
if (campaignFailure !== undefined)
|
|
287
281
|
return fail(campaignFailure);
|
|
288
282
|
if (shortfalls.length > 0)
|
|
@@ -309,7 +303,7 @@ const describeExcess = (one) => {
|
|
|
309
303
|
const [singular, plural] = MEASURE_NOUNS[one.measure];
|
|
310
304
|
return ` ${one.measure}: ${count(one.count, singular, plural)}, ceiling ${String(one.ceiling)}`;
|
|
311
305
|
};
|
|
312
|
-
const renderCampaigns = (report) =>
|
|
306
|
+
const renderCampaigns = (policy, report) => policy.campaigns.renderReports(report.campaigns, report.violations.flatMap((one) => one.kind === "campaign" &&
|
|
313
307
|
one.objective !== undefined &&
|
|
314
308
|
one.sector !== undefined &&
|
|
315
309
|
one.entry !== undefined
|
|
@@ -323,7 +317,7 @@ const renderCampaigns = (report) => renderCampaignReports(report.campaigns, repo
|
|
|
323
317
|
},
|
|
324
318
|
]
|
|
325
319
|
: []));
|
|
326
|
-
const renderText = (report) => {
|
|
320
|
+
const renderText = (policy, report) => {
|
|
327
321
|
const reportable = report.violations.filter((one) => one.kind !== "campaign" && !one.baselined);
|
|
328
322
|
const carried = report.violations.filter((one) => one.kind !== "campaign").length - reportable.length;
|
|
329
323
|
const shortfalls = shortfallsOf(report.coverage);
|
|
@@ -373,20 +367,19 @@ const renderText = (report) => {
|
|
|
373
367
|
"",
|
|
374
368
|
" architecture conformance # which files, nodes and allowances",
|
|
375
369
|
]),
|
|
376
|
-
...renderCampaigns(report),
|
|
370
|
+
...renderCampaigns(policy, report),
|
|
377
371
|
];
|
|
378
372
|
};
|
|
379
373
|
export const check = (policy, roots, options) => Effect.gen(function* () {
|
|
380
374
|
const report_ = checkReport(policy, roots, options.manifestPath);
|
|
381
375
|
// JSON is one object on stdout and nothing else there; the failure, when
|
|
382
376
|
// there is one, is a sentence on stderr and the exit code, as in text.
|
|
383
|
-
yield* report(options.format === "json" ? [JSON.stringify(report_, null, 2)] : renderText(report_));
|
|
384
|
-
const failure = failureOf(report_, shortfallsOf(report_.coverage));
|
|
377
|
+
yield* report(options.format === "json" ? [JSON.stringify(report_, null, 2)] : renderText(policy, report_));
|
|
378
|
+
const failure = failureOf(policy, report_, shortfallsOf(report_.coverage));
|
|
385
379
|
if (failure !== null)
|
|
386
380
|
return yield* Effect.fail(failure);
|
|
387
381
|
});
|
|
388
382
|
const percent = (fraction) => `${String(Math.floor(fraction * 100))}%`;
|
|
389
|
-
const count = (n, noun, plural = `${noun}s`) => `${String(n)} ${n === 1 ? noun : plural}`;
|
|
390
383
|
// The conformance snapshot: `check`'s report grown with what no family
|
|
391
384
|
// reaches, what the allowlists permit and nothing uses, the cycle count and
|
|
392
385
|
// the size of the debt — the whole distance between the tree and the
|
|
@@ -407,7 +400,7 @@ export const snapshotOf = (policy, roots, manifestPath) => {
|
|
|
407
400
|
const byHeight = heightOfViolation(left) - heightOfViolation(right);
|
|
408
401
|
return byHeight !== 0 ? byHeight : left.fingerprint.localeCompare(right.fingerprint);
|
|
409
402
|
});
|
|
410
|
-
const campaigns =
|
|
403
|
+
const campaigns = policy.campaigns.snapshotOf(policy, findings.campaigns);
|
|
411
404
|
return {
|
|
412
405
|
version: SNAPSHOT_VERSION,
|
|
413
406
|
manifest: report_.manifest,
|
|
@@ -441,7 +434,7 @@ const unreachedMark = (covered, total, ceiling) => {
|
|
|
441
434
|
return ` ${String(left)} unreached ≤ ${String(ceiling)} ✓${left < ceiling ? `, lower it to ${String(left)}` : ""}`;
|
|
442
435
|
};
|
|
443
436
|
const outsideNote = (outside) => outside === 0 ? "" : `, ${count(outside, "file")} outside the limits`;
|
|
444
|
-
const renderSnapshot = (snapshot) => {
|
|
437
|
+
const renderSnapshot = (policy, snapshot) => {
|
|
445
438
|
const reportable = snapshot.violations.filter((one) => !one.baselined && !one.ledgered);
|
|
446
439
|
const carried = snapshot.violations.length - reportable.length;
|
|
447
440
|
const row = (family) => {
|
|
@@ -512,7 +505,7 @@ const renderSnapshot = (snapshot) => {
|
|
|
512
505
|
: "") +
|
|
513
506
|
(snapshot.campaigns.some((one) => one.complete && one.ledgered)
|
|
514
507
|
? `, ${count(snapshot.campaigns.filter((one) => one.complete && one.ledgered).length, "complete", "complete")}`
|
|
515
|
-
: ""),
|
|
508
|
+
: ""), policy.campaigns.renderRows(snapshot.campaigns))),
|
|
516
509
|
"",
|
|
517
510
|
`cycles: ${String(snapshot.cycles)}`,
|
|
518
511
|
`baseline: ${count(snapshot.baseline.size, "entry", "entries")}`,
|
|
@@ -525,7 +518,9 @@ const renderSnapshot = (snapshot) => {
|
|
|
525
518
|
// `ok` in the document says what `check` would have done.
|
|
526
519
|
export const conformance = (policy, roots, options) => Effect.gen(function* () {
|
|
527
520
|
const snapshot = snapshotOf(policy, roots, options.manifestPath);
|
|
528
|
-
yield* report(options.format === "json"
|
|
521
|
+
yield* report(options.format === "json"
|
|
522
|
+
? [JSON.stringify(snapshot, null, 2)]
|
|
523
|
+
: renderSnapshot(policy, snapshot));
|
|
529
524
|
});
|
|
530
525
|
// How much of the tree the policy reaches. A probe proves a rule can fire;
|
|
531
526
|
// this is whether the files are there to fire on. Reported per family, with the
|
|
@@ -610,56 +605,9 @@ export const explain = (policy, file, roots = ["packages"]) => Effect.gen(functi
|
|
|
610
605
|
.map((rule) => `${named(rule)} (reach)`),
|
|
611
606
|
];
|
|
612
607
|
const section = (title, lines) => lines.length === 0 ? [] : ["", title, ...lines];
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
// over its files, since a sector's phase is derived from all of them —
|
|
617
|
-
// then each objective's truth table: one line per leaf term and what
|
|
618
|
-
// it answered here, so a detector that "should fire" and does not shows
|
|
619
|
-
// which term is not saying what its author thinks.
|
|
620
|
-
const selectedCampaigns = campaignsSelecting(campaignsOf(policy).campaignRules, relative);
|
|
621
|
-
const evaluations = selectedCampaigns.length === 0 ? [] : collectFindings(policy, roots).campaigns;
|
|
622
|
-
const campaignLines = selectedCampaigns.flatMap((rule) => {
|
|
623
|
-
const at = path.join(policy.repoRoot, relative);
|
|
624
|
-
const text = existsSync(at) ? readFileSync(at, "utf8") : "";
|
|
625
|
-
const input = {
|
|
626
|
-
file: relative,
|
|
627
|
-
text,
|
|
628
|
-
facts: policy.extractor.factsOf(relative, text),
|
|
629
|
-
resolver: policy.resolver,
|
|
630
|
-
fileSystem: policy.fileSystem,
|
|
631
|
-
syntax: policy.syntax.parse(relative, text),
|
|
632
|
-
functions: campaignsOf(policy).functions,
|
|
633
|
-
reports: campaignsOf(policy).reports,
|
|
634
|
-
};
|
|
635
|
-
const evaluation = evaluations.find((one) => one.rule.id === rule.id);
|
|
636
|
-
return [
|
|
637
|
-
...(evaluation === undefined ? [] : explainCampaignLines(policy, evaluation, relative)),
|
|
638
|
-
...rule.objectives.flatMap((objective) => {
|
|
639
|
-
const measure = objective.measure;
|
|
640
|
-
if (measure !== null) {
|
|
641
|
-
// A scalar: what this file adds to its sector's number.
|
|
642
|
-
if (measure.kind === "command")
|
|
643
|
-
return [];
|
|
644
|
-
const parts = measureFile(measure, input);
|
|
645
|
-
const here = measure.kind === "ratio"
|
|
646
|
-
? `${String(parts.of)} of ${String(parts.per)} here`
|
|
647
|
-
: `${String(valueOfParts(measure, parts))} here`;
|
|
648
|
-
return [
|
|
649
|
-
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (measure, ${objective.direction}; ${here})`,
|
|
650
|
-
];
|
|
651
|
-
}
|
|
652
|
-
const table = explainObjective(objective, input);
|
|
653
|
-
if (table.length === 0)
|
|
654
|
-
return [];
|
|
655
|
-
const fired = table.length > 0 && table.every((line) => line.answer);
|
|
656
|
-
return [
|
|
657
|
-
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (${objective.holdout}; ${fired ? "fires here" : "no hit"})`,
|
|
658
|
-
...table.map((line) => ` ${line.answer ? "✓" : "✗"} ${line.term}${line.count === undefined ? "" : ` (${String(line.count)})`}`),
|
|
659
|
-
];
|
|
660
|
-
}),
|
|
661
|
-
];
|
|
662
|
-
});
|
|
608
|
+
// The campaigns selecting the file, evaluated over their files only
|
|
609
|
+
// when one does: a sector's phase is derived from all of them.
|
|
610
|
+
const campaignLines = policy.campaigns.explainLines(policy, relative, () => collectFindings(policy, roots).campaigns);
|
|
663
611
|
yield* report([
|
|
664
612
|
relative,
|
|
665
613
|
"",
|
|
@@ -865,485 +813,21 @@ export const migrate = (repoRoot, configFilename) => Effect.gen(function* () {
|
|
|
865
813
|
`Then delete ${path.basename(from)}: a repository with two manifests is refused.`,
|
|
866
814
|
]);
|
|
867
815
|
});
|
|
868
|
-
// The campaign commands
|
|
869
|
-
//
|
|
870
|
-
// `
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const OBJECTIVE_SUBCOMMANDS = ["clear", "concede"];
|
|
881
|
-
const VALUE_FLAGS = [
|
|
882
|
-
"--reason",
|
|
883
|
-
"--by",
|
|
884
|
-
"--holdouts",
|
|
885
|
-
"--entries",
|
|
886
|
-
"--sector",
|
|
887
|
-
"--campaign",
|
|
888
|
-
"--evidence",
|
|
889
|
-
"--base",
|
|
890
|
-
"--since",
|
|
891
|
-
"--hotfix",
|
|
892
|
-
];
|
|
893
|
-
// The verbs the family shipped with, refused by name: each has a new name
|
|
894
|
-
// or no place.
|
|
895
|
-
const RETIRED = {
|
|
896
|
-
init: "`campaigns init` is gone: `objectives clear <campaign>` writes a first ledger, recording each sector's initial.",
|
|
897
|
-
prune: "`campaigns prune` is now `objectives clear`.",
|
|
898
|
-
allow: "`campaigns allow` is now `objectives concede`.",
|
|
899
|
-
};
|
|
900
|
-
// The positionals after the subcommand, with the value flags and their
|
|
901
|
-
// values stepped over: whatever is left names the roots to walk, as it
|
|
902
|
-
// does for every other command.
|
|
903
|
-
const positionalsOf = (argv) => {
|
|
904
|
-
const positional = [];
|
|
905
|
-
for (let at = 0; at < argv.length; at += 1) {
|
|
906
|
-
const one = argv[at] ?? "";
|
|
907
|
-
if (VALUE_FLAGS.includes(one)) {
|
|
908
|
-
at += 1;
|
|
909
|
-
continue;
|
|
910
|
-
}
|
|
911
|
-
if (!one.startsWith("--"))
|
|
912
|
-
positional.push(one);
|
|
913
|
-
}
|
|
914
|
-
return positional;
|
|
915
|
-
};
|
|
916
|
-
// `campaigns [status | attest <sector> <phase> | note <sector> "<text>" |
|
|
917
|
-
// history [<campaign>]] [roots…]`: the subcommand and its arguments come
|
|
918
|
-
// first.
|
|
919
|
-
export const campaignArgsOf = (argv, ids) => {
|
|
920
|
-
const positional = positionalsOf(argv);
|
|
921
|
-
const [first, ...rest] = positional;
|
|
922
|
-
if (first === undefined)
|
|
923
|
-
return { subcommand: undefined, args: [], roots: [] };
|
|
924
|
-
if (first in RETIRED)
|
|
925
|
-
return { subcommand: first, args: [], roots: rest };
|
|
926
|
-
if (!CAMPAIGN_SUBCOMMANDS.includes(first)) {
|
|
927
|
-
return { subcommand: undefined, args: [], roots: positional };
|
|
928
|
-
}
|
|
929
|
-
switch (first) {
|
|
930
|
-
case "attest":
|
|
931
|
-
return { subcommand: first, args: rest.slice(0, 2), roots: rest.slice(2) };
|
|
932
|
-
case "note":
|
|
933
|
-
return { subcommand: first, args: rest.slice(0, 2), roots: rest.slice(2) };
|
|
934
|
-
case "history": {
|
|
935
|
-
const [second] = rest;
|
|
936
|
-
return second !== undefined && ids.includes(second)
|
|
937
|
-
? { subcommand: first, args: [second], roots: rest.slice(1) }
|
|
938
|
-
: { subcommand: first, args: [], roots: rest };
|
|
939
|
-
}
|
|
940
|
-
case "clear":
|
|
941
|
-
case "concede": {
|
|
942
|
-
const [second] = rest;
|
|
943
|
-
return second !== undefined && ids.includes(second.split("/")[0] ?? "")
|
|
944
|
-
? { subcommand: first, args: [second], roots: rest.slice(1) }
|
|
945
|
-
: { subcommand: first, args: [], roots: rest };
|
|
946
|
-
}
|
|
947
|
-
default:
|
|
948
|
-
return { subcommand: first, args: [], roots: rest };
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
// `objectives [clear [<campaign>[/<objective>]] | concede <campaign>[/<objective>]
|
|
952
|
-
// --reason <text>] [roots…]`.
|
|
953
|
-
export const objectiveArgsOf = (argv, ids) => {
|
|
954
|
-
const positional = positionalsOf(argv);
|
|
955
|
-
const [first, second, ...rest] = positional;
|
|
956
|
-
if (first === undefined || !OBJECTIVE_SUBCOMMANDS.includes(first)) {
|
|
957
|
-
return { subcommand: first, target: null, roots: positional.slice(1) };
|
|
958
|
-
}
|
|
959
|
-
const [campaign = "", objective] = (second ?? "").split("/");
|
|
960
|
-
const named = second !== undefined && ids.includes(campaign);
|
|
961
|
-
return {
|
|
962
|
-
subcommand: first,
|
|
963
|
-
target: named ? { campaign, objective: objective ?? null } : null,
|
|
964
|
-
roots: named ? rest : second === undefined ? [] : [second, ...rest],
|
|
965
|
-
};
|
|
966
|
-
};
|
|
967
|
-
// The one campaign, when there is one, else the one `--campaign` names.
|
|
968
|
-
const campaignFor = (policy, argv) => {
|
|
969
|
-
const named = flagOf(argv, "--campaign");
|
|
970
|
-
if (named !== undefined) {
|
|
971
|
-
const found = campaignsOf(policy).campaignRules.find((rule) => rule.id === named);
|
|
972
|
-
return found === undefined
|
|
973
|
-
? Result.fail(`no campaign is named "${named}"`)
|
|
974
|
-
: Result.succeed(found);
|
|
975
|
-
}
|
|
976
|
-
const [only] = campaignsOf(policy).campaignRules;
|
|
977
|
-
if (campaignsOf(policy).campaignRules.length === 1 && only !== undefined)
|
|
978
|
-
return Result.succeed(only);
|
|
979
|
-
return Result.fail(`this policy declares ${String(campaignsOf(policy).campaignRules.length)} campaigns; say which with --campaign <id>.`);
|
|
980
|
-
};
|
|
981
|
-
const objectiveFor = (rule, objective) => {
|
|
982
|
-
if (objective !== null) {
|
|
983
|
-
return rule.objectives.some((one) => one.id === objective)
|
|
984
|
-
? Result.succeed(objective)
|
|
985
|
-
: Result.fail(`no objective of ${rule.id} is named "${objective}"`);
|
|
986
|
-
}
|
|
987
|
-
const [only] = rule.objectives;
|
|
988
|
-
if (rule.objectives.length === 1 && only !== undefined)
|
|
989
|
-
return Result.succeed(only.id);
|
|
990
|
-
return Result.fail(`campaign ${rule.id} declares ${String(rule.objectives.length)} objectives; say which as ${rule.id}/<objective>.`);
|
|
991
|
-
};
|
|
992
|
-
export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* () {
|
|
993
|
-
const parsed = objectiveArgsOf(argv, campaignsOf(policy).campaignRules.map((rule) => rule.id));
|
|
994
|
-
const roots = parsed.roots.length > 0 ? parsed.roots : defaultRoots;
|
|
995
|
-
if (campaignsOf(policy).campaignRules.length === 0) {
|
|
996
|
-
return yield* report(["this policy declares no campaigns."]);
|
|
997
|
-
}
|
|
998
|
-
const evaluations = () => collectFindings(policy, roots).campaigns;
|
|
999
|
-
switch (parsed.subcommand) {
|
|
1000
|
-
case "clear": {
|
|
1001
|
-
const targets = parsed.target === null
|
|
1002
|
-
? campaignsOf(policy).campaignRules
|
|
1003
|
-
: campaignsOf(policy).campaignRules.filter((rule) => rule.id === parsed.target?.campaign);
|
|
1004
|
-
const by = authorOf(flagOf(argv, "--by")) ?? "unknown";
|
|
1005
|
-
const all = evaluations();
|
|
1006
|
-
const lines = [];
|
|
1007
|
-
for (const rule of targets) {
|
|
1008
|
-
const evaluation = all.find((one) => one.rule.id === rule.id);
|
|
1009
|
-
if (evaluation === undefined)
|
|
1010
|
-
continue;
|
|
1011
|
-
const only = parsed.target?.objective ?? null;
|
|
1012
|
-
if (only !== null && !rule.objectives.some((one) => one.id === only)) {
|
|
1013
|
-
return yield* Effect.fail(fail(`no objective of ${rule.id} is named "${only}"`));
|
|
1014
|
-
}
|
|
1015
|
-
const outcomes = yield* Effect.try({
|
|
1016
|
-
try: () => clear(policy, evaluation, only, by),
|
|
1017
|
-
catch: (cause) => fail(String(cause)),
|
|
1018
|
-
});
|
|
1019
|
-
// Where each sector went, read off the ledgers as they stood: a
|
|
1020
|
-
// clear of one objective places no sector, so it says nothing.
|
|
1021
|
-
const moves = only === null ? sectorMovesOf(policy, evaluation) : [];
|
|
1022
|
-
for (const outcome of outcomes) {
|
|
1023
|
-
const parts = [
|
|
1024
|
-
...(outcome.entered.length > 0
|
|
1025
|
-
? [
|
|
1026
|
-
`${count(outcome.entered.length, "sector")} entered (${outcome.entered.join(", ")})`,
|
|
1027
|
-
]
|
|
1028
|
-
: []),
|
|
1029
|
-
...(outcome.cleared > 0 ? [`${count(outcome.cleared, "holdout")} cleared`] : []),
|
|
1030
|
-
...(outcome.rewritten > 0
|
|
1031
|
-
? [`${count(outcome.rewritten, "holdout")} rewritten`]
|
|
1032
|
-
: []),
|
|
1033
|
-
...(outcome.closed > 0 ? [`${count(outcome.closed, "holdout")} closed`] : []),
|
|
1034
|
-
...(outcome.rebaselined.length > 0
|
|
1035
|
-
? [
|
|
1036
|
-
`${count(outcome.rebaselined.length, "sector")} re-baselined (${outcome.rebaselined.join(", ")})`,
|
|
1037
|
-
]
|
|
1038
|
-
: []),
|
|
1039
|
-
];
|
|
1040
|
-
if (outcome.measure !== undefined) {
|
|
1041
|
-
const moved = [
|
|
1042
|
-
...(outcome.entered.length > 0
|
|
1043
|
-
? [
|
|
1044
|
-
`${count(outcome.entered.length, "sector")} entered (${outcome.entered.join(", ")})`,
|
|
1045
|
-
]
|
|
1046
|
-
: []),
|
|
1047
|
-
...(outcome.measure.improved.length > 0
|
|
1048
|
-
? [
|
|
1049
|
-
`${count(outcome.measure.improved.length, "sector")} improved (${outcome.measure.improved
|
|
1050
|
-
.map((one) => `${one.sector} ${String(one.from)} → ${String(one.to)}`)
|
|
1051
|
-
.join(", ")})`,
|
|
1052
|
-
]
|
|
1053
|
-
: []),
|
|
1054
|
-
...(outcome.measure.grown.length > 0
|
|
1055
|
-
? [
|
|
1056
|
-
`${count(outcome.measure.grown.length, "sector")} grown (${outcome.measure.grown
|
|
1057
|
-
.map((one) => `${one.sector} ${String(one.from)} → ${String(one.to)}, ${one.phase === null ? "measured, not held" : `as ${one.phase} expects`}`)
|
|
1058
|
-
.join(", ")})`,
|
|
1059
|
-
]
|
|
1060
|
-
: []),
|
|
1061
|
-
...(outcome.closed > 0 ? [`${count(outcome.closed, "sector")} closed`] : []),
|
|
1062
|
-
...(outcome.rebaselined.length > 0
|
|
1063
|
-
? [
|
|
1064
|
-
`${count(outcome.rebaselined.length, "sector")} re-baselined (${outcome.rebaselined.join(", ")})`,
|
|
1065
|
-
]
|
|
1066
|
-
: []),
|
|
1067
|
-
];
|
|
1068
|
-
lines.push(`${outcome.campaign}/${outcome.objective}: ${moved.length === 0 ? "nothing to clear" : moved.join(", ")}; held to ${String(outcome.measure.recorded)}.`);
|
|
1069
|
-
continue;
|
|
1070
|
-
}
|
|
1071
|
-
lines.push(`${outcome.campaign}/${outcome.objective}: ${parts.length === 0 ? "nothing to clear" : parts.join(", ")}; ${count(outcome.left, "holdout")} left.`);
|
|
1072
|
-
}
|
|
1073
|
-
for (const move of moves) {
|
|
1074
|
-
lines.push(`${move.campaign}: ${move.sector} ${move.back ? "went back" : "moved"} ${move.from ?? "done"} → ${move.to ?? "done"}` +
|
|
1075
|
-
(move.passed.length === 0
|
|
1076
|
-
? "."
|
|
1077
|
-
: `, passing ${move.passed.join(", ")} in the same clear: nothing there was ever counted for it.`));
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
return yield* report(lines);
|
|
1081
|
-
}
|
|
1082
|
-
case "concede": {
|
|
1083
|
-
if (parsed.target === null) {
|
|
1084
|
-
return yield* Effect.fail(fail("objectives concede needs a campaign: `objectives concede <campaign>[/<objective>] --reason <text>`"));
|
|
1085
|
-
}
|
|
1086
|
-
const rule = campaignsOf(policy).campaignRules.find((one) => one.id === parsed.target?.campaign);
|
|
1087
|
-
if (rule === undefined)
|
|
1088
|
-
return yield* Effect.fail(fail(`no campaign is named "${parsed.target.campaign}"`));
|
|
1089
|
-
const objective = objectiveFor(rule, parsed.target.objective);
|
|
1090
|
-
if (Result.isFailure(objective))
|
|
1091
|
-
return yield* Effect.fail(fail(objective.failure));
|
|
1092
|
-
const reason = flagOf(argv, "--reason");
|
|
1093
|
-
if (reason === undefined) {
|
|
1094
|
-
return yield* Effect.fail(fail("objectives concede needs --reason <text>: growth is recorded with why, or not at all."));
|
|
1095
|
-
}
|
|
1096
|
-
const by = authorOf(flagOf(argv, "--by"));
|
|
1097
|
-
if (by === null) {
|
|
1098
|
-
return yield* Effect.fail(fail("objectives concede needs an author: pass --by <email>, or set git's user.email."));
|
|
1099
|
-
}
|
|
1100
|
-
const evaluation = evaluations().find((one) => one.rule.id === rule.id);
|
|
1101
|
-
if (evaluation === undefined)
|
|
1102
|
-
return yield* Effect.fail(fail(`campaign ${rule.id} was not evaluated`));
|
|
1103
|
-
// `--holdouts` concedes a subset and refuses the rest: a pull
|
|
1104
|
-
// request that legitimately adds one hit while another is an
|
|
1105
|
-
// accident.
|
|
1106
|
-
const chosen = (flagOf(argv, "--holdouts") ?? flagOf(argv, "--entries"))
|
|
1107
|
-
?.split(",")
|
|
1108
|
-
.map((one) => one.trim()) ?? null;
|
|
1109
|
-
const outcome = concede(policy, evaluation, objective.success, chosen, flagOf(argv, "--sector") ?? null, {
|
|
1110
|
-
at: policy.now,
|
|
1111
|
-
by,
|
|
1112
|
-
reason,
|
|
1113
|
-
});
|
|
1114
|
-
if (Result.isFailure(outcome))
|
|
1115
|
-
return yield* Effect.fail(fail(outcome.failure));
|
|
1116
|
-
// A concession that sends a sector back a phase says so, and names
|
|
1117
|
-
// the attestations it revoked on the way.
|
|
1118
|
-
const sentBack = outcome.success.sentBack.flatMap((one) => [
|
|
1119
|
-
"",
|
|
1120
|
-
`this concession moves ${one.sector} back ${one.from ?? "done"} → ${one.to ?? "done"}.`,
|
|
1121
|
-
...(one.revoked.length === 0
|
|
1122
|
-
? []
|
|
1123
|
-
: [
|
|
1124
|
-
`It revokes ${one.sector}'s attestation of ${one.revoked.join(", ")}: what was attested no longer holds. Once it does again, attest it anew:`,
|
|
1125
|
-
...one.revoked.map((phase) => ` architecture campaigns attest ${one.sector} ${phase} --reason "…" --campaign ${rule.id}`),
|
|
1126
|
-
]),
|
|
1127
|
-
]);
|
|
1128
|
-
const scalar = rule.objectives.find((one) => one.id === objective.success)?.measure !== null;
|
|
1129
|
-
if (scalar) {
|
|
1130
|
-
if (outcome.success.conceded.length === 0) {
|
|
1131
|
-
return yield* report([
|
|
1132
|
-
`${rule.id}/${objective.success}: nothing to concede; no sector measures past its record.`,
|
|
1133
|
-
]);
|
|
1134
|
-
}
|
|
1135
|
-
return yield* report([
|
|
1136
|
-
`${rule.id}/${objective.success}: a rise conceded in ${count(outcome.success.conceded.length, "sector")}, recorded by ${by}.`,
|
|
1137
|
-
...outcome.success.conceded.map((one) => ` ${one.sector} · ${one.entry}`),
|
|
1138
|
-
...(outcome.success.left.length === 0
|
|
1139
|
-
? []
|
|
1140
|
-
: [
|
|
1141
|
-
"",
|
|
1142
|
-
`${count(outcome.success.left.length, "sector")} left past the record; check still fails on them.`,
|
|
1143
|
-
]),
|
|
1144
|
-
...sentBack,
|
|
1145
|
-
]);
|
|
1146
|
-
}
|
|
1147
|
-
if (outcome.success.conceded.length === 0) {
|
|
1148
|
-
return yield* report([
|
|
1149
|
-
`${rule.id}/${objective.success}: nothing to concede; every hit is in the ledger.`,
|
|
1150
|
-
]);
|
|
1151
|
-
}
|
|
1152
|
-
return yield* report([
|
|
1153
|
-
`${rule.id}/${objective.success}: ${count(outcome.success.conceded.length, "holdout")} conceded, recorded by ${by}.`,
|
|
1154
|
-
...outcome.success.conceded.map((one) => ` ${one.sector} · ${one.entry}`),
|
|
1155
|
-
...(outcome.success.left.length === 0
|
|
1156
|
-
? []
|
|
1157
|
-
: [
|
|
1158
|
-
"",
|
|
1159
|
-
`${count(outcome.success.left.length, "hit")} left unrecorded; check still fails on them.`,
|
|
1160
|
-
]),
|
|
1161
|
-
...sentBack,
|
|
1162
|
-
]);
|
|
1163
|
-
}
|
|
1164
|
-
default:
|
|
1165
|
-
return yield* Effect.fail(fail(`unknown objectives subcommand "${parsed.subcommand ?? ""}". Try: objectives clear [<campaign>[/<objective>]] | objectives concede <campaign>[/<objective>] --reason <text> [--by <email>] [--sector <name>] [--holdouts a,b]`));
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
export const campaigns = (policy, defaultRoots, argv, configFilename) => Effect.gen(function* () {
|
|
1169
|
-
const parsed = campaignArgsOf(argv, campaignsOf(policy).campaignRules.map((rule) => rule.id));
|
|
1170
|
-
const roots = parsed.roots.length > 0 ? parsed.roots : defaultRoots;
|
|
1171
|
-
if (campaignsOf(policy).campaignRules.length === 0) {
|
|
1172
|
-
return yield* report(["this policy declares no campaigns."]);
|
|
1173
|
-
}
|
|
1174
|
-
const json = argv.includes("--json");
|
|
1175
|
-
const retired = parsed.subcommand === undefined ? undefined : RETIRED[parsed.subcommand];
|
|
1176
|
-
if (retired !== undefined)
|
|
1177
|
-
return yield* Effect.fail(fail(retired));
|
|
1178
|
-
// A positional that is no path is refused, not walked: most often it is
|
|
1179
|
-
// a sector's name, and `--sector` is what asks about one.
|
|
1180
|
-
for (const root of parsed.roots) {
|
|
1181
|
-
if (existsSync(path.resolve(policy.repoRoot, root)))
|
|
1182
|
-
continue;
|
|
1183
|
-
const named = collectFindings(policy, defaultRoots).campaigns.some((one) => one.sectors.has(root));
|
|
1184
|
-
return yield* Effect.fail(fail(`campaigns: ${root} is not a path in the repository, so there is nothing to walk there.` +
|
|
1185
|
-
(named ? ` It names a sector: \`campaigns status --sector ${root}\`.` : "")));
|
|
1186
|
-
}
|
|
1187
|
-
const sector = flagOf(argv, "--sector");
|
|
1188
|
-
if (parsed.subcommand === "status" && sector !== undefined) {
|
|
1189
|
-
const only = argv.includes("--campaign") ? campaignFor(policy, argv) : null;
|
|
1190
|
-
if (only !== null && Result.isFailure(only))
|
|
1191
|
-
return yield* Effect.fail(fail(only.failure));
|
|
1192
|
-
const views = collectFindings(policy, roots)
|
|
1193
|
-
.campaigns.filter((one) => only === null || one.rule.id === only.success.id)
|
|
1194
|
-
.flatMap((one) => {
|
|
1195
|
-
const view = sectorViewOf(policy, one, sector);
|
|
1196
|
-
return view === null ? [] : [view];
|
|
1197
|
-
});
|
|
1198
|
-
if (views.length === 0) {
|
|
1199
|
-
return yield* Effect.fail(fail(`campaigns: no campaign under ${roots.join(", ")} has a sector named ${sector}.`));
|
|
1200
|
-
}
|
|
1201
|
-
return yield* report(json
|
|
1202
|
-
? [JSON.stringify({ version: 1, sectors: views }, null, 2)]
|
|
1203
|
-
: views.flatMap((view, i) => [...(i === 0 ? [] : [""]), ...renderSectorView(view)]));
|
|
1204
|
-
}
|
|
1205
|
-
// `status` without `--changed` is the overview, in text or JSON: the
|
|
1206
|
-
// nudge alone is scoped to a diff.
|
|
1207
|
-
const subcommand = parsed.subcommand === "status" && !argv.includes("--changed") ? undefined : parsed.subcommand;
|
|
1208
|
-
switch (subcommand) {
|
|
1209
|
-
case undefined: {
|
|
1210
|
-
const snapshot = snapshotOf(policy, roots, manifestPathOf(policy.repoRoot, configFilename));
|
|
1211
|
-
if (json) {
|
|
1212
|
-
// Each campaign as the conformance snapshot carries it: its
|
|
1213
|
-
// burn-down, its ladder, and a row per sector.
|
|
1214
|
-
return yield* report([
|
|
1215
|
-
JSON.stringify({ version: 1, campaigns: snapshot.campaigns }, null, 2),
|
|
1216
|
-
]);
|
|
1217
|
-
}
|
|
1218
|
-
return yield* report([
|
|
1219
|
-
`${count(snapshot.campaigns.length, "campaign")} under ${roots.join(", ")}`,
|
|
1220
|
-
"",
|
|
1221
|
-
...renderCampaignRows(snapshot.campaigns),
|
|
1222
|
-
"",
|
|
1223
|
-
" architecture campaigns status --changed [--base <ref>] [--json] # what a diff touches, and what to do",
|
|
1224
|
-
" architecture campaigns status --sector <sector> [--json] # where one sector stands, and what holds it",
|
|
1225
|
-
" architecture objectives clear [<campaign>[/<objective>]] # reconcile the ledgers with the code",
|
|
1226
|
-
' architecture objectives concede <campaign>[/<objective>] --reason "<why>" # record why a count may rise',
|
|
1227
|
-
' architecture campaigns attest <sector> <phase> --reason "<why>" [--evidence <url>]',
|
|
1228
|
-
' architecture campaigns note <sector> "<text>"',
|
|
1229
|
-
" architecture campaigns history [<campaign>] [--since <ref>]",
|
|
1230
|
-
]);
|
|
1231
|
-
}
|
|
1232
|
-
case "status": {
|
|
1233
|
-
const base = flagOf(argv, "--base") ?? null;
|
|
1234
|
-
const diff = yield* Effect.try({
|
|
1235
|
-
try: () => readDiff(policy.repoRoot, base),
|
|
1236
|
-
catch: (cause) => fail(`could not read the diff: ${String(cause)}`),
|
|
1237
|
-
});
|
|
1238
|
-
const current = collectFindings(policy, roots).campaigns;
|
|
1239
|
-
const baseSide = base === null
|
|
1240
|
-
? null
|
|
1241
|
-
: yield* Effect.tryPromise({
|
|
1242
|
-
try: () => baseSideAt(policy, base, roots, (repoRoot) => loadPolicyFromFile(repoRoot, configFilename)),
|
|
1243
|
-
catch: (cause) => fail(`could not evaluate the base tree at ${base}: ${String(cause)}`),
|
|
1244
|
-
});
|
|
1245
|
-
// The ledger mode's "before" for work done ahead of the plan is the
|
|
1246
|
-
// HEAD tree, evaluated once per commit and cached: no ledger counts
|
|
1247
|
-
// the holdouts of a phase a sector has not entered.
|
|
1248
|
-
const headTree = base !== null || !current.some((one) => one.rule.phases.length > 0)
|
|
1249
|
-
? null
|
|
1250
|
-
: yield* Effect.tryPromise({
|
|
1251
|
-
try: () => baseSideAt(policy, "HEAD", roots, (repoRoot) => loadPolicyFromFile(repoRoot, configFilename)),
|
|
1252
|
-
catch: (cause) => fail(`could not evaluate the tree at HEAD: ${String(cause)}`),
|
|
1253
|
-
});
|
|
1254
|
-
const hotfix = flagOf(argv, "--hotfix") ?? null;
|
|
1255
|
-
const nudge = nudgeOf(policy, current, diff, baseSide, hotfix, hotfix === null ? null : authorOf(flagOf(argv, "--by")), headTree);
|
|
1256
|
-
yield* report(json ? [JSON.stringify(nudge, null, 2)] : renderNudge(nudge, policy.now));
|
|
1257
|
-
if (!nudge.ok) {
|
|
1258
|
-
const ahead = nudge.sectors.some((one) => one.verdict === "ahead");
|
|
1259
|
-
const back = nudge.sectors.some((one) => one.verdict !== "ok" && one.verdict !== "ahead" && one.verdict !== "hotfix");
|
|
1260
|
-
return yield* Effect.fail(fail([
|
|
1261
|
-
...(back ? ["the diff sends a sector back under its onTouch"] : []),
|
|
1262
|
-
...(ahead ? ["the diff works ahead of a sector's phase under its onAhead"] : []),
|
|
1263
|
-
].join("; ")));
|
|
1264
|
-
}
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
case "attest": {
|
|
1268
|
-
const [sector, phase] = parsed.args;
|
|
1269
|
-
if (sector === undefined || phase === undefined) {
|
|
1270
|
-
return yield* Effect.fail(fail('campaigns attest needs a sector and a phase: `campaigns attest <sector> <phase> --reason "<why>"`'));
|
|
1271
|
-
}
|
|
1272
|
-
const reason = flagOf(argv, "--reason");
|
|
1273
|
-
if (reason === undefined)
|
|
1274
|
-
return yield* Effect.fail(fail("campaigns attest needs --reason <text>."));
|
|
1275
|
-
const by = authorOf(flagOf(argv, "--by"));
|
|
1276
|
-
if (by === null)
|
|
1277
|
-
return yield* Effect.fail(fail("campaigns attest needs an author: pass --by <email>, or set git's user.email."));
|
|
1278
|
-
const rule = campaignFor(policy, argv);
|
|
1279
|
-
if (Result.isFailure(rule))
|
|
1280
|
-
return yield* Effect.fail(fail(rule.failure));
|
|
1281
|
-
const evaluation = collectFindings(policy, roots).campaigns.find((one) => one.rule.id === rule.success.id);
|
|
1282
|
-
if (evaluation === undefined)
|
|
1283
|
-
return yield* Effect.fail(fail(`campaign ${rule.success.id} was not evaluated`));
|
|
1284
|
-
const written = attest(policy, evaluation, sector, phase, {
|
|
1285
|
-
reason,
|
|
1286
|
-
evidence: flagOf(argv, "--evidence"),
|
|
1287
|
-
by,
|
|
1288
|
-
});
|
|
1289
|
-
if (Result.isFailure(written))
|
|
1290
|
-
return yield* Effect.fail(fail(written.failure));
|
|
1291
|
-
return yield* report([
|
|
1292
|
-
`${rule.success.id}: sector ${sector} attested at ${phase} by ${by}, in ${written.success}.`,
|
|
1293
|
-
"Run `objectives clear` to move it on.",
|
|
1294
|
-
]);
|
|
1295
|
-
}
|
|
1296
|
-
case "note": {
|
|
1297
|
-
const [sector, text] = parsed.args;
|
|
1298
|
-
if (sector === undefined || text === undefined) {
|
|
1299
|
-
return yield* Effect.fail(fail('campaigns note needs a sector and a text: `campaigns note <sector> "<text>"`'));
|
|
1300
|
-
}
|
|
1301
|
-
const by = authorOf(flagOf(argv, "--by"));
|
|
1302
|
-
if (by === null)
|
|
1303
|
-
return yield* Effect.fail(fail("campaigns note needs an author: pass --by <email>, or set git's user.email."));
|
|
1304
|
-
const rule = campaignFor(policy, argv);
|
|
1305
|
-
if (Result.isFailure(rule))
|
|
1306
|
-
return yield* Effect.fail(fail(rule.failure));
|
|
1307
|
-
const evaluation = collectFindings(policy, roots).campaigns.find((one) => one.rule.id === rule.success.id);
|
|
1308
|
-
if (evaluation === undefined)
|
|
1309
|
-
return yield* Effect.fail(fail(`campaign ${rule.success.id} was not evaluated`));
|
|
1310
|
-
const written = note(policy, evaluation, sector, text, by);
|
|
1311
|
-
if (Result.isFailure(written))
|
|
1312
|
-
return yield* Effect.fail(fail(written.failure));
|
|
1313
|
-
return yield* report([
|
|
1314
|
-
`${rule.success.id}: note left on ${sector}, in ${written.success}.`,
|
|
1315
|
-
]);
|
|
1316
|
-
}
|
|
1317
|
-
case "history": {
|
|
1318
|
-
const [named] = parsed.args;
|
|
1319
|
-
const targets = named === undefined
|
|
1320
|
-
? campaignsOf(policy).campaignRules
|
|
1321
|
-
: campaignsOf(policy).campaignRules.filter((rule) => rule.id === named);
|
|
1322
|
-
const manifestPath = path
|
|
1323
|
-
.relative(policy.repoRoot, manifestPathOf(policy.repoRoot, configFilename))
|
|
1324
|
-
.replaceAll(path.sep, "/");
|
|
1325
|
-
const lines = [];
|
|
1326
|
-
for (const rule of targets) {
|
|
1327
|
-
const rows = historyOf(policy, rule, flagOf(argv, "--since") ?? null, [manifestPath]);
|
|
1328
|
-
if (json) {
|
|
1329
|
-
lines.push(JSON.stringify({ campaign: rule.id, rows }, null, 2));
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
if (lines.length > 0)
|
|
1333
|
-
lines.push("");
|
|
1334
|
-
for (const line of renderHistory(rule, rows))
|
|
1335
|
-
lines.push(line);
|
|
1336
|
-
}
|
|
1337
|
-
return yield* report(lines);
|
|
1338
|
-
}
|
|
1339
|
-
case "clear":
|
|
1340
|
-
case "concede":
|
|
1341
|
-
// The ledger verbs answer under `objectives`; accepted here too.
|
|
1342
|
-
return yield* objectives(policy, defaultRoots, argv);
|
|
1343
|
-
default:
|
|
1344
|
-
return yield* Effect.fail(fail(`unknown campaigns subcommand "${parsed.subcommand}". Try: campaigns | campaigns status --changed | campaigns attest <sector> <phase> --reason <text> | campaigns note <sector> "<text>" | campaigns history [<campaign>]`));
|
|
1345
|
-
}
|
|
816
|
+
// The campaign commands, answered by the family the host was composed with:
|
|
817
|
+
// `campaigns` alone is the status table, `status --changed` the nudge,
|
|
818
|
+
// `attest` and `note` write a sector's record, `history` replays the ledgers;
|
|
819
|
+
// `objectives clear` and `objectives concede` write the ledgers. Without
|
|
820
|
+
// `@goodbones/campaigns` installed, each says what to install.
|
|
821
|
+
const verbContext = (policy, defaultRoots, argv, configFilename) => ({
|
|
822
|
+
policy,
|
|
823
|
+
defaultRoots,
|
|
824
|
+
argv,
|
|
825
|
+
evaluate: (roots) => collectFindings(policy, roots).campaigns,
|
|
826
|
+
reload: (repoRoot) => loadPolicyFromFile(repoRoot, configFilename),
|
|
827
|
+
manifestPath: manifestPathOf(policy.repoRoot, configFilename),
|
|
1346
828
|
});
|
|
829
|
+
export const objectives = (policy, defaultRoots, argv) => policy.campaigns.objectives(verbContext(policy, defaultRoots, argv));
|
|
830
|
+
export const campaigns = (policy, defaultRoots, argv, configFilename) => policy.campaigns.campaigns(verbContext(policy, defaultRoots, argv, configFilename));
|
|
1347
831
|
// The commands that judge a campaign, and so ask its report source.
|
|
1348
832
|
const READS_REPORTS = new Set([
|
|
1349
833
|
"check",
|
|
@@ -1393,7 +877,7 @@ configFilename) => Effect.gen(function* () {
|
|
|
1393
877
|
// for the campaign to report; a report that does not parse is refused.
|
|
1394
878
|
if (READS_REPORTS.has(command)) {
|
|
1395
879
|
yield* Effect.tryPromise({
|
|
1396
|
-
try: () =>
|
|
880
|
+
try: () => policy.campaigns.readReports(policy),
|
|
1397
881
|
catch: (cause) => fail(String(cause)),
|
|
1398
882
|
});
|
|
1399
883
|
}
|