filegrc 0.16.4 → 0.16.5
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/package.json +1 -1
- package/src/cli.js +3 -3
- package/src/program-readiness.js +99 -14
- package/src/reconciliation.js +1 -0
- package/src/retention-schedule-approval.js +5 -5
- package/src/web.js +20 -5
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1830,7 +1830,7 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1830
1830
|
const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
|
|
1831
1831
|
const stages = buildAgentProgramPath(model).map((stage) => {
|
|
1832
1832
|
if (stage.id === "audit") {
|
|
1833
|
-
const auditAction = auditReadiness
|
|
1833
|
+
const auditAction = !auditReadiness || auditReadiness.status === "not-started"
|
|
1834
1834
|
? {
|
|
1835
1835
|
id: "create-audit",
|
|
1836
1836
|
status: "action",
|
|
@@ -2001,11 +2001,11 @@ function nextActionCommands(stage, action) {
|
|
|
2001
2001
|
const commands = [`npx filegrc guide ${resourceType} --json`];
|
|
2002
2002
|
if (action.resourceId) {
|
|
2003
2003
|
const resourceId = shellArgument(action.resourceId);
|
|
2004
|
-
commands.push(`npx filegrc get ${resourceId} --mutation`);
|
|
2004
|
+
commands.push(`npx filegrc get ${resourceId} --mutation > MUTATION.json`);
|
|
2005
2005
|
commands.push(`npx filegrc update ${resourceType} ${resourceId} MUTATION.json --json`);
|
|
2006
2006
|
} else {
|
|
2007
2007
|
commands.push(`npx filegrc list ${resourceType} --json`);
|
|
2008
|
-
commands.push(`npx filegrc scaffold ${resourceType} --title "NAME"`);
|
|
2008
|
+
commands.push(`npx filegrc scaffold ${resourceType} --title "NAME" > MUTATION.json`);
|
|
2009
2009
|
commands.push("npx filegrc create MUTATION.json --json");
|
|
2010
2010
|
}
|
|
2011
2011
|
return commands;
|
package/src/program-readiness.js
CHANGED
|
@@ -470,7 +470,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
470
470
|
)));
|
|
471
471
|
items.push(item(
|
|
472
472
|
"commitments",
|
|
473
|
-
scope.systems.length
|
|
473
|
+
!scope.systems.length ? "later" : uncoveredSystems.length === 0 && unscopedCommitments.length === 0 ? "complete" : "action",
|
|
474
474
|
"Record service commitments and system requirements",
|
|
475
475
|
scope.systems.length
|
|
476
476
|
? `${completeCommitments.length} complete active ${completeCommitments.length === 1 ? "commitment covers" : "commitments cover"} ${scope.systems.length - uncoveredSystems.length} of ${scope.systems.length} in-scope systems.${unscopedCommitments.length ? ` ${unscopedCommitments.length} Commitment${unscopedCommitments.length === 1 ? " has" : "s have"} no System scope and must be reviewed explicitly.` : ""}`
|
|
@@ -711,14 +711,23 @@ function reportingRouteItem(records, byId, asOf, timezone = "UTC") {
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
export function collectionReviewReadinessItem(assessment) {
|
|
714
|
+
const firstIncomplete = assessment.incompleteRecordProposals?.[0]
|
|
715
|
+
|| assessment.recordProposals?.find(({ complete }) => !complete);
|
|
716
|
+
const needsFirstRecord = assessment.recordCount === 0
|
|
717
|
+
&& !(assessment.configuration.decisions || []).some((decision) => ["zero-population", "externally-managed"].includes(decision));
|
|
718
|
+
const proposalAction = !assessment.complete && firstIncomplete;
|
|
719
|
+
const pendingRetentionRow = assessment.resourceType === "retention-schedule-item"
|
|
720
|
+
? assessment.records?.find(({ status }) => status === "planned")
|
|
721
|
+
: null;
|
|
722
|
+
const approvalAction = !proposalAction && !pendingRetentionRow
|
|
723
|
+
? collectionApprovalIssueAction(assessment)
|
|
724
|
+
: null;
|
|
714
725
|
const proposalUnits = (assessment.recordProposals || []).map((proposal) => ({
|
|
715
726
|
id: `${assessment.resourceType}-proposal-${proposal.resourceId}`,
|
|
716
727
|
status: proposal.complete ? "complete" : "action",
|
|
717
728
|
title: `${proposal.title} proposal`
|
|
718
729
|
}));
|
|
719
730
|
const proposalBatchComplete = proposalUnits.every(({ status }) => status === "complete");
|
|
720
|
-
const scheduleApprovalBlocked = assessment.resourceType === "retention-schedule-item"
|
|
721
|
-
&& proposalUnits.some(({ status }) => status !== "complete");
|
|
722
731
|
const batchProgressUnits = assessment.resourceType !== "retention-schedule-item" && proposalUnits.length
|
|
723
732
|
? [
|
|
724
733
|
{
|
|
@@ -737,24 +746,100 @@ export function collectionReviewReadinessItem(assessment) {
|
|
|
737
746
|
: null;
|
|
738
747
|
return item(
|
|
739
748
|
`collection-review-${assessment.resourceType}`,
|
|
740
|
-
assessment.complete ? "complete" :
|
|
741
|
-
|
|
742
|
-
? `
|
|
743
|
-
:
|
|
744
|
-
|
|
745
|
-
|
|
749
|
+
assessment.complete ? "complete" : pendingRetentionRow && !proposalAction ? "blocked" : "action",
|
|
750
|
+
proposalAction
|
|
751
|
+
? `Complete ${firstIncomplete.title} proposal`
|
|
752
|
+
: needsFirstRecord
|
|
753
|
+
? `Add a record for ${assessment.configuration.title.toLowerCase()}`
|
|
754
|
+
: approvalAction
|
|
755
|
+
? approvalAction.title
|
|
756
|
+
: assessment.status === "stale"
|
|
757
|
+
? `Review ${assessment.configuration.title.toLowerCase()} again`
|
|
758
|
+
: `Review ${assessment.configuration.title.toLowerCase()}`,
|
|
759
|
+
proposalAction
|
|
760
|
+
? `Complete ${firstIncomplete.title} before reviewing ${assessment.configuration.title.toLowerCase()}.`
|
|
761
|
+
: assessment.message,
|
|
762
|
+
proposalAction
|
|
763
|
+
? { id: firstIncomplete.resourceId, type: assessment.resourceType }
|
|
764
|
+
: needsFirstRecord ? { type: assessment.resourceType }
|
|
765
|
+
: approvalAction ? { type: approvalAction.resourceType, ...(approvalAction.resourceId ? { id: approvalAction.resourceId } : {}) }
|
|
766
|
+
: assessment.review || { type: assessment.resourceType },
|
|
746
767
|
{
|
|
747
|
-
resourceType: assessment.resourceType,
|
|
768
|
+
resourceType: approvalAction?.resourceType || assessment.resourceType,
|
|
748
769
|
reviewPoints: assessment.configuration.reviewPoints,
|
|
770
|
+
...(approvalAction?.createResource ? { createResource: true } : {}),
|
|
771
|
+
...(pendingRetentionRow && !proposalAction ? { unresolvedAssignments: [{
|
|
772
|
+
resourceType: "retention-schedule-item",
|
|
773
|
+
resourceId: pendingRetentionRow.id,
|
|
774
|
+
reasons: ["Complete or retire the planned retention row before approving the schedule."]
|
|
775
|
+
}] } : {}),
|
|
749
776
|
...(batchProgressUnits ? { progressUnits: batchProgressUnits } : {}),
|
|
750
|
-
commands:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
777
|
+
commands: proposalAction
|
|
778
|
+
? [
|
|
779
|
+
`npx filegrc guide ${assessment.resourceType} --json`,
|
|
780
|
+
`npx filegrc get ${shellArgument(firstIncomplete.resourceId)} --mutation > MUTATION.json`,
|
|
781
|
+
`npx filegrc update ${assessment.resourceType} ${shellArgument(firstIncomplete.resourceId)} MUTATION.json --json`
|
|
782
|
+
]
|
|
783
|
+
: needsFirstRecord
|
|
784
|
+
? [
|
|
785
|
+
`npx filegrc guide ${assessment.resourceType} --json`,
|
|
786
|
+
`npx filegrc scaffold ${assessment.resourceType} --title "NAME" > MUTATION.json`,
|
|
787
|
+
"npx filegrc create MUTATION.json --json"
|
|
788
|
+
]
|
|
789
|
+
: approvalAction
|
|
790
|
+
? approvalAction.commands
|
|
791
|
+
: [
|
|
792
|
+
`npx filegrc review-collection ${assessment.resourceType} --scaffold > REVIEW.json`,
|
|
793
|
+
`npx filegrc review-collection ${assessment.resourceType} REVIEW.json --preview --json`,
|
|
794
|
+
`npx filegrc review-collection ${assessment.resourceType} REVIEW.json --yes --json`
|
|
795
|
+
]
|
|
754
796
|
}
|
|
755
797
|
);
|
|
756
798
|
}
|
|
757
799
|
|
|
800
|
+
function collectionApprovalIssueAction(assessment) {
|
|
801
|
+
const issue = assessment.approvalIssues?.[0];
|
|
802
|
+
if (!issue || issue.code === "invalid-retention-schedule-approval-binding") return null;
|
|
803
|
+
const rows = assessment.records || [];
|
|
804
|
+
const documentId = issue.resourceId || rows.find(({ scheduleDocumentId }) => scheduleDocumentId)?.scheduleDocumentId;
|
|
805
|
+
if (issue.code.includes("retention-schedule-document")) {
|
|
806
|
+
const resourceId = issue.code === "missing-retention-schedule-document" || issue.code === "multiple-retention-schedule-documents"
|
|
807
|
+
? null : documentId;
|
|
808
|
+
return {
|
|
809
|
+
title: issue.code === "missing-retention-schedule-document" ? "Add the Data Retention Schedule document"
|
|
810
|
+
: issue.code === "multiple-retention-schedule-documents" ? "Resolve duplicate Data Retention Schedule documents"
|
|
811
|
+
: "Complete the Data Retention Schedule document",
|
|
812
|
+
resourceType: "document",
|
|
813
|
+
resourceId,
|
|
814
|
+
createResource: issue.code === "missing-retention-schedule-document",
|
|
815
|
+
commands: resourceId
|
|
816
|
+
? ["npx filegrc guide document --json", `npx filegrc get ${shellArgument(resourceId)} --mutation > MUTATION.json`, `npx filegrc update document ${shellArgument(resourceId)} MUTATION.json --json`]
|
|
817
|
+
: issue.code === "missing-retention-schedule-document"
|
|
818
|
+
? ["npx filegrc guide document --json", 'npx filegrc scaffold document --title "Data Retention Schedule" > MUTATION.json', "npx filegrc create MUTATION.json --json"]
|
|
819
|
+
: ["npx filegrc guide document --json", "npx filegrc list document --workflow --json"]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (["retention-row-wrong-schedule-document", "incomplete-retention-schedule-row"].includes(issue.code)) {
|
|
823
|
+
const row = rows.find(({ id }) => id === issue.resourceId);
|
|
824
|
+
if (row) return {
|
|
825
|
+
title: `Complete ${row.title}`,
|
|
826
|
+
resourceType: "retention-schedule-item",
|
|
827
|
+
resourceId: row.id,
|
|
828
|
+
commands: ["npx filegrc guide retention-schedule-item --json", `npx filegrc get ${shellArgument(row.id)} --mutation > MUTATION.json`, `npx filegrc update retention-schedule-item ${shellArgument(row.id)} MUTATION.json --json`]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (issue.code === "unreviewed-similar-information-types") return {
|
|
832
|
+
title: "Review similar Information Types",
|
|
833
|
+
resourceType: "information-type",
|
|
834
|
+
commands: ["npx filegrc list information-type --workflow --json", "npx filegrc review-collection information-type --scaffold > REVIEW.json", "npx filegrc review-collection information-type REVIEW.json --preview --json", "npx filegrc review-collection information-type REVIEW.json --yes --json"]
|
|
835
|
+
};
|
|
836
|
+
return {
|
|
837
|
+
title: "Complete retention schedule coverage",
|
|
838
|
+
resourceType: "retention-schedule-item",
|
|
839
|
+
commands: ["npx filegrc guide retention-schedule-item --json", "npx filegrc list retention-schedule-item --workflow --json", 'npx filegrc scaffold retention-schedule-item --title "RETENTION RULE" > MUTATION.json', "npx filegrc create MUTATION.json --json"]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
758
843
|
function requiredAppointmentsItem(records, model) {
|
|
759
844
|
const assessments = assessRequiredAppointments(records, model);
|
|
760
845
|
const incomplete = assessments.filter(({ requiredness, state }) => (
|
package/src/reconciliation.js
CHANGED
|
@@ -769,6 +769,7 @@ function validateMergeIdentities(index, model) {
|
|
|
769
769
|
const identities = new Map();
|
|
770
770
|
for (const parent of parents) {
|
|
771
771
|
const state = recordStateAtCommit(index, model, parent);
|
|
772
|
+
if (!state) continue;
|
|
772
773
|
let snapshot = snapshots.get(state);
|
|
773
774
|
if (!snapshot) {
|
|
774
775
|
snapshot = state.snapshot || snapshotRecordState(state);
|
|
@@ -43,7 +43,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
43
43
|
|| !substantiveMarkdown(scheduleContent)
|
|
44
44
|
|| openPlaceholderCount(scheduleContent) > 0)
|
|
45
45
|
) {
|
|
46
|
-
issues.push(issue("incomplete-retention-schedule-document", "Complete the Data Retention Schedule document, its owner, linked Control, and Markdown before requesting approval of the complete schedule."));
|
|
46
|
+
issues.push(issue("incomplete-retention-schedule-document", "Complete the Data Retention Schedule document, its owner, linked Control, and Markdown before requesting approval of the complete schedule.", schedule.id));
|
|
47
47
|
}
|
|
48
48
|
if (
|
|
49
49
|
schedule
|
|
@@ -60,7 +60,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
60
60
|
? activeRows.filter(({ scheduleDocumentId }) => scheduleDocumentId !== schedule.id)
|
|
61
61
|
: [];
|
|
62
62
|
if (mismatched.length) {
|
|
63
|
-
issues.push(issue("retention-row-wrong-schedule-document", `Link every active retention row to the current Data Retention Schedule document before approval: ${mismatched[0].title}
|
|
63
|
+
issues.push(issue("retention-row-wrong-schedule-document", `Link every active retention row to the current Data Retention Schedule document before approval: ${mismatched[0].title}.`, mismatched[0].id));
|
|
64
64
|
}
|
|
65
65
|
const revisions = resourceReviewRevisionsSync(
|
|
66
66
|
loaded,
|
|
@@ -68,7 +68,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
68
68
|
);
|
|
69
69
|
const incomplete = activeRows.filter((row) => !retentionRuleIsCurrent(row, revisions, byId, loaded));
|
|
70
70
|
if (incomplete.length) {
|
|
71
|
-
issues.push(issue("incomplete-retention-schedule-row", `Complete the retention decisions before approving the schedule revision: ${incomplete[0].title}
|
|
71
|
+
issues.push(issue("incomplete-retention-schedule-row", `Complete the retention decisions before approving the schedule revision: ${incomplete[0].title}.`, incomplete[0].id));
|
|
72
72
|
}
|
|
73
73
|
const proposedRows = rows.filter(rowProposesCoverage);
|
|
74
74
|
const scopedPrograms = program.programIds
|
|
@@ -96,8 +96,8 @@ function sameIds(left = [], right = []) {
|
|
|
96
96
|
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function issue(code, message) {
|
|
100
|
-
return { code, message };
|
|
99
|
+
function issue(code, message, resourceId) {
|
|
100
|
+
return { code, message, ...(resourceId ? { resourceId } : {}) };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function primaryMarkdown(loaded, record) {
|
package/src/web.js
CHANGED
|
@@ -578,8 +578,8 @@ function readinessOverview() {
|
|
|
578
578
|
const stage = READINESS_STAGES.find((candidate) => candidate.id === id);
|
|
579
579
|
const current = stageProgress(stage);
|
|
580
580
|
const remaining = current.total - current.complete;
|
|
581
|
-
const status =
|
|
582
|
-
return [stage.title, stage.summary, href, status,
|
|
581
|
+
const status = current.status === "In progress" ? remaining + " pages need work" : current.status;
|
|
582
|
+
return [stage.title, stage.summary, href, status, current.tone];
|
|
583
583
|
};
|
|
584
584
|
const stages = [
|
|
585
585
|
programStage("scope", "#/stage/scope"),
|
|
@@ -787,6 +787,7 @@ function wireRetentionScheduleTable(total, initialPage = 1) {
|
|
|
787
787
|
function renderRetentionScheduleIssues() {
|
|
788
788
|
const actions = retentionSchedulePageItems().filter((item) => (
|
|
789
789
|
item.id !== "collection-review-retention-schedule-item"
|
|
790
|
+
|| !item.commands?.some((command) => command.includes(" review-collection retention-schedule-item"))
|
|
790
791
|
));
|
|
791
792
|
if (!actions.length) return "";
|
|
792
793
|
return '<section class="retention-issues"><div class="section-head"><div><p class="kicker">Before approval</p><h2>Items to resolve</h2></div></div><div class="retention-issue-list">' + actions.map((item) => {
|
|
@@ -1719,7 +1720,7 @@ function retentionSchedulePageItems() {
|
|
|
1719
1720
|
key: item.id,
|
|
1720
1721
|
state: "ready",
|
|
1721
1722
|
href: item.id === "collection-review-retention-schedule-item"
|
|
1722
|
-
?
|
|
1723
|
+
? retentionCollectionActionHref(item)
|
|
1723
1724
|
: item.id?.startsWith("retention-use-")
|
|
1724
1725
|
? retentionScheduleItemHref(item)
|
|
1725
1726
|
: item.resourceId && item.resourceType
|
|
@@ -1732,6 +1733,19 @@ function retentionSchedulePageItems() {
|
|
|
1732
1733
|
]);
|
|
1733
1734
|
}
|
|
1734
1735
|
|
|
1736
|
+
function retentionCollectionActionHref(item) {
|
|
1737
|
+
if (item.resourceType === "document") return item.resourceId
|
|
1738
|
+
? '#/resource/document/' + encodeURIComponent(item.resourceId) + '?stage=policies'
|
|
1739
|
+
: item.createResource
|
|
1740
|
+
? '#/resources/document?new=1&documentScope=program&documentKind=schedule&programRole=required&title=Data%20Retention%20Schedule'
|
|
1741
|
+
: '#/resources/document';
|
|
1742
|
+
if (item.resourceType === "information-type") return '#/resources/information-type?review-collection=1';
|
|
1743
|
+
if (item.resourceType === "retention-schedule-item" && item.resourceId && item.resourceId !== item.id) {
|
|
1744
|
+
return '#/resource/retention-schedule-item/' + encodeURIComponent(item.resourceId) + '?stage=policies';
|
|
1745
|
+
}
|
|
1746
|
+
return "#/retention-schedule";
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1735
1749
|
function distinctPageActionItems(items) {
|
|
1736
1750
|
const distinct = new Map();
|
|
1737
1751
|
for (const item of items) {
|
|
@@ -1842,14 +1856,14 @@ function derivedStagePageState(stage, destination) {
|
|
|
1842
1856
|
.filter(({ id }) => id.startsWith("source-family-")) || [];
|
|
1843
1857
|
const incomplete = evidenceItems.filter(({ status }) => status !== "complete").length;
|
|
1844
1858
|
return incomplete
|
|
1845
|
-
? { complete: false, label: incomplete + " " + pluralize("source", incomplete) + " need work" }
|
|
1859
|
+
? { complete: false, label: incomplete + " " + pluralize("source", incomplete) + (incomplete === 1 ? " needs work" : " need work") }
|
|
1846
1860
|
: { complete: evidenceItems.length > 0, label: evidenceItems.length ? "Ready" : "Not configured", countsTowardProgress: evidenceItems.length > 0 };
|
|
1847
1861
|
}
|
|
1848
1862
|
if (destination.utility === "retention-schedule") {
|
|
1849
1863
|
const incomplete = retentionSchedulePageItems().length;
|
|
1850
1864
|
return !incomplete
|
|
1851
1865
|
? { complete: true, label: "Approved" }
|
|
1852
|
-
: { complete: false, label: incomplete ? incomplete + " " + pluralize("item", incomplete) + " need work" : "Approval needed" };
|
|
1866
|
+
: { complete: false, label: incomplete ? incomplete + " " + pluralize("item", incomplete) + (incomplete === 1 ? " needs work" : " need work") : "Approval needed" };
|
|
1853
1867
|
}
|
|
1854
1868
|
const items = stagePageItems(stage, destination);
|
|
1855
1869
|
const deferredStates = new Set(["later", "scheduled", "upcoming", "waiting-external"]);
|
|
@@ -3223,6 +3237,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
3223
3237
|
...(params.get("title") ? { title: params.get("title") } : {}),
|
|
3224
3238
|
...(params.get("documentKind") ? { documentKind: params.get("documentKind") } : {}),
|
|
3225
3239
|
...(documentScope ? { workflowScope: documentScope } : {}),
|
|
3240
|
+
...(params.get("programRole") ? { programRole: params.get("programRole") } : {}),
|
|
3226
3241
|
...relationshipSeed
|
|
3227
3242
|
} : {
|
|
3228
3243
|
...(params.get("title") ? { title: params.get("title") } : {}),
|