filegrc 0.7.0 → 0.8.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
@@ -1,6 +1,7 @@
1
1
  import { cp, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { modelSupports } from "../model/index.js";
4
5
  import { assessRequiredAppointments } from "./appointments.js";
5
6
  import { assessSourceCoverageReadiness } from "./source-coverage.js";
6
7
  import { coverageEnd, coverageStart } from "./coverage.js";
@@ -21,6 +22,7 @@ import {
21
22
  import { assessAuditPreparation } from "./audit-preparation.js";
22
23
  import { assessProgramReadiness } from "./program-readiness.js";
23
24
  import { resolveProgram } from "./program.js";
25
+ import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
24
26
  import { planReconciliation } from "./reconciliation.js";
25
27
  import { currentCalendarDate } from "./time.js";
26
28
  import { measureTiming } from "./timing.js";
@@ -92,7 +94,7 @@ async function assessWorkflowUnmeasured(input, options = {}) {
92
94
  if (options.auditId && selectedAuditRecords[0]?.programId && selectedAuditRecords[0].programId !== programRecord.id) {
93
95
  throw new Error(`Audit "${options.auditId}" belongs to Program "${selectedAuditRecords[0].programId}", not "${programRecord.id}".`);
94
96
  }
95
- const audits = String(loaded.model.modelVersion) === "4"
97
+ const audits = modelSupports(loaded.model, "program-scope")
96
98
  ? selectedAuditRecords.filter(({ programId }) => programId === programRecord.id)
97
99
  : selectedAuditRecords;
98
100
  const timezone = options.timezone || workspace?.timezone || "UTC";
@@ -304,15 +306,24 @@ function programFindings(program) {
304
306
  }
305
307
 
306
308
  function auditFindings(preparation) {
307
- return preparation.stages.flatMap((stage) => stage.items.map((item) => normalizeFinding(
308
- `audit.${preparation.audit?.id || "unscoped"}.${stage.id}.${item.id}`,
309
- item,
310
- {
311
- assessment: stage.id === "auditor" ? "audit-closure" : "audit-readiness",
312
- stage: stage.id,
313
- auditId: preparation.audit?.id || null
314
- }
315
- )));
309
+ return preparation.stages.flatMap((stage) => stage.items.map((item) => {
310
+ const finding = normalizeFinding(
311
+ `audit.${preparation.audit?.id || "unscoped"}.${stage.id}.${item.id}`,
312
+ item,
313
+ {
314
+ assessment: stage.id === "auditor" ? "audit-closure" : "audit-readiness",
315
+ stage: stage.id,
316
+ auditId: preparation.audit?.id || null
317
+ }
318
+ );
319
+ if (
320
+ finding.state === "ready"
321
+ && !finding.actions.length
322
+ && finding.subject?.type === "audit"
323
+ && preparation.audit
324
+ ) finding.actions = [mutationAction(preparation.audit)];
325
+ return finding;
326
+ }));
316
327
  }
317
328
 
318
329
  function validationFindings(validation, loaded) {
@@ -623,7 +634,7 @@ function appointmentFinding(kind, template, record, state, requiredness) {
623
634
  }
624
635
 
625
636
  function sourceCoverageFindings(loaded, program) {
626
- const selected = String(loaded.model.modelVersion) === "4"
637
+ const selected = modelSupports(loaded.model, "program-scope")
627
638
  ? new Set(program?.controlIds || [])
628
639
  : null;
629
640
  const selectedControlIds = loaded.resources
@@ -870,8 +881,9 @@ async function assessPeriodHealth(loaded, options) {
870
881
  }
871
882
 
872
883
  function auditLifecycleFindings(loaded, audits, program) {
873
- if (!["3", "4"].includes(String(loaded.model.modelVersion))) return [];
884
+ if (!modelSupports(loaded.model, "guided-workflow")) return [];
874
885
  const target = program || resolveProgram(loaded);
886
+ const byId = new Map(loaded.resources.map((record) => [record.id, record]));
875
887
  const findings = [];
876
888
  for (const audit of audits) {
877
889
  if (!(audit.controlIds || []).length) {
@@ -917,45 +929,54 @@ function auditLifecycleFindings(loaded, audits, program) {
917
929
  ));
918
930
  }
919
931
  }
920
- if (!["planning", "draft"].includes(audit.status)) {
921
- if (!audit.engagementTermsDocumentId) {
922
- findings.push(auditLifecycleFinding(
923
- audit,
924
- "engagement-terms",
925
- "audit-readiness",
926
- "Link the engagement terms",
927
- "Record the CPA firm's engagement terms without representing its professional judgments as management facts."
928
- ));
929
- }
930
- if (!(audit.managementAcknowledgedByIds || []).length || !audit.managementAcknowledgedOn) {
931
- findings.push(auditLifecycleFinding(
932
- audit,
933
- "management-acknowledgement",
934
- "audit-readiness",
935
- "Record management acknowledgement",
936
- "Name who acknowledged the engagement terms and the actual acknowledgement date."
937
- ));
938
- }
939
- }
940
932
  const lateStage = ["report-draft", "issued", "delivered", "complete"].includes(audit.status);
941
- if (lateStage && !audit.subsequentEventsReview) {
933
+ const subsequentEventsIssue = lateStage ? subsequentEventsReviewIssue(audit) : null;
934
+ if (subsequentEventsIssue) {
942
935
  findings.push(auditLifecycleFinding(
943
936
  audit,
944
937
  "subsequent-events",
945
938
  "audit-readiness",
946
939
  "Complete the subsequent-events review",
947
- "Review incidents, changes, findings, subservice coverage, representations, and system-description disclosures through the report date."
940
+ subsequentEventsIssue.message
948
941
  ));
949
942
  }
950
- if (["fieldwork", "report-draft", "issued", "delivered", "complete"].includes(audit.status) && !audit.packetDelivery) {
943
+ const signatoryIssue = modelSupports(loaded.model, "program-scope") && lateStage
944
+ ? signatoryAppointmentIssue(audit, byId)
945
+ : null;
946
+ if (signatoryIssue) {
947
+ findings.push(auditLifecycleFinding(
948
+ audit,
949
+ "signatory-authority",
950
+ "audit-readiness",
951
+ "Confirm authorized signatories",
952
+ signatoryIssue.message
953
+ ));
954
+ }
955
+ const packetIssue = ["fieldwork", "report-draft", "issued", "delivered", "complete"].includes(audit.status)
956
+ ? packetDeliveryIssue(audit.packetDelivery)
957
+ : null;
958
+ if (packetIssue) {
951
959
  findings.push(auditLifecycleFinding(
952
960
  audit,
953
961
  "packet-delivery",
954
962
  "delivery-readiness",
955
963
  "Approve and record packet delivery",
956
- "Record the least-disclosure review, redaction decision, recipient, delivery system, exact packet revision and manifest, management approval, delivery date, and receipt."
964
+ packetIssue
957
965
  ));
958
966
  }
967
+ if (["issued", "delivered", "complete"].includes(audit.status)) {
968
+ const reportEvidence = loaded.resources.find((record) => record.id === audit.reportEvidenceId);
969
+ const reportIssue = soc2ReportEvidenceIssue(reportEvidence, audit, loaded.model.modelVersion);
970
+ if (reportIssue) {
971
+ findings.push(auditLifecycleFinding(
972
+ audit,
973
+ "report-evidence",
974
+ "audit-closure",
975
+ "Link the exact issued SOC 2 report",
976
+ reportIssue.message
977
+ ));
978
+ }
979
+ }
959
980
  if (audit.status !== "complete") {
960
981
  const nextStep = auditClosureNextStep(audit.status);
961
982
  findings.push(auditLifecycleFinding(
@@ -991,10 +1012,8 @@ function auditLifecycleFindings(loaded, audits, program) {
991
1012
  ));
992
1013
  }
993
1014
  for (const [field, title, message] of [
994
- ["reportEvidenceId", "Link the issued report", "Link the exact issued report evidence and its coverage."],
995
1015
  ["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."],
996
- ["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."],
997
- ["signatoryAppointmentIds", "Confirm authorized signatories", "Link active authority Appointments for management assertion and representation signers."]
1016
+ ["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
998
1017
  ]) {
999
1018
  if (field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
1000
1019
  findings.push(auditLifecycleFinding(audit, field, "audit-closure", title, message));
@@ -1003,6 +1022,41 @@ function auditLifecycleFindings(loaded, audits, program) {
1003
1022
  return findings;
1004
1023
  }
1005
1024
 
1025
+ export function packetDeliveryIssue(delivery) {
1026
+ if (!delivery) {
1027
+ return "Record the least-disclosure review, redaction decision, recipient, delivery system, exact packet revision and manifest, management approval, delivery date, and receipt.";
1028
+ }
1029
+ if (!(delivery.classificationReviewedByIds || []).length || !(delivery.approvedByIds || []).length) {
1030
+ return "Name the people who performed the least-disclosure review and approved the delivery.";
1031
+ }
1032
+ for (const [field, label] of [
1033
+ ["redactionDecision", "redaction decision"],
1034
+ ["recipient", "recipient"],
1035
+ ["deliverySystem", "delivery system"],
1036
+ ["packetCommit", "packet Git revision"],
1037
+ ["manifestChecksum", "manifest checksum"],
1038
+ ["receiptReference", "delivery receipt reference"]
1039
+ ]) {
1040
+ if (!String(delivery[field] || "").trim()) return `Record the ${label} for the delivered packet.`;
1041
+ }
1042
+ if (!/^[a-f0-9]{40}$/i.test(delivery.packetCommit)) {
1043
+ return "Record the delivered packet's exact 40-character Git commit.";
1044
+ }
1045
+ if (!/^(?:sha256:)?[a-f0-9]{64}$/i.test(delivery.manifestChecksum)) {
1046
+ return "Record the packet manifest checksum as a SHA-256 digest.";
1047
+ }
1048
+ if (
1049
+ !delivery.classificationReviewedOn
1050
+ || !delivery.approvedOn
1051
+ || !delivery.deliveredOn
1052
+ || delivery.classificationReviewedOn > delivery.approvedOn
1053
+ || delivery.approvedOn > delivery.deliveredOn
1054
+ ) {
1055
+ return "Record chronological least-disclosure review, management approval, and delivery dates.";
1056
+ }
1057
+ return null;
1058
+ }
1059
+
1006
1060
  function normalizeFinding(code, item, context) {
1007
1061
  const state = findingState(item.status);
1008
1062
  const subject = item.resourceType
@@ -1186,6 +1240,7 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
1186
1240
  && !findings.some((finding) => finding.assessment === "audit-closure" && blockingFinding(finding));
1187
1241
  const deliveryFindingKeys = findingKeys(findings, "delivery-readiness");
1188
1242
  const policyActivations = program.policyActivations || [];
1243
+ const documentActivations = program.documentActivations || [];
1189
1244
  const policiesOperating = policyActivations.length > 0
1190
1245
  && policyActivations.every(({ state }) => state === "active-and-operating");
1191
1246
  return {
@@ -1216,6 +1271,18 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
1216
1271
  findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".policy-activation-")),
1217
1272
  policies: policyActivations
1218
1273
  },
1274
+ documentActivation: {
1275
+ status: documentActivations.length && documentActivations.every(({ state }) => state === "active-and-operating")
1276
+ ? "complete"
1277
+ : documentActivations.length ? "needs-work" : "not-started",
1278
+ message: documentActivations.length && documentActivations.every(({ state }) => state === "active-and-operating")
1279
+ ? "Every required governed Document is separately approved, activated, effective, and operating."
1280
+ : documentActivations.length
1281
+ ? "Approve required plans and schedules in Step 2, implement their linked requirements, then activate their exact revisions in Step 3."
1282
+ : "No required governed Document activation is configured.",
1283
+ findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".document-")),
1284
+ documents: documentActivations
1285
+ },
1219
1286
  policyLibraryReview: {
1220
1287
  status: program.policyLibraryProposals?.length ? "review" : "current",
1221
1288
  message: program.policyLibraryProposals?.length
@@ -1451,14 +1518,10 @@ function periodFinding(key, title, message, state, subject, actions, dependencie
1451
1518
 
1452
1519
  function auditClosureNextStep(status) {
1453
1520
  const steps = {
1454
- planning: {
1521
+ planned: {
1455
1522
  title: "Confirm the engagement and start audit preparation",
1456
1523
  message: "Link the agreed engagement terms, confirm scope and management acknowledgement, then move the audit to in progress."
1457
1524
  },
1458
- draft: {
1459
- title: "Finalize the draft engagement",
1460
- message: "Resolve the draft scope and ownership, link the agreed engagement terms, then move the audit to in progress."
1461
- },
1462
1525
  "in-progress": {
1463
1526
  title: "Begin fieldwork",
1464
1527
  message: "Finish management preparation, confirm the fieldwork dates, and move the audit to fieldwork when the CPA firm begins testing."
package/src/workspace.js CHANGED
@@ -2,8 +2,13 @@ import { readFile, readdir } from "node:fs/promises";
2
2
  import { join, relative, sep } from "node:path";
3
3
  import { loadModel } from "../model/index.js";
4
4
  import { resolveWorkspaceRoot } from "./paths.js";
5
+ import { measureTiming } from "./timing.js";
5
6
 
6
7
  export async function loadWorkspace(input = process.cwd()) {
8
+ return measureTiming("workspace-load", () => loadWorkspaceUnmeasured(input));
9
+ }
10
+
11
+ async function loadWorkspaceUnmeasured(input) {
7
12
  const root = resolveWorkspaceRoot(input);
8
13
  const dataRoot = join(root, "data");
9
14
  const diagnostics = [];