filegrc 0.10.0 → 0.11.0

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/workflow.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  import { assessAuditPreparation } from "./audit-preparation.js";
24
24
  import { assessProgramReadiness } from "./program-readiness.js";
25
25
  import { resolveProgram } from "./program.js";
26
+ import { resourceReviewRevisions, retentionReviewResourceIds, retentionRuleIsCurrent } from "./retention.js";
26
27
  import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
27
28
  import { planReconciliation } from "./reconciliation.js";
28
29
  import { currentCalendarDate } from "./time.js";
@@ -141,10 +142,10 @@ async function assessWorkflowUnmeasured(input, options = {}) {
141
142
  ...programFindings(program),
142
143
  ...Object.values(auditPreparations).flatMap(auditFindings),
143
144
  ...appointmentTemplateFindings(loaded),
144
- ...sourceCoverageFindings(loaded, programRecord),
145
+ ...await sourceCoverageFindings(loaded, programRecord),
145
146
  ...reconciliationFindings(reconciliation),
146
147
  ...periodFindings,
147
- ...auditLifecycleFindings(loaded, audits, programRecord)
148
+ ...await auditLifecycleFindings(loaded, audits, programRecord)
148
149
  ];
149
150
  const findings = [
150
151
  ...validationFindings(validation, loaded),
@@ -638,7 +639,7 @@ function appointmentFinding(kind, template, record, state, requiredness) {
638
639
  };
639
640
  }
640
641
 
641
- function sourceCoverageFindings(loaded, program) {
642
+ async function sourceCoverageFindings(loaded, program) {
642
643
  const selected = modelSupports(loaded.model, "program-scope")
643
644
  ? new Set(program?.controlIds || [])
644
645
  : null;
@@ -650,7 +651,7 @@ function sourceCoverageFindings(loaded, program) {
650
651
  && (!selected || selected.has(record.id))
651
652
  ))
652
653
  .map(({ id }) => id);
653
- return assessSourceCoverageReadiness(loaded, selectedControlIds, program)
654
+ return (await assessSourceCoverageReadiness(loaded, selectedControlIds, program))
654
655
  .map(({ family, record, complete }) => {
655
656
  const state = complete ? "complete" : "ready";
656
657
  const code = `evidence-source.${family.id}.coverage`;
@@ -885,10 +886,12 @@ async function assessPeriodHealth(loaded, options) {
885
886
  return findings;
886
887
  }
887
888
 
888
- function auditLifecycleFindings(loaded, audits, program) {
889
+ async function auditLifecycleFindings(loaded, audits, program) {
889
890
  if (!modelSupports(loaded.model, "guided-workflow")) return [];
890
891
  const target = program || resolveProgram(loaded);
891
892
  const byId = new Map(loaded.resources.map((record) => [record.id, record]));
893
+ const retentionRules = loaded.resources.filter((record) => record.type === "retention-schedule-item");
894
+ const retentionRevisions = await resourceReviewRevisions(loaded, retentionRules.flatMap((rule) => retentionReviewResourceIds(rule, loaded)));
892
895
  const findings = [];
893
896
  for (const audit of audits) {
894
897
  if (!(audit.controlIds || []).length) {
@@ -1016,11 +1019,21 @@ function auditLifecycleFindings(loaded, audits, program) {
1016
1019
  `${records.length} ${noun} remain open, so this engagement cannot be treated as closed.`
1017
1020
  ));
1018
1021
  }
1022
+ const linkedRetentionCurrent = (audit.retentionScheduleItemIds || []).some((id) => {
1023
+ const rule = byId.get(id);
1024
+ return rule?.type === "retention-schedule-item"
1025
+ && rule.status === "active"
1026
+ && (rule.scopeResourceIds || []).some((scopeId) => scopeId === audit.id || scopeId === target?.id)
1027
+ && retentionRuleIsCurrent(rule, retentionRevisions, byId, loaded);
1028
+ });
1029
+ const retentionClosure = loaded.model.resources.audit?.fields?.retentionScheduleItemIds
1030
+ ? ["retentionScheduleItemIds", "Link the audit retention schedule", "Link an active, current retention schedule item scoped to this Audit or its Program."]
1031
+ : ["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."];
1019
1032
  for (const [field, title, message] of [
1020
- ["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."],
1033
+ retentionClosure,
1021
1034
  ["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
1022
1035
  ]) {
1023
- if (field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
1036
+ if (field === "retentionScheduleItemIds" ? linkedRetentionCurrent : field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
1024
1037
  findings.push(auditLifecycleFinding(audit, field, "audit-closure", title, message));
1025
1038
  }
1026
1039
  }
@@ -1082,6 +1095,12 @@ function normalizeFinding(code, item, context) {
1082
1095
  severity: findingSeverity(state, context.assessment),
1083
1096
  title: item.title,
1084
1097
  message: item.message,
1098
+ ...(item.resourceId ? { resourceId: item.resourceId } : {}),
1099
+ ...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
1100
+ ...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),
1101
+ ...(item.informationTypeId ? { informationTypeId: item.informationTypeId } : {}),
1102
+ ...(item.retentionScheduleItemIds ? { retentionScheduleItemIds: item.retentionScheduleItemIds } : {}),
1103
+ ...(item.staleResourceIds ? { staleResourceIds: item.staleResourceIds } : {}),
1085
1104
  ...(subject ? { subject } : {}),
1086
1105
  dependencies,
1087
1106
  actions: (item.commands || []).map((command) => ({