@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/build/dts/run.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export type CoverageReport = Readonly<Record<CoverageFamily, {
|
|
|
35
35
|
readonly covered: number;
|
|
36
36
|
readonly total: number;
|
|
37
37
|
readonly floor?: number;
|
|
38
|
+
readonly ceiling?: number;
|
|
38
39
|
}>>;
|
|
39
40
|
export type ConformanceReport = Readonly<Record<ConformanceMeasure, {
|
|
40
41
|
readonly count: number;
|
|
@@ -43,6 +44,7 @@ export type ConformanceReport = Readonly<Record<ConformanceMeasure, {
|
|
|
43
44
|
export type CheckReport = {
|
|
44
45
|
readonly version: 1;
|
|
45
46
|
readonly files: number;
|
|
47
|
+
readonly outside: number;
|
|
46
48
|
readonly roots: ReadonlyArray<string>;
|
|
47
49
|
readonly ok: boolean;
|
|
48
50
|
readonly manifest: {
|
package/build/dts/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,kBAAkB,EAEvB,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"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"}
|
package/build/esm/run.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, reportSpecsOf, snapshotCampaignsOf, valueOfParts, widenedExtensions, } from "@goodbones/campaigns";
|
|
5
|
-
import { baselineOf, breaches, CONFORMANCE_MEASURES, coverageOf, cyclesIn, decodeBaseline, decodeManifest, EMPTY_BASELINE, evaluateGraph, evaluateMemberSite, evaluateResolvedEdge, evaluateSelectedBindings, evaluateStructure, evaluateSurface, exportRulesSelecting, findManifestFile, fingerprintOf, formatManifestYaml, formatMessage, fractionsOf, hasGraphRules, heightOf, listSourceFiles, makeBaselineFilter, MANIFEST_FILENAMES, MANIFEST_SCHEMA_ID, memberRulesSelecting, readManifestFile, requiredSiblingsOf, residueOf, rulesSelecting, serializeBaseline, slackOf, SNAPSHOT_VERSION, staleEntriesOf, surfaceRulesSelecting, vacancyOf, } from "@goodbones/core";
|
|
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
|
+
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
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Result from "effect/Result";
|
|
8
8
|
import { loadPolicyFromFile, manifestPathOf } from "./config-loader.js";
|
|
@@ -140,13 +140,18 @@ const sha256Of = (file) => {
|
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
export const checkReport = (policy, roots, manifestPath) => reportOf(policy, roots, manifestPath, collectFindings(policy, roots)).report;
|
|
143
|
+
// The patterns the policy's `limits.outside` names, alias-expanded, each a
|
|
144
|
+
// subtree.
|
|
145
|
+
const outsideOf = (policy) => globsOf(policy.config.limits?.outside ?? []).map((glob) => globToRegExp(expandAliases(glob, policy.config.aliases ?? {})));
|
|
143
146
|
const measuresOf = (policy, files, edges) => {
|
|
144
147
|
// Slack is measured over the walked files as well as the edges: an
|
|
145
148
|
// allowlist that selects no file is vacant, and its entries are reported as
|
|
146
149
|
// that rather than as lines nobody needs.
|
|
147
150
|
const { concentration, slack } = slackOf(policy.importRules, edges, files);
|
|
148
151
|
return {
|
|
149
|
-
|
|
152
|
+
// Residue is a count of files, and counts the ones the limits count. A
|
|
153
|
+
// node is live or vacant by every file under it, counted or not.
|
|
154
|
+
residue: residueOf(policy, countedFiles(files, outsideOf(policy))),
|
|
150
155
|
vacant: vacancyOf(policy.importRules, files),
|
|
151
156
|
slack,
|
|
152
157
|
concentration,
|
|
@@ -188,22 +193,29 @@ const reportOf = (policy, roots, manifestPath, findings) => {
|
|
|
188
193
|
const campaigns = campaignReportsOf(policy, findings.campaigns);
|
|
189
194
|
// The floors. A policy states how much of the tree it reaches, per
|
|
190
195
|
// family; falling under is a policy that quietly stopped covering files.
|
|
196
|
+
// Both are held over the files the limits count: what `limits.outside`
|
|
197
|
+
// names is judged by every rule and left out of these numbers.
|
|
191
198
|
const floors = policy.config.limits?.coverage ?? {};
|
|
199
|
+
const unreached = policy.config.limits?.unreached ?? {};
|
|
192
200
|
const files = listSourceFiles(policy.repoRoot, roots, policy.languages);
|
|
193
|
-
const
|
|
201
|
+
const counted = countedFiles(files, outsideOf(policy));
|
|
202
|
+
const found = coverageOf(policy, counted);
|
|
194
203
|
const covered = (family) => family === "structure" ? found.structure.enumerated : found[family].covered;
|
|
195
204
|
const coverage = Object.fromEntries(COVERAGE_FAMILIES.map((family) => {
|
|
196
205
|
const floor = floors[family];
|
|
206
|
+
const ceiling = unreached[family];
|
|
197
207
|
return [
|
|
198
208
|
family,
|
|
199
209
|
{
|
|
200
210
|
covered: covered(family),
|
|
201
211
|
total: found.files,
|
|
202
212
|
...(floor === undefined ? {} : { floor }),
|
|
213
|
+
...(ceiling === undefined ? {} : { ceiling }),
|
|
203
214
|
},
|
|
204
215
|
];
|
|
205
216
|
}));
|
|
206
217
|
const shortfalls = shortfallsOf(coverage);
|
|
218
|
+
const overruns = overrunsOf(coverage);
|
|
207
219
|
// The ceilings. What no family reaches, what no file is under and what
|
|
208
220
|
// nothing imports through are each a count the policy may hold itself
|
|
209
221
|
// to; rising over one is a manifest that quietly widened.
|
|
@@ -219,11 +231,13 @@ const reportOf = (policy, roots, manifestPath, findings) => {
|
|
|
219
231
|
const report = {
|
|
220
232
|
version: 1,
|
|
221
233
|
files: findings.files,
|
|
234
|
+
outside: files.length - counted.length,
|
|
222
235
|
roots,
|
|
223
236
|
ok: reportable === 0 &&
|
|
224
237
|
findings.unresolved.length === 0 &&
|
|
225
238
|
stale.length === 0 &&
|
|
226
239
|
shortfalls.length === 0 &&
|
|
240
|
+
overruns.length === 0 &&
|
|
227
241
|
excesses.length === 0 &&
|
|
228
242
|
campaignFailuresOf(campaigns).length === 0,
|
|
229
243
|
manifest: {
|
|
@@ -250,6 +264,14 @@ const shortfallsOf = (coverage) => COVERAGE_FAMILIES.flatMap((family) => {
|
|
|
250
264
|
? []
|
|
251
265
|
: [{ family, actual, floor }];
|
|
252
266
|
});
|
|
267
|
+
const overrunsOf = (coverage) => COVERAGE_FAMILIES.flatMap((family) => {
|
|
268
|
+
const { ceiling, covered, total } = coverage[family];
|
|
269
|
+
const unreached = total - covered;
|
|
270
|
+
return ceiling === undefined ||
|
|
271
|
+
!breaches({ direction: "down", limit: ceiling, tolerance: 0 }, unreached)
|
|
272
|
+
? []
|
|
273
|
+
: [{ family, unreached, ceiling }];
|
|
274
|
+
});
|
|
253
275
|
const excessesOf = (conformance) => CONFORMANCE_MEASURES.flatMap((measure) => {
|
|
254
276
|
const { ceiling, count } = conformance[measure];
|
|
255
277
|
return ceiling === undefined ||
|
|
@@ -265,6 +287,8 @@ const failureOf = (report, shortfalls) => {
|
|
|
265
287
|
return fail(campaignFailure);
|
|
266
288
|
if (shortfalls.length > 0)
|
|
267
289
|
return fail("coverage below floor");
|
|
290
|
+
if (overrunsOf(report.coverage).length > 0)
|
|
291
|
+
return fail("unreached above ceiling");
|
|
268
292
|
if (excessesOf(report.conformance).length > 0)
|
|
269
293
|
return fail("conformance above ceiling");
|
|
270
294
|
if (report.ok)
|
|
@@ -303,6 +327,7 @@ const renderText = (report) => {
|
|
|
303
327
|
const reportable = report.violations.filter((one) => one.kind !== "campaign" && !one.baselined);
|
|
304
328
|
const carried = report.violations.filter((one) => one.kind !== "campaign").length - reportable.length;
|
|
305
329
|
const shortfalls = shortfallsOf(report.coverage);
|
|
330
|
+
const overruns = overrunsOf(report.coverage);
|
|
306
331
|
const excesses = excessesOf(report.conformance);
|
|
307
332
|
return [
|
|
308
333
|
...reportable.map(describe),
|
|
@@ -330,6 +355,15 @@ const renderText = (report) => {
|
|
|
330
355
|
"",
|
|
331
356
|
" architecture coverage # which files no rule reaches",
|
|
332
357
|
]),
|
|
358
|
+
...(overruns.length === 0
|
|
359
|
+
? []
|
|
360
|
+
: [
|
|
361
|
+
"",
|
|
362
|
+
"more files are unreached than the ceiling the policy states for itself:",
|
|
363
|
+
...overruns.map((one) => ` ${one.family}: ${count(one.unreached, "file")} unreached, ceiling ${String(one.ceiling)}`),
|
|
364
|
+
"",
|
|
365
|
+
" architecture coverage # which files no rule reaches",
|
|
366
|
+
]),
|
|
333
367
|
...(excesses.length === 0
|
|
334
368
|
? []
|
|
335
369
|
: [
|
|
@@ -379,6 +413,7 @@ export const snapshotOf = (policy, roots, manifestPath) => {
|
|
|
379
413
|
manifest: report_.manifest,
|
|
380
414
|
roots: report_.roots,
|
|
381
415
|
files: report_.files,
|
|
416
|
+
outside: report_.outside,
|
|
382
417
|
ok: report_.ok,
|
|
383
418
|
coverage: report_.coverage,
|
|
384
419
|
conformance: report_.conformance,
|
|
@@ -395,18 +430,29 @@ export const snapshotOf = (policy, roots, manifestPath) => {
|
|
|
395
430
|
campaigns,
|
|
396
431
|
};
|
|
397
432
|
};
|
|
433
|
+
// The unreached ceiling beside a family's row, and the ratchet's nudge when
|
|
434
|
+
// the count has fallen under it: a ceiling is lowered by hand.
|
|
435
|
+
const unreachedMark = (covered, total, ceiling) => {
|
|
436
|
+
if (ceiling === undefined)
|
|
437
|
+
return "";
|
|
438
|
+
const left = total - covered;
|
|
439
|
+
if (left > ceiling)
|
|
440
|
+
return ` ${String(left)} unreached > ${String(ceiling)} ✗`;
|
|
441
|
+
return ` ${String(left)} unreached ≤ ${String(ceiling)} ✓${left < ceiling ? `, lower it to ${String(left)}` : ""}`;
|
|
442
|
+
};
|
|
443
|
+
const outsideNote = (outside) => outside === 0 ? "" : `, ${count(outside, "file")} outside the limits`;
|
|
398
444
|
const renderSnapshot = (snapshot) => {
|
|
399
445
|
const reportable = snapshot.violations.filter((one) => !one.baselined && !one.ledgered);
|
|
400
446
|
const carried = snapshot.violations.length - reportable.length;
|
|
401
447
|
const row = (family) => {
|
|
402
|
-
const { covered, floor, total } = snapshot.coverage[family];
|
|
448
|
+
const { ceiling, covered, floor, total } = snapshot.coverage[family];
|
|
403
449
|
const fraction = total === 0 ? 1 : covered / total;
|
|
404
450
|
const mark = floor === undefined
|
|
405
451
|
? ""
|
|
406
452
|
: fraction >= floor
|
|
407
453
|
? ` ≥ ${percent(floor)} ✓`
|
|
408
454
|
: ` < ${percent(floor)} ✗`;
|
|
409
|
-
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(total)} ${percent(fraction).padStart(4)}${mark}`;
|
|
455
|
+
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(total)} ${percent(fraction).padStart(4)}${mark}${unreachedMark(covered, total, ceiling)}`;
|
|
410
456
|
};
|
|
411
457
|
const section = (title, lines) => [
|
|
412
458
|
"",
|
|
@@ -428,7 +474,7 @@ const renderSnapshot = (snapshot) => {
|
|
|
428
474
|
return ` ≤ ${String(ceiling)} ✓${actual < ceiling ? `, lower it to ${String(actual)}` : ""}`;
|
|
429
475
|
};
|
|
430
476
|
return [
|
|
431
|
-
`${String(snapshot.files)} files under ${snapshot.roots.join(", ")}, against ${snapshot.manifest.path}`,
|
|
477
|
+
`${String(snapshot.files)} files under ${snapshot.roots.join(", ")}, against ${snapshot.manifest.path}${outsideNote(snapshot.outside)}`,
|
|
432
478
|
...section("coverage", COVERAGE_FAMILIES.map(row)),
|
|
433
479
|
...section(`residue: ${count(snapshot.residue.files.length, "file")} no family reaches` +
|
|
434
480
|
(snapshot.residue.folders.length === 0
|
|
@@ -485,10 +531,14 @@ export const conformance = (policy, roots, options) => Effect.gen(function* () {
|
|
|
485
531
|
// this is whether the files are there to fire on. Reported per family, with the
|
|
486
532
|
// adoption backlog — the tiers that said "not tightened yet" — beneath it.
|
|
487
533
|
export const coverage = (policy, roots) => Effect.gen(function* () {
|
|
488
|
-
|
|
534
|
+
// The limits are held over the files they count, so that is what the
|
|
535
|
+
// rows are over: what `limits.outside` names is said, and left out.
|
|
536
|
+
const walked = listSourceFiles(policy.repoRoot, roots, policy.languages);
|
|
537
|
+
const files = countedFiles(walked, outsideOf(policy));
|
|
489
538
|
const found = coverageOf(policy, files);
|
|
490
539
|
const fractions = fractionsOf(found);
|
|
491
540
|
const floors = policy.config.limits?.coverage ?? {};
|
|
541
|
+
const ceilings = policy.config.limits?.unreached ?? {};
|
|
492
542
|
const row = (family, covered, note) => {
|
|
493
543
|
const floor = floors[family];
|
|
494
544
|
const mark = floor === undefined
|
|
@@ -496,10 +546,10 @@ export const coverage = (policy, roots) => Effect.gen(function* () {
|
|
|
496
546
|
: fractions[family] >= floor
|
|
497
547
|
? ` ≥ ${percent(floor)} ✓`
|
|
498
548
|
: ` < ${percent(floor)} ✗`;
|
|
499
|
-
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(found.files)} ${percent(fractions[family]).padStart(4)} ${note}${mark}`;
|
|
549
|
+
return ` ${family.padEnd(10)} ${String(covered).padStart(5)}/${String(found.files)} ${percent(fractions[family]).padStart(4)} ${note}${mark}${unreachedMark(covered, found.files, ceilings[family])}`;
|
|
500
550
|
};
|
|
501
551
|
yield* report([
|
|
502
|
-
`${String(
|
|
552
|
+
`${String(walked.length)} files under ${roots.join(", ")}${outsideNote(walked.length - files.length)}`,
|
|
503
553
|
"",
|
|
504
554
|
row("imports", found.imports.covered, "under an import allowlist"),
|
|
505
555
|
row("structure", found.structure.enumerated, `in an enumerated folder (${String(found.structure.open)} in an open one, ${String(found.structure.total - found.structure.enumerated - found.structure.open)} in none)`),
|
|
@@ -596,7 +646,7 @@ export const explain = (policy, file, roots = ["packages"]) => Effect.gen(functi
|
|
|
596
646
|
? `${String(parts.of)} of ${String(parts.per)} here`
|
|
597
647
|
: `${String(valueOfParts(measure, parts))} here`;
|
|
598
648
|
return [
|
|
599
|
-
` ${objective.name} — ${firstSentence(objective.
|
|
649
|
+
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (measure, ${objective.direction}; ${here})`,
|
|
600
650
|
];
|
|
601
651
|
}
|
|
602
652
|
const table = explainObjective(objective, input);
|
|
@@ -604,7 +654,7 @@ export const explain = (policy, file, roots = ["packages"]) => Effect.gen(functi
|
|
|
604
654
|
return [];
|
|
605
655
|
const fired = table.length > 0 && table.every((line) => line.answer);
|
|
606
656
|
return [
|
|
607
|
-
` ${objective.name} — ${firstSentence(objective.
|
|
657
|
+
` ${objective.name} — ${firstSentence(objective.intent ?? objective.message)} (${objective.holdout}; ${fired ? "fires here" : "no hit"})`,
|
|
608
658
|
...table.map((line) => ` ${line.answer ? "✓" : "✗"} ${line.term}${line.count === undefined ? "" : ` (${String(line.count)})`}`),
|
|
609
659
|
];
|
|
610
660
|
}),
|
|
@@ -740,6 +790,7 @@ limits:
|
|
|
740
790
|
# perimeter: file
|
|
741
791
|
# objectives:
|
|
742
792
|
# is-ts:
|
|
793
|
+
# intent: A JavaScript file is one the strict compiler never checks.
|
|
743
794
|
# holdout: file
|
|
744
795
|
# match: { path: { file: "\\\\.(js|jsx)$" } }
|
|
745
796
|
# probes: { fires: [{ path: src/legacy/util.js }], ignores: [{ path: src/util.ts }] }
|
|
@@ -965,6 +1016,9 @@ export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* (
|
|
|
965
1016
|
try: () => clear(policy, evaluation, only, by),
|
|
966
1017
|
catch: (cause) => fail(String(cause)),
|
|
967
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) : [];
|
|
968
1022
|
for (const outcome of outcomes) {
|
|
969
1023
|
const parts = [
|
|
970
1024
|
...(outcome.entered.length > 0
|
|
@@ -997,6 +1051,13 @@ export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* (
|
|
|
997
1051
|
.join(", ")})`,
|
|
998
1052
|
]
|
|
999
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
|
+
: []),
|
|
1000
1061
|
...(outcome.closed > 0 ? [`${count(outcome.closed, "sector")} closed`] : []),
|
|
1001
1062
|
...(outcome.rebaselined.length > 0
|
|
1002
1063
|
? [
|
|
@@ -1009,6 +1070,12 @@ export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* (
|
|
|
1009
1070
|
}
|
|
1010
1071
|
lines.push(`${outcome.campaign}/${outcome.objective}: ${parts.length === 0 ? "nothing to clear" : parts.join(", ")}; ${count(outcome.left, "holdout")} left.`);
|
|
1011
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
|
+
}
|
|
1012
1079
|
}
|
|
1013
1080
|
return yield* report(lines);
|
|
1014
1081
|
}
|
|
@@ -1046,6 +1113,18 @@ export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* (
|
|
|
1046
1113
|
});
|
|
1047
1114
|
if (Result.isFailure(outcome))
|
|
1048
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
|
+
]);
|
|
1049
1128
|
const scalar = rule.objectives.find((one) => one.id === objective.success)?.measure !== null;
|
|
1050
1129
|
if (scalar) {
|
|
1051
1130
|
if (outcome.success.conceded.length === 0) {
|
|
@@ -1062,6 +1141,7 @@ export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* (
|
|
|
1062
1141
|
"",
|
|
1063
1142
|
`${count(outcome.success.left.length, "sector")} left past the record; check still fails on them.`,
|
|
1064
1143
|
]),
|
|
1144
|
+
...sentBack,
|
|
1065
1145
|
]);
|
|
1066
1146
|
}
|
|
1067
1147
|
if (outcome.success.conceded.length === 0) {
|
|
@@ -1078,6 +1158,7 @@ export const objectives = (policy, defaultRoots, argv) => Effect.gen(function* (
|
|
|
1078
1158
|
"",
|
|
1079
1159
|
`${count(outcome.success.left.length, "hit")} left unrecorded; check still fails on them.`,
|
|
1080
1160
|
]),
|
|
1161
|
+
...sentBack,
|
|
1081
1162
|
]);
|
|
1082
1163
|
}
|
|
1083
1164
|
default:
|
|
@@ -1094,15 +1175,53 @@ export const campaigns = (policy, defaultRoots, argv, configFilename) => Effect.
|
|
|
1094
1175
|
const retired = parsed.subcommand === undefined ? undefined : RETIRED[parsed.subcommand];
|
|
1095
1176
|
if (retired !== undefined)
|
|
1096
1177
|
return yield* Effect.fail(fail(retired));
|
|
1097
|
-
|
|
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) {
|
|
1098
1209
|
case undefined: {
|
|
1099
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
|
+
}
|
|
1100
1218
|
return yield* report([
|
|
1101
1219
|
`${count(snapshot.campaigns.length, "campaign")} under ${roots.join(", ")}`,
|
|
1102
1220
|
"",
|
|
1103
1221
|
...renderCampaignRows(snapshot.campaigns),
|
|
1104
1222
|
"",
|
|
1105
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",
|
|
1106
1225
|
" architecture objectives clear [<campaign>[/<objective>]] # reconcile the ledgers with the code",
|
|
1107
1226
|
' architecture objectives concede <campaign>[/<objective>] --reason "<why>" # record why a count may rise',
|
|
1108
1227
|
' architecture campaigns attest <sector> <phase> --reason "<why>" [--evidence <url>]',
|
|
@@ -1111,9 +1230,6 @@ export const campaigns = (policy, defaultRoots, argv, configFilename) => Effect.
|
|
|
1111
1230
|
]);
|
|
1112
1231
|
}
|
|
1113
1232
|
case "status": {
|
|
1114
|
-
if (!argv.includes("--changed")) {
|
|
1115
|
-
return yield* Effect.fail(fail("campaigns status takes --changed: the nudge is scoped to a diff."));
|
|
1116
|
-
}
|
|
1117
1233
|
const base = flagOf(argv, "--base") ?? null;
|
|
1118
1234
|
const diff = yield* Effect.try({
|
|
1119
1235
|
try: () => readDiff(policy.repoRoot, base),
|
|
@@ -1126,11 +1242,26 @@ export const campaigns = (policy, defaultRoots, argv, configFilename) => Effect.
|
|
|
1126
1242
|
try: () => baseSideAt(policy, base, roots, (repoRoot) => loadPolicyFromFile(repoRoot, configFilename)),
|
|
1127
1243
|
catch: (cause) => fail(`could not evaluate the base tree at ${base}: ${String(cause)}`),
|
|
1128
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
|
+
});
|
|
1129
1254
|
const hotfix = flagOf(argv, "--hotfix") ?? null;
|
|
1130
|
-
const nudge = nudgeOf(policy, current, diff, baseSide, hotfix, hotfix === null ? null : authorOf(flagOf(argv, "--by")));
|
|
1255
|
+
const nudge = nudgeOf(policy, current, diff, baseSide, hotfix, hotfix === null ? null : authorOf(flagOf(argv, "--by")), headTree);
|
|
1131
1256
|
yield* report(json ? [JSON.stringify(nudge, null, 2)] : renderNudge(nudge, policy.now));
|
|
1132
|
-
if (!nudge.ok)
|
|
1133
|
-
|
|
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
|
+
}
|
|
1134
1265
|
return;
|
|
1135
1266
|
}
|
|
1136
1267
|
case "attest": {
|