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 +13 -20
- package/package.json +1 -1
- package/src/audit-preparation.js +121 -14
- package/src/audit-selection.js +33 -0
- package/src/cli.js +13 -3
- package/src/collection-review.js +124 -31
- package/src/collection-revision.js +6 -1
- package/src/collection-scope.js +121 -5
- package/src/files.js +13 -1
- package/src/git.js +86 -0
- package/src/index.js +3 -2
- package/src/program-path.js +1 -1
- package/src/program-readiness.js +287 -34
- package/src/retention-schedule-approval.js +36 -17
- package/src/retention.js +81 -23
- package/src/server.js +15 -1
- package/src/source-coverage.js +7 -0
- package/src/state.js +2 -0
- package/src/validate.js +33 -3
- package/src/web.js +264 -134
- package/src/workflow.js +5 -1
package/model/v11.json
CHANGED
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
},
|
|
568
568
|
"vendor": {
|
|
569
569
|
"title": "Vendor inventory",
|
|
570
|
-
"description": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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": "
|
|
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
|
|
7709
|
-
"Keep the item planned until
|
|
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
|
-
"
|
|
7832
|
-
"status": "active"
|
|
7833
|
-
},
|
|
7834
|
-
"label": "Approved by"
|
|
7831
|
+
"label": "Legacy row reviewer"
|
|
7835
7832
|
},
|
|
7836
7833
|
"approvedOn": {
|
|
7837
7834
|
"type": "date",
|
|
7838
|
-
"
|
|
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
package/src/audit-preparation.js
CHANGED
|
@@ -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
|
|
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
|
|
107
|
-
|
|
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:
|
|
115
|
-
total:
|
|
116
|
-
percent:
|
|
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
|
-
: `${
|
|
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(`${
|
|
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}
|
|
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(`${
|
|
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])
|