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/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 activeRows = rows.filter(({ record }) => record.status === "active").length;
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
- 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(displayStatus(record), "status", record.type, true) + '</td><td data-label="Next action">' + recordWorkflowCell("retention-schedule-item", entry, "?stage=policies") + '</td></tr>';
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 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><label class="history-toggle"><input type="checkbox" data-retention-history ' + (showHistory ? "checked" : "") + '> Show retired</label></div>';
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>Define the retention rules management has approved. Step 3 checks whether implemented systems and evidence sources follow them.</p></div><div class="actions">' + documentAction + add + '</div></div>' + resourceGuide("retention-schedule-item") +
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>' + activeRows + ' active ' + pluralize("row", activeRows) + '</strong><small>Document and rows are approved together</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>' +
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 approval are complete.",
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
- let visible = 0;
726
- table.querySelectorAll("[data-retention-row]").forEach((row) => {
727
- const show = (historyToggle.checked || row.dataset.history !== "true")
728
- && (!query || row.textContent.toLowerCase().includes(query));
729
- row.hidden = !show;
730
- if (show) visible += 1;
731
- });
732
- noResults.hidden = visible !== 0 || total === 0;
733
- count.textContent = visible + " of " + total + " " + pluralize("row", total);
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
- return state.programReadiness?.stages
763
- ?.find(({ id }) => id === "policies")
764
- ?.items.find(({ id }) => id === "document-approval-" + documentId) || null;
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 && !scheduleBlocker
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(scheduleBlocker || assessment.message) + '</span></p>';
1174
- const compactReviewSummary = current && !scheduleBlocker
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
- : scheduleBlocker
1180
- ? '<button class="button" type="button" disabled title="' + esc(scheduleBlocker) + '">Review and approve schedule</button>'
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 = scheduleBlocker ? "Not ready" : current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required";
1185
- 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><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>' + (current ? reviewSummary : "") + '</div></details>';
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
- if (!["approved", "active"].includes(document.status) || !document.approvedOn || !document.approvedContentRevisions) {
1205
- return "Approve the governing document and bind its exact Markdown revision first.";
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"