filegrc 0.15.1 → 0.15.3
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 +111 -10
- package/src/cli.js +7 -0
- 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 +2 -1
- package/src/obligation-members.js +31 -0
- package/src/obligations.js +39 -6
- package/src/program-path.js +1 -1
- package/src/program-readiness.js +102 -15
- package/src/retention-schedule-approval.js +36 -17
- package/src/retention.js +59 -20
- package/src/server.js +15 -1
- package/src/source-coverage.js +7 -0
- package/src/state.js +2 -0
- package/src/validate.js +35 -15
- package/src/web.js +80 -39
- package/src/workflow.js +3 -1
package/src/web.js
CHANGED
|
@@ -639,7 +639,11 @@ function renderRetentionSchedulePage(main, params = new URLSearchParams()) {
|
|
|
639
639
|
const documentEntry = documentEntries.length === 1 ? documentEntries[0] : null;
|
|
640
640
|
const rows = resourcesOfType("retention-schedule-item")
|
|
641
641
|
.sort((left, right) => left.record.title.localeCompare(right.record.title));
|
|
642
|
-
const
|
|
642
|
+
const completedProposalIds = new Set(state.programReadiness?.stages
|
|
643
|
+
?.find(({ id }) => id === "policies")
|
|
644
|
+
?.items.filter(({ id, status }) => id.startsWith("retention-rule-") && status === "complete")
|
|
645
|
+
.map(({ id }) => id.slice("retention-rule-".length)) || []);
|
|
646
|
+
const proposedRows = rows.filter(({ record }) => completedProposalIds.has(record.id)).length;
|
|
643
647
|
const documentReadiness = documentEntry ? retentionScheduleDocumentReadiness(documentEntry.record.id) : null;
|
|
644
648
|
const documentReady = documentEntries.length === 1 && documentReadiness?.status === "complete";
|
|
645
649
|
const documentAction = documentEntry
|
|
@@ -656,21 +660,25 @@ function renderRetentionSchedulePage(main, params = new URLSearchParams()) {
|
|
|
656
660
|
const cutoff = retentionCutoffLabel(record.cutoff);
|
|
657
661
|
const period = retentionPeriodLabel(record.retentionPeriod);
|
|
658
662
|
const action = record.dispositionAction ? properCase(record.dispositionAction) : "Not selected";
|
|
659
|
-
|
|
663
|
+
const status = record.status === "active" && state.collectionReviews?.["retention-schedule-item"]?.status !== "current"
|
|
664
|
+
? "proposed"
|
|
665
|
+
: displayStatus(record);
|
|
666
|
+
return '<tr data-retention-row data-history="' + (["superseded", "retired"].includes(record.status) ? "true" : "false") + '"><td data-label="Rule" data-primary-field><a class="record-title" href="#/resource/retention-schedule-item/' + encodeURIComponent(record.id) + '?stage=policies">' + esc(record.title) + '</a><small>' + esc(record.description || record.id) + '</small></td><td data-label="Information types">' + esc(informationTypes) + '</td><td data-label="Scope">' + esc(scope) + '</td><td data-label="Retention"><strong>' + esc(period) + '</strong><small>From ' + esc(cutoff) + '</small></td><td data-label="Disposition">' + esc(action) + '</td><td data-label="Status">' + formatValue(status, "status", record.type, true) + '</td><td data-label="Next action">' + recordWorkflowCell("retention-schedule-item", entry, "?stage=policies") + '</td></tr>';
|
|
660
667
|
}).join("")
|
|
661
668
|
: '<tr><td colspan="7">' + empty("No retention rules are needed until an actual information use requires one.") + '</td></tr>';
|
|
662
669
|
const add = state.readOnly ? "" : '<button class="button primary" type="button" data-add-retention-row>Add schedule row</button>';
|
|
663
670
|
const guideTrigger = '<button class="guide-trigger" id="resource-guide-trigger" type="button" aria-label="About Data Retention Schedule" aria-haspopup="dialog" aria-controls="resource-guide" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button>';
|
|
664
671
|
const initialFilter = params.get("q") || "";
|
|
665
672
|
const showHistory = params.get("history") === "1";
|
|
666
|
-
const
|
|
673
|
+
const retiredRows = rows.filter(({ record }) => ["superseded", "retired"].includes(record.status)).length;
|
|
674
|
+
const tableTools = '<div class="retention-table-tools"><label><span class="sr-only">Filter schedule rows</span><input type="search" data-retention-search placeholder="Filter schedule rows" value="' + esc(initialFilter) + '"></label>' + (retiredRows ? '<label class="history-toggle"><input type="checkbox" data-retention-history ' + (showHistory ? "checked" : "") + '> Show ' + retiredRows + ' retired ' + pluralize("row", retiredRows) + '</label>' : '<input type="checkbox" data-retention-history hidden>') + '</div>';
|
|
667
675
|
main.innerHTML = '<div class="page retention-schedule-page"><nav class="breadcrumbs"><a href="#/stage/policies">Step 2</a><span>/</span><span>Data Retention Schedule</span></nav>' +
|
|
668
|
-
'<div class="page-intro"><div><p class="kicker">Step 2</p><div class="page-title-line"><h2>Data Retention Schedule</h2>' + guideTrigger + '</div><p>
|
|
669
|
-
'<div class="retention-summary"><div><span class="retention-summary-label">Governing document</span>' + documentSummary + '</div><div><span class="retention-summary-label">Schedule revision</span><span><strong>' +
|
|
670
|
-
'<section class="retention-rows"><div class="section-head"><div><p class="kicker">Structured schedule</p><h2>Schedule rows</h2><p>Add a row when an Information Type and its operational scope need a distinct cutoff, period, or disposition rule.</p></div>' + tableTools + '</div><div class="record-table-wrap"><table class="record-table"><thead><tr><th>Rule</th><th>Information types</th><th>Scope</th><th>Retention</th><th>Disposition</th><th>Status</th><th>Next action</th></tr></thead><tbody>' + rowHtml + '</tbody></table></div><p class="retention-filter-empty" data-retention-empty hidden>No schedule rows match this filter.</p><p class="retention-row-count" data-retention-count aria-live="polite"></p></section>' +
|
|
676
|
+
'<div class="page-intro"><div><p class="kicker">Step 2</p><div class="page-title-line"><h2>Data Retention Schedule</h2>' + guideTrigger + '</div><p>Owners finish each proposed retention row first. A separate reviewer then approves the complete document and all proposed rows once.</p></div><div class="actions">' + documentAction + add + '</div></div>' + resourceGuide("retention-schedule-item") +
|
|
677
|
+
'<div class="retention-summary"><div><span class="retention-summary-label">Governing document</span>' + documentSummary + '</div><div><span class="retention-summary-label">Schedule revision</span><span><strong>' + proposedRows + ' completed ' + pluralize("proposal", proposedRows) + '</strong><small>Final approval follows every row proposal</small></span><span class="badge ' + (state.collectionReviews?.["retention-schedule-item"]?.status === "current" ? "good" : "warn") + '">' + (state.collectionReviews?.["retention-schedule-item"]?.status === "current" ? "Approved" : "Approval needed") + '</span></div></div>' +
|
|
678
|
+
'<section class="retention-rows"><div class="section-head"><div><p class="kicker">Structured schedule</p><h2>Schedule rows</h2><p>Add a row when an Information Type and its operational scope need a distinct cutoff, period, or disposition rule.</p></div>' + tableTools + '</div><div class="record-table-wrap"><table class="record-table"><thead><tr><th>Rule</th><th>Information types</th><th>Scope</th><th>Retention</th><th>Disposition</th><th>Status</th><th>Next action</th></tr></thead><tbody>' + rowHtml + '</tbody></table></div><p class="retention-filter-empty" data-retention-empty hidden>No schedule rows match this filter.</p><p class="retention-row-count" data-retention-count aria-live="polite"></p><nav class="pagination retention-pagination" aria-label="Data Retention Schedule pages" hidden><button class="button" type="button" data-retention-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-retention-page="next">Next</button></nav></section>' +
|
|
671
679
|
renderRetentionScheduleIssues() + collectionReviewPanel("retention-schedule-item", true) + '</div>';
|
|
672
680
|
resourceGuideCleanup = setupResourceGuide(main);
|
|
673
|
-
wireRetentionScheduleTable(rows.length);
|
|
681
|
+
wireRetentionScheduleTable(rows.length, Number(params.get("page")) || 1);
|
|
674
682
|
main.querySelector("[data-review-collection]")?.addEventListener("click", () => openCollectionReviewDialog("retention-schedule-item"));
|
|
675
683
|
main.querySelector("[data-add-retention-document]")?.addEventListener("click", () => openEditor("document", null, {
|
|
676
684
|
seed: {
|
|
@@ -708,38 +716,60 @@ function openRetentionScheduleItemEditor(seed = {}) {
|
|
|
708
716
|
...(scheduleDocuments.length === 1 ? { scheduleDocumentId: scheduleDocuments[0].record.id } : {}),
|
|
709
717
|
...seed
|
|
710
718
|
},
|
|
711
|
-
description: "Define one retention decision. Keep it planned until its scope, cutoff, period, disposition, sources, and
|
|
719
|
+
description: "Define one retention decision. Keep it planned until its scope, cutoff, period, disposition, sources, owner review, and current revision bindings are complete.",
|
|
712
720
|
saveLabel: "Save schedule row"
|
|
713
721
|
});
|
|
714
722
|
}
|
|
715
723
|
|
|
716
|
-
function wireRetentionScheduleTable(total) {
|
|
724
|
+
function wireRetentionScheduleTable(total, initialPage = 1) {
|
|
717
725
|
const table = root.querySelector(".retention-rows");
|
|
718
726
|
if (!table) return;
|
|
719
727
|
const search = table.querySelector("[data-retention-search]");
|
|
720
728
|
const historyToggle = table.querySelector("[data-retention-history]");
|
|
721
729
|
const count = table.querySelector("[data-retention-count]");
|
|
722
730
|
const noResults = table.querySelector("[data-retention-empty]");
|
|
731
|
+
const pagination = table.querySelector(".retention-pagination");
|
|
732
|
+
const pageStatus = pagination.querySelector(".page-status");
|
|
733
|
+
const previous = pagination.querySelector('[data-retention-page="previous"]');
|
|
734
|
+
const next = pagination.querySelector('[data-retention-page="next"]');
|
|
735
|
+
let pageNumber = Number.isInteger(initialPage) && initialPage > 0 ? initialPage : 1;
|
|
723
736
|
const filter = () => {
|
|
724
737
|
const query = search.value.trim().toLowerCase();
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
738
|
+
const filtered = [...table.querySelectorAll("[data-retention-row]")].filter((row) => (
|
|
739
|
+
(historyToggle.checked || row.dataset.history !== "true")
|
|
740
|
+
&& (!query || row.textContent.toLowerCase().includes(query))
|
|
741
|
+
));
|
|
742
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / LIST_PAGE_SIZE));
|
|
743
|
+
pageNumber = Math.min(pageNumber, totalPages);
|
|
744
|
+
const start = (pageNumber - 1) * LIST_PAGE_SIZE;
|
|
745
|
+
const visible = new Set(filtered.slice(start, start + LIST_PAGE_SIZE));
|
|
746
|
+
table.querySelectorAll("[data-retention-row]").forEach((row) => { row.hidden = !visible.has(row); });
|
|
747
|
+
noResults.hidden = filtered.length !== 0 || total === 0;
|
|
748
|
+
const firstVisible = filtered.length ? start + 1 : 0;
|
|
749
|
+
const lastVisible = Math.min(start + LIST_PAGE_SIZE, filtered.length);
|
|
750
|
+
const hiddenByHistory = historyToggle.checked ? 0 : [...table.querySelectorAll('[data-retention-row][data-history="true"]')]
|
|
751
|
+
.filter((row) => !query || row.textContent.toLowerCase().includes(query)).length;
|
|
752
|
+
const matching = filtered.length + hiddenByHistory;
|
|
753
|
+
count.textContent = firstVisible + "–" + lastVisible + " of " + filtered.length + (historyToggle.checked ? " shown " : " active ") + pluralize("row", filtered.length)
|
|
754
|
+
+ (hiddenByHistory ? " · " + hiddenByHistory + " retired hidden" : "")
|
|
755
|
+
+ (query ? " · " + matching + " matching" : "")
|
|
756
|
+
+ " · " + total + " total";
|
|
757
|
+
pagination.hidden = totalPages === 1;
|
|
758
|
+
pageStatus.textContent = "Page " + pageNumber + " of " + totalPages;
|
|
759
|
+
previous.disabled = pageNumber === 1;
|
|
760
|
+
next.disabled = pageNumber === totalPages;
|
|
734
761
|
};
|
|
735
762
|
const updateLocation = () => {
|
|
736
763
|
const params = new URLSearchParams();
|
|
737
764
|
if (search.value.trim()) params.set("q", search.value.trim());
|
|
738
765
|
if (historyToggle.checked) params.set("history", "1");
|
|
766
|
+
if (pageNumber > 1) params.set("page", String(pageNumber));
|
|
739
767
|
history.replaceState(null, "", "#/retention-schedule" + (params.size ? "?" + params : ""));
|
|
740
768
|
};
|
|
741
|
-
search.addEventListener("input", () => { filter(); updateLocation(); });
|
|
742
|
-
historyToggle.addEventListener("change", () => { filter(); updateLocation(); });
|
|
769
|
+
search.addEventListener("input", () => { pageNumber = 1; filter(); updateLocation(); });
|
|
770
|
+
historyToggle.addEventListener("change", () => { pageNumber = 1; filter(); updateLocation(); });
|
|
771
|
+
previous.addEventListener("click", () => { pageNumber -= 1; filter(); updateLocation(); });
|
|
772
|
+
next.addEventListener("click", () => { pageNumber += 1; filter(); updateLocation(); });
|
|
743
773
|
filter();
|
|
744
774
|
}
|
|
745
775
|
|
|
@@ -759,9 +789,16 @@ function renderRetentionScheduleIssues() {
|
|
|
759
789
|
}
|
|
760
790
|
|
|
761
791
|
function retentionScheduleDocumentReadiness(documentId) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
792
|
+
const documentIssueCodes = new Set([
|
|
793
|
+
"missing-retention-schedule-document",
|
|
794
|
+
"multiple-retention-schedule-documents",
|
|
795
|
+
"incomplete-retention-schedule-document"
|
|
796
|
+
]);
|
|
797
|
+
const issue = state.collectionReviews?.["retention-schedule-item"]?.approvalIssues
|
|
798
|
+
?.find(({ code }) => documentIssueCodes.has(code));
|
|
799
|
+
return issue
|
|
800
|
+
? { status: "action", message: issue.message }
|
|
801
|
+
: { status: "complete", message: "The document proposal is complete. Final approval follows all completed row proposals." };
|
|
765
802
|
}
|
|
766
803
|
|
|
767
804
|
function referenceTitle(id) {
|
|
@@ -1162,27 +1199,34 @@ function collectionReviewPanel(type, force = false) {
|
|
|
1162
1199
|
const current = assessment.status === "current";
|
|
1163
1200
|
const scheduleReview = type === "retention-schedule-item" && modelSupports("retention-schedule-approval");
|
|
1164
1201
|
const scheduleBlocker = scheduleReview ? retentionScheduleApprovalBlocker() : null;
|
|
1202
|
+
const proposalBlocker = assessment.incompleteRecordProposals?.length
|
|
1203
|
+
? "Complete " + assessment.incompleteRecordProposals.length + " record " + (assessment.incompleteRecordProposals.length === 1 ? "proposal" : "proposals") + " before the collection review."
|
|
1204
|
+
: null;
|
|
1205
|
+
const reviewBlocker = scheduleBlocker || proposalBlocker;
|
|
1165
1206
|
const needsFirstRecord = collectionNeedsFirstRecord(type);
|
|
1166
1207
|
const reviewerNames = (assessment.review?.reviewedByIds || [])
|
|
1167
1208
|
.map((id) => state.resources.find(({ record }) => record.id === id)?.record.title || id);
|
|
1168
1209
|
const reviewNote = assessment.review?.rationale
|
|
1169
1210
|
? " Note: " + esc(assessment.review.rationale)
|
|
1170
1211
|
: "";
|
|
1171
|
-
const reviewSummary = current && !
|
|
1212
|
+
const reviewSummary = current && !reviewBlocker
|
|
1172
1213
|
? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + "." + reviewNote + '</span></p>'
|
|
1173
|
-
: '<p class="collection-review-result"><strong>' + (scheduleBlocker ? "Complete schedule first" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(
|
|
1174
|
-
const compactReviewSummary = current && !
|
|
1214
|
+
: '<p class="collection-review-result"><strong>' + (scheduleBlocker ? "Complete schedule first" : proposalBlocker ? "Complete proposals first" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(reviewBlocker || assessment.message) + '</span></p>';
|
|
1215
|
+
const compactReviewSummary = current && !reviewBlocker
|
|
1175
1216
|
? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + ".</span></p>"
|
|
1176
1217
|
: reviewSummary;
|
|
1177
1218
|
const action = state.readOnly
|
|
1178
1219
|
? ""
|
|
1179
|
-
:
|
|
1180
|
-
? '<button class="button" type="button" disabled title="' + esc(
|
|
1220
|
+
: reviewBlocker
|
|
1221
|
+
? '<button class="button" type="button" disabled title="' + esc(reviewBlocker) + '">' + (scheduleReview ? "Review and approve schedule" : "Review and confirm") + '</button>'
|
|
1181
1222
|
: needsFirstRecord
|
|
1182
1223
|
? '<a class="button primary" href="#/resources/' + encodeURIComponent(type) + '?new=1">Add first ' + esc(state.model.resources[type].title.toLowerCase()) + '</a>'
|
|
1183
1224
|
: '<button class="button ' + (current ? "" : "primary") + '" type="button" data-review-collection="' + esc(type) + '">' + (current ? scheduleReview ? "Approve a new revision" : "Review again" : scheduleReview ? "Review and approve schedule" : "Review and confirm") + '</button>';
|
|
1184
|
-
const status =
|
|
1185
|
-
const
|
|
1225
|
+
const status = reviewBlocker ? "Not ready" : current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required";
|
|
1226
|
+
const proposalSummary = assessment.recordProposals?.length
|
|
1227
|
+
? '<p class="collection-review-proposal-summary"><strong>' + assessment.recordProposals.filter(({ complete }) => complete).length + ' of ' + assessment.recordProposals.length + ' record proposals complete.</strong> The collection review follows every proposal.</p>'
|
|
1228
|
+
: "";
|
|
1229
|
+
const details = '<details ' + (current ? 'class="collection-review-details"' : "open") + '><summary>' + (current ? scheduleReview ? "Show approved revision" : "Show scope confirmation" : "What to review") + '</summary><div class="collection-review-detail-content"><div class="collection-review-head"><div><p class="kicker">' + (scheduleReview ? "Schedule approval" : "Scope confirmation") + '</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + status + '</span></div>' + proposalSummary + '<ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>' + (current ? reviewSummary : "") + '</div></details>';
|
|
1186
1230
|
const summary = current
|
|
1187
1231
|
? '<div class="collection-review-current-row"><div class="collection-review-complete-summary"><span class="kicker">' + (scheduleReview ? "Approved schedule" : "Scope confirmation") + '</span>' + compactReviewSummary + '</div>' + details + action + '</div>'
|
|
1188
1232
|
: details;
|
|
@@ -1201,15 +1245,8 @@ function retentionScheduleApprovalBlocker() {
|
|
|
1201
1245
|
const document = documents.length === 1 ? documents[0] : null;
|
|
1202
1246
|
if (documents.length > 1) return "Keep exactly one current program Data Retention Schedule document before approval.";
|
|
1203
1247
|
if (!document) return "Add the governing Data Retention Schedule document first.";
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
const documentReadiness = retentionScheduleDocumentReadiness(document.id);
|
|
1208
|
-
if (documentReadiness?.status !== "complete") {
|
|
1209
|
-
return documentReadiness?.message || "Complete the governing document before approving the schedule.";
|
|
1210
|
-
}
|
|
1211
|
-
const planned = resourcesOfType("retention-schedule-item").find(({ record }) => record.status === "planned")?.record;
|
|
1212
|
-
if (planned) return "Complete or retire the planned row: " + planned.title + ".";
|
|
1248
|
+
const approvalIssue = state.collectionReviews?.["retention-schedule-item"]?.approvalIssues?.[0];
|
|
1249
|
+
if (approvalIssue) return approvalIssue.message;
|
|
1213
1250
|
const incomplete = retentionReviewItems("policies", false).find((item) => (
|
|
1214
1251
|
item.status === "action" && item.id !== "collection-review-retention-schedule-item"
|
|
1215
1252
|
));
|
|
@@ -4752,6 +4789,9 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
4752
4789
|
Array.isArray(group) ? { fields: group } : group
|
|
4753
4790
|
));
|
|
4754
4791
|
const oneOf = new Set(oneOfGroups.flatMap((group) => group.fields || []).filter((name) => !name.startsWith("$markdown:")));
|
|
4792
|
+
const hiddenFields = type === "retention-schedule-item" && modelSupports("retention-schedule-approval")
|
|
4793
|
+
? new Set(["approvedByIds", "approvedOn"])
|
|
4794
|
+
: new Set();
|
|
4755
4795
|
const activeOneOf = new Set(oneOfGroups
|
|
4756
4796
|
.filter((group) => !group.when || conditionMatches(record, group.when))
|
|
4757
4797
|
.flatMap((group) => group.fields || []));
|
|
@@ -4765,6 +4805,7 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
4765
4805
|
...oneOf
|
|
4766
4806
|
])].filter((name) => (
|
|
4767
4807
|
!["id", "type"].includes(name)
|
|
4808
|
+
&& !hiddenFields.has(name)
|
|
4768
4809
|
&& fields[name]
|
|
4769
4810
|
&& !(options.occurrenceReview && ["members", "expectedCount", "completedCount", "conclusion"].includes(name))
|
|
4770
4811
|
));
|
package/src/workflow.js
CHANGED
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { modelSupports } from "../model/index.js";
|
|
5
5
|
import { applicabilityReviewIsCurrent } from "./applicability-scope.js";
|
|
6
|
+
import { retentionScheduleIsAuthoritative } from "./collection-review.js";
|
|
6
7
|
import { assessRequiredAppointments } from "./appointments.js";
|
|
7
8
|
import { assessSourceCoverageReadiness } from "./source-coverage.js";
|
|
8
9
|
import { coverageEnd, coverageStart } from "./coverage.js";
|
|
@@ -918,6 +919,7 @@ async function auditLifecycleFindings(loaded, audits, program) {
|
|
|
918
919
|
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
919
920
|
const retentionRules = loaded.resources.filter((record) => record.type === "retention-schedule-item");
|
|
920
921
|
const retentionRevisions = await resourceReviewRevisions(loaded, retentionRules.flatMap((rule) => retentionReviewResourceIds(rule, loaded)));
|
|
922
|
+
const retentionScheduleApproved = retentionScheduleIsAuthoritative(loaded, target);
|
|
921
923
|
const findings = [];
|
|
922
924
|
for (const audit of audits) {
|
|
923
925
|
if (!(audit.controlIds || []).length) {
|
|
@@ -1045,7 +1047,7 @@ async function auditLifecycleFindings(loaded, audits, program) {
|
|
|
1045
1047
|
`${records.length} ${noun} remain open, so this engagement cannot be treated as closed.`
|
|
1046
1048
|
));
|
|
1047
1049
|
}
|
|
1048
|
-
const linkedRetentionCurrent = (audit.retentionScheduleItemIds || []).some((id) => {
|
|
1050
|
+
const linkedRetentionCurrent = retentionScheduleApproved && (audit.retentionScheduleItemIds || []).some((id) => {
|
|
1049
1051
|
const rule = byId.get(id);
|
|
1050
1052
|
return rule?.type === "retention-schedule-item"
|
|
1051
1053
|
&& rule.status === "active"
|