filegrc 0.15.0 → 0.15.2

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/model/v11.json CHANGED
@@ -567,7 +567,7 @@
567
567
  },
568
568
  "vendor": {
569
569
  "title": "Vendor inventory",
570
- "description": "Confirm material external provider relationships separately from Program System and Component scope.",
570
+ "description": "Finish each material external provider record, then confirm the Vendor inventory once as a complete collection separate from Program System and Component scope.",
571
571
  "reviewPoints": [
572
572
  "Add Vendors for material external commercial or service relationships.",
573
573
  "Confirm contracts, due diligence, criticality, information access, and monitoring.",
@@ -593,7 +593,7 @@
593
593
  },
594
594
  "complementary-control": {
595
595
  "title": "Customer and provider responsibilities",
596
- "description": "Confirm whether any in-scope Control depends on a customer or carved-out provider action, including when no such dependency exists.",
596
+ "description": "Finish each customer or carved-out provider responsibility record, then confirm the collection once, including when no such dependency exists.",
597
597
  "reviewPoints": [
598
598
  "Think through the customer and carved-out provider actions that in-scope Controls depend on, and add any missing Complementary Control.",
599
599
  "Confirm each statement names a specific action, responsible party, affected System, and dependent Control.",
@@ -606,7 +606,7 @@
606
606
  },
607
607
  "component": {
608
608
  "title": "Scoped Components",
609
- "description": "Confirm the Components that materially participate in the selected Program Systems.",
609
+ "description": "Finish each Component proposal, then confirm once that the collection materially participating in the selected Program Systems is complete.",
610
610
  "reviewPoints": [
611
611
  "Start with each bounded System and include only Components that deliver its service, support Controls, produce authoritative Evidence, or support relevant operations.",
612
612
  "Confirm every System use has the right role and a specific rationale.",
@@ -631,7 +631,7 @@
631
631
  },
632
632
  "information-type": {
633
633
  "title": "Information Type inventory",
634
- "description": "Confirm that Information Types are complete, distinct, and linked to the Systems, Components, and Vendors that use or access them.",
634
+ "description": "Finish each Information Type proposal, then confirm once that the collection is complete, distinct, and linked to the Systems, Components, and Vendors that use or access it.",
635
635
  "reviewPoints": [
636
636
  "Review near-duplicate names and choose one canonical Information Type only after confirming they mean the same thing.",
637
637
  "Retire superseded records through a reviewed migration that rewrites every relationship.",
@@ -644,11 +644,11 @@
644
644
  },
645
645
  "retention-schedule-item": {
646
646
  "title": "Data Retention Schedule",
647
- "description": "Approve the schedule document and its structured rows as one governed schedule. Include only actual information uses and keep undecided organization values planned.",
647
+ "description": "Complete each structured row proposal, then approve the schedule document and all proposed rows in one independent review. Include only actual information uses and keep undecided organization values planned.",
648
648
  "reviewPoints": [
649
- "Review the Data Retention Schedule document, its Markdown, and every included structured row as one revision.",
649
+ "Confirm the owner completed every row proposal, then review the Data Retention Schedule document, its Markdown, and every proposed row as one revision.",
650
650
  "Confirm schedule coverage across the Systems, Components, Vendors, source families, logs, backups, audit records, and FileGRC records the organization actually uses.",
651
- "Confirm every active item has an approved cutoff, retention period, disposition action, owner, approver, and exact reviewed source revisions; keep undecided behavior planned."
651
+ "Confirm every proposed item has a reviewed cutoff, retention period, disposition action, owner, and exact source revisions; keep undecided behavior planned."
652
652
  ],
653
653
  "decisions": [
654
654
  "complete",
@@ -7699,14 +7699,14 @@
7699
7699
  "pluralTitle": "Retention Schedule",
7700
7700
  "group": "governance",
7701
7701
  "collection": "retention-schedule-items",
7702
- "description": "One reviewed schedule item that states which Information Types and operational scope it covers, when retention starts, how long information is kept, and its approved disposition.",
7702
+ "description": "One owner-completed proposal that states which Information Types and operational scope it covers, when retention starts, how long information is kept, and its proposed disposition. It becomes authoritative through the single whole-schedule approval.",
7703
7703
  "guidance": {
7704
7704
  "policyBasis": "Management must translate legal, contractual, privacy, security, and business needs into explicit retention and disposition instructions without treating starter periods as organization facts.",
7705
- "cadence": "Review before activation and after a source Policy, Requirement, Commitment, information use, System, Component, Vendor, or legal-hold process changes.",
7705
+ "cadence": "Complete the proposal before whole-schedule approval and revisit it after a source Policy, Requirement, Commitment, information use, System, Component, Vendor, or legal-hold process changes.",
7706
7706
  "reviewPoints": [
7707
7707
  "Confirm the Information Types and operational scope covered by this item.",
7708
- "Confirm the cutoff, retention period, and disposition action from approved management sources.",
7709
- "Keep the item planned until management approves every organization-specific value."
7708
+ "Confirm the cutoff, retention period, and disposition action from management sources.",
7709
+ "Keep the item planned until the owner completes every organization-specific value and binds the current source revisions."
7710
7710
  ],
7711
7711
  "sourceResourceIds": [
7712
7712
  "policy-information-security",
@@ -7828,16 +7828,11 @@
7828
7828
  "relation": [
7829
7829
  "person"
7830
7830
  ],
7831
- "requiredWhen": {
7832
- "status": "active"
7833
- },
7834
- "label": "Approved by"
7831
+ "label": "Legacy row reviewer"
7835
7832
  },
7836
7833
  "approvedOn": {
7837
7834
  "type": "date",
7838
- "requiredWhen": {
7839
- "status": "active"
7840
- }
7835
+ "label": "Legacy row review date"
7841
7836
  },
7842
7837
  "reviewedSourceRevisions": {
7843
7838
  "type": "object",
@@ -7891,8 +7886,6 @@
7891
7886
  "retentionPeriod",
7892
7887
  "dispositionAction",
7893
7888
  "dispositionInstructions",
7894
- "approvedByIds",
7895
- "approvedOn",
7896
7889
  "reviewedSourceRevisions",
7897
7890
  "supersedesId",
7898
7891
  "statusTransition"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { modelSupports } from "../model/index.js";
4
4
  import { applicabilityReviewIsCurrent } from "./applicability-scope.js";
5
+ import { selectDefaultAudit } from "./audit-selection.js";
5
6
  import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
6
7
  import {
7
8
  coverageContains,
@@ -63,20 +64,21 @@ export async function assessAuditPreparation(input, options = {}) {
63
64
  const records = loaded.resources;
64
65
  const byId = new Map(records.map((record) => [record.id, record]));
65
66
  const audits = records.filter((record) => record.type === "audit");
67
+ const calendarAsOf = options.asOf || currentCalendarDate(loaded.workspace?.timezone || "UTC");
66
68
  const audit = options.auditId
67
69
  ? audits.find((record) => record.id === options.auditId)
68
70
  : options.selectDefault === false
69
71
  ? null
70
- : audits.find((record) => !["complete", "closed", "canceled"].includes(record.status)) || audits[0];
72
+ : selectDefaultAudit(audits, calendarAsOf);
71
73
  if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
72
74
 
73
- const calendarAsOf = options.asOf || currentCalendarDate(loaded.workspace?.timezone || "UTC");
74
75
  const formalPeriodEnd = audit?.auditKind === "soc-2-type-2" ? coverageEnd(audit.coverage) : null;
75
76
  const readinessAsOf = formalPeriodEnd && formalPeriodEnd < calendarAsOf ? formalPeriodEnd : calendarAsOf;
76
77
  const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
77
78
  asOf: readinessAsOf,
78
79
  generatedAt: options.generatedAt,
79
- programId: audit?.programId || options.programId
80
+ programId: audit?.programId || options.programId,
81
+ auditId: audit?.id
80
82
  });
81
83
  const documentActivations = audit && modelSupports(loaded.model, "governed-document-activation")
82
84
  ? await auditDocumentActivationAssessments(loaded, audit, byId, calendarAsOf)
@@ -103,17 +105,21 @@ export async function assessAuditPreparation(input, options = {}) {
103
105
  }
104
106
  const items = stages.flatMap((stage) => stage.items);
105
107
  const counts = countStatuses(items);
106
- const managedItems = items.filter((item) => !["external", "info", "later"].includes(item.status));
107
- const completedManagedItems = managedItems.filter((item) => item.status === "complete");
108
+ const progressUnits = items.flatMap((current) => (
109
+ current.progressUnit === false || ["external", "info", "later"].includes(current.status)
110
+ ? []
111
+ : current.progressUnits?.length ? current.progressUnits : [current]
112
+ )).filter((current) => !["external", "info", "later"].includes(current.status));
113
+ const completedProgressUnits = progressUnits.filter((current) => current.status === "complete");
108
114
  return {
109
115
  schemaVersion: 1,
110
116
  generatedAt: options.generatedAt || new Date().toISOString(),
111
117
  audit: audit ? auditSummary(audit) : null,
112
118
  status: !audit ? "not-started" : counts.action ? "needs-work" : "management-ready",
113
119
  progress: {
114
- complete: completedManagedItems.length,
115
- total: managedItems.length,
116
- percent: managedItems.length ? Math.round((completedManagedItems.length / managedItems.length) * 100) : 0
120
+ complete: completedProgressUnits.length,
121
+ total: progressUnits.length,
122
+ percent: progressUnits.length ? Math.round((completedProgressUnits.length / progressUnits.length) * 100) : 0
117
123
  },
118
124
  counts,
119
125
  canInitialize: Boolean(audit
@@ -746,6 +752,12 @@ function canonicalScopeValue(value) {
746
752
  function programFoundationStage(programReadiness, workspace, audit) {
747
753
  const needsOperatingPeriod = audit?.auditKind === "soc-2-type-2";
748
754
  const ready = needsOperatingPeriod ? programReadiness.operating : programReadiness.evidenceReady;
755
+ const setupRemaining = programReadiness.setupProgress?.remaining
756
+ ?? (programReadiness.progress?.mode === "setup-progress" ? programReadiness.progress.remaining : 0);
757
+ const activeWindow = programReadiness.progress?.mode === "operating-window";
758
+ const activeWindowLabel = programReadiness.progress?.source === "audit"
759
+ ? "audit window"
760
+ : "candidate operating period";
749
761
  return stage("program", "Program Readiness", "The management program can be prepared and operated without an audit record or CPA firm.", [
750
762
  item(
751
763
  "evidence-ready",
@@ -755,9 +767,13 @@ function programFoundationStage(programReadiness, workspace, audit) {
755
767
  ? needsOperatingPeriod
756
768
  ? `${programReadiness.target.label} is operating for the selected engagement.`
757
769
  : `${programReadiness.target.label} is evidence-ready.`
770
+ : needsOperatingPeriod && activeWindow
771
+ ? setupRemaining
772
+ ? `The ${activeWindowLabel} is active, but ${setupRemaining} required setup ${setupRemaining === 1 ? "action remains" : "actions remain"} across scope, policies, controls, and evidence preparation.`
773
+ : `The ${activeWindowLabel} is active, but current operating work needs attention before treating the engagement as management-ready.`
758
774
  : needsOperatingPeriod && programReadiness.evidenceReady
759
775
  ? "Start the candidate period and finish the current operating-readiness work before treating a Type 2 engagement as management-ready."
760
- : `${programReadiness.counts.action} program-readiness actions remain across scope, policies, controls, and evidence preparation.`,
776
+ : `${setupRemaining} required setup ${setupRemaining === 1 ? "action remains" : "actions remain"} across scope, policies, controls, and evidence preparation.`,
761
777
  workspace || { type: "workspace" }
762
778
  )
763
779
  ]);
@@ -953,7 +969,7 @@ function occurrenceContinuityStage(audit, records, model, asOf) {
953
969
  const gapStatuses = new Set(["overdue", "blocked", "due", "proposed"]);
954
970
  const calendarGaps = plan.calendarItems.filter(({ status }) => gapStatuses.has(status));
955
971
  const eventGaps = plan.eventRuns
956
- .filter((run) => run.occurredOn >= periodStart && run.occurredOn <= periodThrough)
972
+ .filter((run) => run.status !== "canceled" && run.occurredOn >= periodStart && run.occurredOn <= periodThrough)
957
973
  .flatMap((run) => run.actions)
958
974
  .filter((action) => (
959
975
  (gapStatuses.has(action.status) || action.lateCompletion)
@@ -962,6 +978,57 @@ function occurrenceContinuityStage(audit, records, model, asOf) {
962
978
  const gaps = [...calendarGaps, ...eventGaps];
963
979
  const lateGaps = eventGaps.filter(({ lateCompletion }) => lateCompletion);
964
980
  const firstGap = gaps[0];
981
+ const progressUnits = occurrenceProgressUnits(plan, {
982
+ selectedControlIds,
983
+ periodStart,
984
+ periodThrough
985
+ });
986
+ return occurrenceContinuityStageResult({
987
+ audit,
988
+ gaps,
989
+ lateGaps,
990
+ firstGap,
991
+ eventGaps,
992
+ periodThrough,
993
+ progressUnits
994
+ });
995
+ }
996
+
997
+ export function occurrenceProgressUnits(plan, { selectedControlIds, periodStart, periodThrough }) {
998
+ return [
999
+ ...plan.calendarItems.flatMap((occurrence) => {
1000
+ const completedMemberIds = new Set(occurrence.completedMemberIds || []);
1001
+ const conclusionComplete = occurrence.reconciliationStatus === "reconciled"
1002
+ && ["complete", "complete-with-exceptions", "zero-population"].includes(occurrence.operatingResult);
1003
+ const occurrenceComplete = conclusionComplete || occurrence.legacySchedule && occurrence.status === "complete";
1004
+ const memberUnits = (occurrence.expectedMemberIds || []).map((resourceId) => ({
1005
+ id: `occurrence-${occurrence.occurrenceKey}-member-${resourceId}`,
1006
+ status: occurrenceComplete || completedMemberIds.has(resourceId) ? "complete" : "action",
1007
+ title: `${occurrence.title} member ${resourceId}`
1008
+ }));
1009
+ const membersComplete = memberUnits.every(({ status }) => status === "complete");
1010
+ return [
1011
+ ...memberUnits,
1012
+ {
1013
+ id: `occurrence-${occurrence.occurrenceKey}-reconciliation`,
1014
+ status: occurrenceComplete ? "complete" : membersComplete && occurrence.membershipFinal ? "action" : "blocked",
1015
+ title: `${occurrence.title} reconciliation`
1016
+ }
1017
+ ];
1018
+ }),
1019
+ ...plan.eventRuns
1020
+ .filter((run) => run.status !== "canceled" && run.occurredOn >= periodStart && run.occurredOn <= periodThrough)
1021
+ .flatMap((run) => run.actions)
1022
+ .filter((action) => action.controlIds.some((id) => selectedControlIds.has(id)))
1023
+ .map((action) => ({
1024
+ id: `event-action-${action.actionItemId || action.key}`,
1025
+ status: action.status === "complete" && !action.lateCompletion ? "complete" : "action",
1026
+ title: action.title
1027
+ }))
1028
+ ];
1029
+ }
1030
+
1031
+ function occurrenceContinuityStageResult({ audit, gaps, lateGaps, firstGap, eventGaps, periodThrough, progressUnits }) {
965
1032
  return stage(
966
1033
  "occurrences",
967
1034
  "Operating Occurrences",
@@ -985,7 +1052,8 @@ function occurrenceContinuityStage(audit, records, model, asOf) {
985
1052
  commands: firstGap?.actionItemId ? [
986
1053
  `npx filegrc get ${firstGap.actionItemId} --json`,
987
1054
  `npx filegrc scaffold finding --title "Late operating occurrence review"`
988
- ] : []
1055
+ ] : [],
1056
+ progressUnits
989
1057
  }
990
1058
  )]
991
1059
  );
@@ -1336,12 +1404,33 @@ function populationsStage(audit, records, byId, model) {
1336
1404
  const items = templates.map((template) => {
1337
1405
  const population = populations.find((record) => record.populationKind === template.kind);
1338
1406
  const result = populationResult(population, audit, byId);
1407
+ const preparationComplete = populationPreparationComplete(population, audit);
1339
1408
  return item(
1340
1409
  `population-${template.kind}`,
1341
1410
  result.status,
1342
1411
  template.title,
1343
1412
  result.message || `Use ${template.sourcePrompt} as the starting point, then record the exact authoritative source.`,
1344
- population || { type: "audit-population" }
1413
+ population || { type: "audit-population" },
1414
+ {
1415
+ progressUnits: population?.status === "not-applicable" ? [
1416
+ {
1417
+ id: `population-${template.kind}-applicability`,
1418
+ status: result.status,
1419
+ title: `${template.title} applicability decision`
1420
+ }
1421
+ ] : [
1422
+ {
1423
+ id: `population-${template.kind}-preparation`,
1424
+ status: preparationComplete ? "complete" : "action",
1425
+ title: `${template.title} preparation`
1426
+ },
1427
+ {
1428
+ id: `population-${template.kind}-conclusion`,
1429
+ status: result.status === "complete" ? "complete" : preparationComplete ? "action" : "blocked",
1430
+ title: `${template.title} reconciliation conclusion`
1431
+ }
1432
+ ]
1433
+ }
1345
1434
  );
1346
1435
  });
1347
1436
  return stage(
@@ -1388,7 +1477,7 @@ function auditorStage(audit, byId, modelVersion) {
1388
1477
  ]);
1389
1478
  }
1390
1479
 
1391
- function populationResult(population, audit, byId) {
1480
+ export function populationResult(population, audit, byId) {
1392
1481
  if (!population) return { status: "action", message: "Initialize this population for the engagement." };
1393
1482
  if (!coverageMatches(
1394
1483
  population.coverage,
@@ -1421,6 +1510,7 @@ function populationResult(population, audit, byId) {
1421
1510
  && evidence.type === "evidence"
1422
1511
  && evidence.artifactKind === "population-export"
1423
1512
  && evidence.status === "verified"
1513
+ && (population.ownerIds || []).length > 0
1424
1514
  && (population.sourceComponentId || population.sourceSystemId)
1425
1515
  && (evidence.sourceComponentId || evidence.sourceSystemId) === (population.sourceComponentId || population.sourceSystemId)
1426
1516
  && coverageMatches(
@@ -1447,6 +1537,21 @@ function populationResult(population, audit, byId) {
1447
1537
  };
1448
1538
  }
1449
1539
 
1540
+ function populationPreparationComplete(population, audit) {
1541
+ if (!population || !coverageMatches(
1542
+ population.coverage,
1543
+ coverageStart(audit?.coverage),
1544
+ coverageEnd(audit?.coverage)
1545
+ )) return false;
1546
+ if (population.status === "not-applicable") {
1547
+ return !(population.controlIds || []).length && Boolean(String(population.notApplicableReason || "").trim());
1548
+ }
1549
+ return Boolean(
1550
+ (population.ownerIds || []).length
1551
+ && (population.sourceComponentId || population.sourceSystemId)
1552
+ );
1553
+ }
1554
+
1450
1555
  function timestampDate(value, timezone) {
1451
1556
  if (!value || !timezone) return null;
1452
1557
  const instant = new Date(value);
@@ -1718,7 +1823,9 @@ function item(id, status, title, message, resource = {}, options = {}) {
1718
1823
  ...(options.commands?.length ? { commands: options.commands } : {}),
1719
1824
  ...(options.affectedActionItemIds?.length ? { affectedActionItemIds: options.affectedActionItemIds } : {}),
1720
1825
  ...(options.affectedEventIds?.length ? { affectedEventIds: options.affectedEventIds } : {}),
1721
- ...(options.lateCount ? { lateCount: options.lateCount } : {})
1826
+ ...(options.lateCount ? { lateCount: options.lateCount } : {}),
1827
+ ...(options.progressUnits?.length ? { progressUnits: options.progressUnits } : {}),
1828
+ ...(options.progressUnit === false ? { progressUnit: false } : {})
1722
1829
  };
1723
1830
  }
1724
1831
 
@@ -0,0 +1,33 @@
1
+ export function selectDefaultAudit(audits = [], asOf = "") {
2
+ const terminal = new Set(["complete", "closed", "canceled", "delivered"]);
3
+ const coverageStart = (audit) => audit?.coverage?.startsOn
4
+ || audit?.coverage?.periodStart
5
+ || audit?.coverage?.on
6
+ || "";
7
+ const statusOrder = new Map([
8
+ ["planned", 0],
9
+ ["in-progress", 1],
10
+ ["fieldwork", 2],
11
+ ["report-draft", 3],
12
+ ["issued", 4],
13
+ ["delivered", 5],
14
+ ["complete", 6]
15
+ ]);
16
+ const open = audits.filter((audit) => !terminal.has(audit.status));
17
+ const candidates = [...(open.length ? open : audits)];
18
+ candidates.sort((left, right) => {
19
+ const leftStart = coverageStart(left);
20
+ const rightStart = coverageStart(right);
21
+ const leftStarted = Boolean(leftStart && (!asOf || leftStart <= asOf));
22
+ const rightStarted = Boolean(rightStart && (!asOf || rightStart <= asOf));
23
+ return Number(rightStarted) - Number(leftStarted)
24
+ || (leftStarted && rightStarted
25
+ ? rightStart.localeCompare(leftStart)
26
+ : leftStart && rightStart
27
+ ? leftStart.localeCompare(rightStart)
28
+ : Number(Boolean(rightStart)) - Number(Boolean(leftStart)))
29
+ || (statusOrder.get(right.status) ?? -1) - (statusOrder.get(left.status) ?? -1)
30
+ || String(left.id || "").localeCompare(String(right.id || ""));
31
+ });
32
+ return candidates[0] || null;
33
+ }
package/src/cli.js CHANGED
@@ -136,6 +136,13 @@ export async function runCli(argv = process.argv.slice(2)) {
136
136
  if (result.usedFallbackPort) {
137
137
  console.log(`Port ${result.requestedPort} is already in use. Using ${result.address.port} instead.`);
138
138
  }
139
+ if (result.startupSynchronization?.status === "updated") {
140
+ console.log(`Updated the authoritative branch to ${result.startupSynchronization.shortCommit} before serving.`);
141
+ } else if (result.startupSynchronization?.status === "blocked") {
142
+ console.warn(`Remote startup sync is blocked because local and remote history diverged (${result.startupSynchronization.ahead} ahead, ${result.startupSynchronization.behind} behind). Reconcile the branch before relying on this server.`);
143
+ } else if (result.startupSynchronization?.status === "failed") {
144
+ console.warn(`Remote startup sync did not complete: ${result.startupSynchronization.message}`);
145
+ }
139
146
  console.log(`filegrc workspace: ${result.url}`);
140
147
  console.log(`Data: ${result.root}/data`);
141
148
  printGithubStarMessage();
@@ -447,16 +454,17 @@ export async function runCli(argv = process.argv.slice(2)) {
447
454
  const loaded = await loadWorkspace(root);
448
455
  const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId: flags.program });
449
456
  const output = flags.summary ? summarizeProgramReadiness(result) : result;
457
+ const headlineStatus = result.progress.status || result.status;
450
458
  if (flags.json) console.log(JSON.stringify(output, null, 2));
451
459
  else if (flags.summary) {
452
- console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
460
+ console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
453
461
  for (const stage of output.stages) {
454
- console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} actions`);
462
+ console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} checks need action`);
455
463
  }
456
464
  if (output.firstAction) console.log(`Next: ${output.firstAction.title}\t${output.firstAction.message}`);
457
465
  }
458
466
  else {
459
- console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
467
+ console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
460
468
  console.log(
461
469
  `${result.target.label}`
462
470
  + (result.target.candidateCoverage?.kind === "range"
@@ -2036,6 +2044,8 @@ function summarizeProgramReadiness(result) {
2036
2044
  suggestedCandidatePeriodStart: result.suggestedCandidatePeriodStart,
2037
2045
  target: result.target,
2038
2046
  progress: result.progress,
2047
+ setupProgress: result.setupProgress,
2048
+ checkProgress: result.checkProgress,
2039
2049
  counts: result.counts,
2040
2050
  scopeCounts: Object.fromEntries(
2041
2051
  Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])