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.
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
+ import { modelSupports } from "../model/index.js";
4
+ import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
3
5
  import {
4
6
  coverageContains,
5
7
  coverageEnd,
@@ -9,11 +11,24 @@ import {
9
11
  coverageStart
10
12
  } from "./coverage.js";
11
13
  import { createResource, createResources, deleteResource, updateResource } from "./files.js";
14
+ import { getChangedDataPathsSinceRevision, getFileAtRevision, hasGitRevision } from "./git.js";
12
15
  import { createResourceId } from "./id.js";
13
16
  import { currentPartyPeople, partiesIndependent } from "./parties.js";
14
17
  import { resolveDataPath } from "./paths.js";
15
18
  import { assessProgramReadiness } from "./program-readiness.js";
16
19
  import { markdownEntries } from "./resource-markdown.js";
20
+ import { contentRevisionBindingsMatch, governedDocumentIsOperating } from "./program-lifecycle.js";
21
+ import {
22
+ auditorWasEngaged,
23
+ missingSoc2References,
24
+ personWasActiveOn,
25
+ recordWasInUseDuringAudit,
26
+ REQUIRED_SOC2_DESCRIPTION_REFERENCES,
27
+ REQUIRED_SOC2_SECURITY_REFERENCES,
28
+ signatoryAppointmentIssue,
29
+ subsequentEventsReviewIssue
30
+ } from "./soc2.js";
31
+ import { currentCalendarDate } from "./time.js";
17
32
  import { loadWorkspace } from "./workspace.js";
18
33
 
19
34
  const NON_EVIDENCE_RECORD_TYPES = new Set([
@@ -39,7 +54,6 @@ const NON_EVIDENCE_RECORD_TYPES = new Set([
39
54
  "vendor",
40
55
  "workspace"
41
56
  ]);
42
-
43
57
  export async function assessAuditPreparation(input, options = {}) {
44
58
  const loaded = input?.resources && input?.model && input?.entries
45
59
  ? input
@@ -55,23 +69,27 @@ export async function assessAuditPreparation(input, options = {}) {
55
69
  if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
56
70
 
57
71
  const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
72
+ asOf: options.asOf,
58
73
  generatedAt: options.generatedAt,
59
74
  programId: audit?.programId
60
75
  });
76
+ const documentActivations = audit && modelSupports(loaded.model, "governed-document-activation")
77
+ ? await auditDocumentActivationAssessments(loaded, audit, byId, programReadiness.asOf)
78
+ : [];
61
79
  const stages = [
62
80
  programFoundationStage(programReadiness, loaded.workspace),
63
81
  engagementStage(audit, byId, programReadiness),
64
- scopeStage(audit, records, byId, programReadiness)
82
+ scopeStage(loaded, audit, records, byId, programReadiness)
65
83
  ];
66
84
  const fieldworkSections = audit
67
85
  ? [
68
- await documentsStage(loaded, audit, byId),
86
+ await documentsStage(loaded, audit, byId, programReadiness.asOf),
69
87
  evidenceStage(audit, records, byId, loaded.model),
70
88
  populationsStage(audit, records, byId, loaded.model)
71
89
  ]
72
90
  : [];
73
91
  stages.push(fieldworkStage(audit, fieldworkSections));
74
- stages.push(auditorStage());
92
+ stages.push(auditorStage(audit, byId, loaded.model.modelVersion));
75
93
 
76
94
  for (const stage of stages) {
77
95
  stage.counts = countStatuses(stage.items);
@@ -97,10 +115,23 @@ export async function assessAuditPreparation(input, options = {}) {
97
115
  && coverageStart(audit.coverage)
98
116
  && coverageEnd(audit.coverage)
99
117
  && initializationNeeded(audit, records, loaded.model)),
118
+ documentActivations,
100
119
  stages
101
120
  };
102
121
  }
103
122
 
123
+ export async function assessAuditDocumentActivations(input = process.cwd(), options = {}) {
124
+ const loaded = input?.resources && input?.model && input?.entries
125
+ ? input
126
+ : await loadWorkspace(input);
127
+ if (!modelSupports(loaded.model, "governed-document-activation")) return [];
128
+ const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
129
+ if (!audit) throw new Error(`Audit "${options.auditId || ""}" was not found.`);
130
+ const byId = new Map(loaded.resources.map((record) => [record.id, record]));
131
+ const asOf = options.asOf || currentCalendarDate(loaded.workspace.timezone);
132
+ return auditDocumentActivationAssessments(loaded, audit, byId, asOf);
133
+ }
134
+
104
135
  export async function prepareAuditWorkspace(input, options = {}) {
105
136
  const loaded = await loadWorkspace(input);
106
137
  const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
@@ -158,7 +189,7 @@ export async function prepareAuditWorkspace(input, options = {}) {
158
189
  const selectedControls = (audit.controlIds || [])
159
190
  .map((id) => loaded.resources.find((record) => record.id === id))
160
191
  .filter(Boolean);
161
- const v4 = String(loaded.model.modelVersion) === "4";
192
+ const v4 = modelSupports(loaded.model, "program-scope");
162
193
  const sourceSystems = loaded.resources.filter((record) => record.type === (v4 ? "component" : "system"));
163
194
  const populations = (audit.auditKind === "soc-2-type-2" ? model.populationTemplates || [] : [])
164
195
  .filter((template) => !existingKinds.has(template.kind))
@@ -212,7 +243,7 @@ export async function prepareAuditWorkspace(input, options = {}) {
212
243
  };
213
244
  }
214
245
 
215
- function scopeStage(audit, records, byId, programReadiness) {
246
+ function scopeStage(loaded, audit, records, byId, programReadiness) {
216
247
  const items = [];
217
248
  if (!audit) {
218
249
  items.push(item(
@@ -259,11 +290,24 @@ function scopeStage(audit, records, byId, programReadiness) {
259
290
  ));
260
291
  }
261
292
 
293
+ const v4 = modelSupports(programReadiness.dataModelVersion, "program-scope");
294
+ const engagementStart = coverageStart(audit.coverage);
295
+ const engagementEnd = coverageEnd(audit.coverage);
296
+ const scopeRevision = assessScopeRevision(loaded, audit, v4);
297
+ items.push(item(
298
+ "scope-revision",
299
+ scopeRevision.status,
300
+ "Bind the reviewed engagement scope",
301
+ scopeRevision.message,
302
+ audit
303
+ ));
262
304
  const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
263
305
  const completeSystems = systems.filter((system) => (
264
- system.status === "active"
265
- && system.description
266
- && system.classificationId
306
+ recordWasInUseDuringAudit(system, engagementStart, engagementEnd)
307
+ && (v4
308
+ ? system.purpose && system.boundary && (system.servicesProvided || []).length
309
+ : system.description)
310
+ && (v4 || system.classificationId)
267
311
  && (system.ownerIds || []).length
268
312
  ));
269
313
  items.push(item(
@@ -271,12 +315,11 @@ function scopeStage(audit, records, byId, programReadiness) {
271
315
  systems.length && completeSystems.length === systems.length ? "complete" : "action",
272
316
  "Define the service boundary",
273
317
  systems.length
274
- ? `${completeSystems.length} of ${systems.length} selected systems are active, explicitly in scope, owned, classified, and described.`
318
+ ? `${completeSystems.length} of ${systems.length} selected systems were in use for the engagement, explicitly in scope, owned, and described${v4 ? "" : ", with a classification"}.`
275
319
  : "Select every in-scope service and supporting system, then describe its owner, environment, data, vendors, and boundary.",
276
320
  systems[0] || { type: "system" }
277
321
  ));
278
322
 
279
- const engagementStart = coverageStart(audit.coverage);
280
323
  const commitments = records.filter((record) => record.type === "commitment"
281
324
  && record.status === "active"
282
325
  && systems.some((system) => (record.systemIds || []).includes(system.id)));
@@ -304,48 +347,121 @@ function scopeStage(audit, records, byId, programReadiness) {
304
347
  const frameworkRequirementIds = records
305
348
  .filter((record) => record.type === "requirement" && (audit.frameworkIds || []).includes(record.frameworkId))
306
349
  .map((record) => record.id);
350
+ const program = audit.programId ? byId.get(audit.programId) : null;
351
+ const v4Decisions = new Map((program?.requirementApplicability || []).map((decision) => [decision.requirementId, decision]));
307
352
  const unresolvedRequirements = frameworkRequirementIds
308
353
  .map((id) => byId.get(id))
309
- .filter((requirement) => (
310
- requirement.applicability === "undetermined"
311
- || (requirement.applicability === "not-applicable" && !requirement.applicabilityRationale)
312
- ));
313
- const applicableRequirementIds = frameworkRequirementIds.filter((id) => byId.get(id)?.applicability === "applicable");
354
+ .filter((requirement) => {
355
+ if (!v4) {
356
+ return requirement.applicability === "undetermined"
357
+ || (requirement.applicability === "not-applicable" && !requirement.applicabilityRationale);
358
+ }
359
+ const decision = v4Decisions.get(requirement.id);
360
+ return !decision
361
+ || decision.decision === "undetermined"
362
+ || (decision.decision === "not-applicable" && !decision.rationale);
363
+ });
364
+ const applicableRequirementIds = frameworkRequirementIds.filter((id) => (
365
+ v4 ? v4Decisions.get(id)?.decision === "applicable" : byId.get(id)?.applicability === "applicable"
366
+ ));
314
367
  const missingApplicableRequirements = applicableRequirementIds.filter((id) => !(audit.requirementIds || []).includes(id));
315
368
  const selectedRequirements = (audit.requirementIds || []).map((id) => byId.get(id)).filter(Boolean);
316
369
  const unexpectedRequirements = selectedRequirements.filter((requirement) => (
317
370
  !(audit.frameworkIds || []).includes(requirement.frameworkId)
318
- || requirement.applicability !== "applicable"
371
+ || (v4
372
+ ? v4Decisions.get(requirement.id)?.decision !== "applicable"
373
+ : requirement.applicability !== "applicable")
319
374
  ));
320
- const descriptionCriteriaSelected = selectedRequirements.some((requirement) => (
321
- (requirement.tags || []).includes("description-criteria")
322
- || /^DC\d+/i.test(requirement.reference || "")
375
+ const selectedControls = (audit.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
376
+ const uncoveredRequirements = selectedRequirements.filter((requirement) => (
377
+ !isDescriptionRequirement(requirement)
378
+ && !selectedControls.some((control) => (control.requirementIds || []).includes(requirement.id))
379
+ ));
380
+ const descriptionRequirements = frameworkRequirementIds
381
+ .map((id) => byId.get(id))
382
+ .filter(isDescriptionRequirement);
383
+ const missingDescriptionRequirements = descriptionRequirements.filter((requirement) => (
384
+ !(audit.requirementIds || []).includes(requirement.id)
323
385
  ));
386
+ const missingRequiredDescriptionReferences = v4
387
+ ? missingSoc2References(descriptionRequirements, REQUIRED_SOC2_DESCRIPTION_REFERENCES)
388
+ : [];
389
+ const invalidMandatoryDescriptionDecisions = v4
390
+ ? descriptionRequirements.filter((requirement) => (
391
+ REQUIRED_SOC2_DESCRIPTION_REFERENCES.includes(String(requirement.reference || "").trim().toUpperCase())
392
+ && v4Decisions.get(requirement.id)?.decision !== "applicable"
393
+ ))
394
+ : [];
395
+ const securityRequirements = frameworkRequirementIds
396
+ .map((id) => byId.get(id))
397
+ .filter(isSecurityRequirement);
398
+ const missingRequiredSecurityReferences = v4
399
+ ? missingSoc2References(securityRequirements, REQUIRED_SOC2_SECURITY_REFERENCES)
400
+ : [];
401
+ const missingSelectedSecurityReferences = v4
402
+ ? missingSoc2References(selectedRequirements.filter(isSecurityRequirement), REQUIRED_SOC2_SECURITY_REFERENCES)
403
+ : [];
404
+ const invalidMandatorySecurityDecisions = v4
405
+ ? securityRequirements.filter((requirement) => (
406
+ REQUIRED_SOC2_SECURITY_REFERENCES.includes(String(requirement.reference || "").trim().toUpperCase())
407
+ && v4Decisions.get(requirement.id)?.decision !== "applicable"
408
+ ))
409
+ : [];
324
410
  const criteriaComplete = (audit.frameworkIds || []).length
325
411
  && (audit.requirementIds || []).length
326
412
  && (audit.controlIds || []).length
327
413
  && !unresolvedRequirements.length
328
414
  && !missingApplicableRequirements.length
329
415
  && !unexpectedRequirements.length
330
- && descriptionCriteriaSelected;
416
+ && !uncoveredRequirements.length
417
+ && descriptionRequirements.length
418
+ && !missingDescriptionRequirements.length
419
+ && !missingRequiredDescriptionReferences.length
420
+ && !missingRequiredSecurityReferences.length
421
+ && !missingSelectedSecurityReferences.length
422
+ && !invalidMandatorySecurityDecisions.length
423
+ && !invalidMandatoryDescriptionDecisions.length;
331
424
  items.push(item(
332
425
  "criteria",
333
426
  criteriaComplete ? "complete" : "action",
334
- "Confirm criteria and controls in scope",
427
+ "Confirm Trust Services criteria, Description Criteria, and Controls",
335
428
  criteriaComplete
336
- ? `${audit.requirementIds.length} applicable criteria and ${audit.controlIds.length} controls are selected, with applicability resolved for the selected frameworks.`
429
+ ? `${selectedRequirements.filter((requirement) => !isDescriptionRequirement(requirement)).length} applicable Trust Services criteria, ${selectedRequirements.filter(isDescriptionRequirement).length} SOC 2 Description Criteria, and ${audit.controlIds.length} Controls are selected. Every applicable Trust Services criterion maps to a selected Control; Description Criteria govern the system description and do not map to Controls.`
337
430
  : unresolvedRequirements.length
338
431
  ? `Resolve applicability and record a rationale for ${unresolvedRequirements.length} selected-framework criteria.`
339
- : missingApplicableRequirements.length
340
- ? `Add ${missingApplicableRequirements.length} applicable selected-framework criteria to the engagement.`
341
- : unexpectedRequirements.length
342
- ? `Remove ${unexpectedRequirements.length} criteria that are not applicable members of the selected frameworks.`
343
- : !descriptionCriteriaSelected
344
- ? "Select the applicable SOC 2 description criteria as well as the Trust Services Criteria."
345
- : "Select the Security criteria, any optional Trust Services Categories, and the controls included in this report.",
346
- audit
432
+ : !descriptionRequirements.length
433
+ ? "Select the SOC 2 Description Criteria framework and all nine criteria. These govern the system description and do not map to Controls."
434
+ : missingRequiredDescriptionReferences.length
435
+ ? `Use the complete SOC 2 Description Criteria set; ${missingRequiredDescriptionReferences.join(", ")} ${missingRequiredDescriptionReferences.length === 1 ? "is" : "are"} missing from the selected framework.`
436
+ : missingRequiredSecurityReferences.length
437
+ ? `Use the complete SOC 2 Security Common Criteria set; ${missingRequiredSecurityReferences.join(", ")} ${missingRequiredSecurityReferences.length === 1 ? "is" : "are"} missing from the selected framework.`
438
+ : invalidMandatoryDescriptionDecisions.length
439
+ ? `Mark all nine Description Criteria applicable in the selected Program; ${invalidMandatoryDescriptionDecisions.map(({ reference }) => reference).join(", ")} ${invalidMandatoryDescriptionDecisions.length === 1 ? "is" : "are"} missing, undetermined, or not applicable.`
440
+ : invalidMandatorySecurityDecisions.length
441
+ ? `Mark every Security Common Criterion applicable in the selected Program; ${invalidMandatorySecurityDecisions.map(({ reference }) => reference).join(", ")} ${invalidMandatorySecurityDecisions.length === 1 ? "is" : "are"} missing, undetermined, or not applicable.`
442
+ : missingSelectedSecurityReferences.length
443
+ ? `Add every Security Common Criterion to the engagement; ${missingSelectedSecurityReferences.join(", ")} ${missingSelectedSecurityReferences.length === 1 ? "is" : "are"} missing.`
444
+ : missingDescriptionRequirements.length
445
+ ? `Add all nine SOC 2 Description Criteria to the engagement; ${missingDescriptionRequirements.length} ${missingDescriptionRequirements.length === 1 ? "is" : "are"} missing.`
446
+ : missingApplicableRequirements.length
447
+ ? `Add ${missingApplicableRequirements.length} applicable selected-framework Trust Services criteria to the engagement.`
448
+ : unexpectedRequirements.length
449
+ ? `Remove ${unexpectedRequirements.length} criteria that are not applicable members of the selected frameworks.`
450
+ : uncoveredRequirements.length
451
+ ? `Map ${uncoveredRequirements.length} selected Trust Services criteria to Controls included in the engagement.`
452
+ : "Select the Security criteria, any optional Trust Services Categories, and the Controls included in this report.",
453
+ audit,
454
+ {
455
+ commands: unresolvedRequirements.length
456
+ ? [
457
+ "npx filegrc review-applicability --scaffold --type requirement > decisions.json",
458
+ "npx filegrc review-applicability decisions.json --preview --json"
459
+ ]
460
+ : [`npx filegrc get ${audit.id} --mutation`]
461
+ }
347
462
  ));
348
463
 
464
+ const treatments = audit.subserviceTreatments || [];
349
465
  const expectedSubserviceVendorIds = new Set(systems.flatMap((system) => system.subserviceVendorIds || []));
350
466
  const missingSubserviceVendorIds = [...expectedSubserviceVendorIds].filter((id) => !(audit.subserviceVendorIds || []).includes(id));
351
467
  const inclusiveSystemIds = records
@@ -355,16 +471,54 @@ function scopeStage(audit, records, byId, programReadiness) {
355
471
  .map((id) => byId.get(id))
356
472
  .filter((control) => (control?.systemIds || []).some((id) => inclusiveSystemIds.includes(id)))
357
473
  .length;
358
- const subserviceComplete = Boolean(audit.subserviceMethod)
359
- && !((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable")
360
- && !missingSubserviceVendorIds.length
361
- && !(audit.subserviceMethod === "inclusive" && (!inclusiveSystemIds.length || !inclusiveControlCount));
474
+ const v4InclusiveTreatments = treatments.filter(({ method }) => method === "inclusive");
475
+ const treatmentComponentCounts = new Map();
476
+ for (const treatment of treatments) {
477
+ for (const componentId of treatment.componentIds || []) {
478
+ treatmentComponentCounts.set(componentId, (treatmentComponentCounts.get(componentId) || 0) + 1);
479
+ }
480
+ }
481
+ const v4InvalidTreatments = treatments.filter((treatment) => (
482
+ byId.get(treatment.vendorId)?.type !== "vendor"
483
+ || !recordWasInUseDuringAudit(byId.get(treatment.vendorId), engagementStart, engagementEnd)
484
+ || !(treatment.componentIds || []).length
485
+ || (treatment.componentIds || []).some((componentId) => {
486
+ const component = byId.get(componentId);
487
+ return component?.type !== "component"
488
+ || !recordWasInUseDuringAudit(component, engagementStart, engagementEnd)
489
+ || component.vendorId !== treatment.vendorId
490
+ || (treatmentComponentCounts.get(componentId) || 0) > 1
491
+ || !(component.systemUses || []).some(({ systemId }) => (audit.systemIds || []).includes(systemId));
492
+ })
493
+ ));
494
+ const v4InclusiveWithoutControls = v4InclusiveTreatments.filter((treatment) => !selectedControls.some((control) => (
495
+ (control.componentIds || []).some((id) => (treatment.componentIds || []).includes(id))
496
+ )));
497
+ const subserviceComplete = v4
498
+ ? Boolean(audit.subserviceConclusion && audit.subserviceConclusionRationale)
499
+ && (audit.subserviceConclusion === "identified" ? treatments.length > 0 : treatments.length === 0)
500
+ && v4InvalidTreatments.length === 0
501
+ && v4InclusiveWithoutControls.length === 0
502
+ : Boolean(audit.subserviceMethod)
503
+ && !((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable")
504
+ && !missingSubserviceVendorIds.length
505
+ && !(audit.subserviceMethod === "inclusive" && (!inclusiveSystemIds.length || !inclusiveControlCount));
362
506
  items.push(item(
363
507
  "subservices",
364
508
  subserviceComplete ? "complete" : "action",
365
509
  "Decide how subservice organizations are presented",
366
510
  subserviceComplete
367
- ? `${displayValue(audit.subserviceMethod)} method selected for ${(audit.subserviceVendorIds || []).length} subservice organizations${audit.subserviceMethod === "inclusive" ? `, with ${inclusiveControlCount} included controls` : ""}.`
511
+ ? v4
512
+ ? audit.subserviceConclusion === "identified"
513
+ ? `${treatments.length} Vendor and Component subservice treatments are recorded with a rationale.`
514
+ : "Management recorded and explained that no subservice organizations are included in the report scope."
515
+ : `${displayValue(audit.subserviceMethod)} method selected for ${(audit.subserviceVendorIds || []).length} subservice organizations${audit.subserviceMethod === "inclusive" ? `, with ${inclusiveControlCount} included controls` : ""}.`
516
+ : v4
517
+ ? v4InvalidTreatments.length
518
+ ? `${v4InvalidTreatments.length} subservice ${v4InvalidTreatments.length === 1 ? "treatment does" : "treatments do"} not identify one Vendor's supplied Components in use within the selected System boundary during the engagement, or ${v4InvalidTreatments.length === 1 ? "repeats" : "repeat"} a Component across treatments.`
519
+ : v4InclusiveWithoutControls.length
520
+ ? `${v4InclusiveWithoutControls.length} inclusive subservice treatments have no selected Controls linked to their Components.`
521
+ : "Record whether subservice organizations are identified, explain the conclusion, and record each Vendor, Component, carve-out or inclusive method, and rationale when applicable."
368
522
  : missingSubserviceVendorIds.length
369
523
  ? `Add ${missingSubserviceVendorIds.length} subservice organizations already identified by the in-scope systems.`
370
524
  : audit.subserviceMethod === "inclusive"
@@ -404,6 +558,175 @@ function scopeStage(audit, records, byId, programReadiness) {
404
558
  return stage("period", "Confirm the Formal Period", "Record the auditor-agreed report type, date or period, scope, criteria, systems, and dependency treatment.", items);
405
559
  }
406
560
 
561
+ function isDescriptionRequirement(requirement) {
562
+ return (requirement.tags || []).includes("description-criteria") || /^DC\d+/i.test(requirement.reference || "");
563
+ }
564
+
565
+ function isSecurityRequirement(requirement) {
566
+ const tags = requirement?.tags || [];
567
+ return tags.includes("security") || tags.includes("common-criteria") || /^CC\d+(?:\.|$)/i.test(requirement?.reference || "");
568
+ }
569
+
570
+ const AUDIT_SCOPE_REVISION_FIELDS = [
571
+ "auditKind",
572
+ "programId",
573
+ "frameworkIds",
574
+ "scope",
575
+ "coverage",
576
+ "systemIds",
577
+ "requirementIds",
578
+ "controlIds",
579
+ "commitmentIds",
580
+ "complementaryControlIds",
581
+ "complementaryControlsConclusion",
582
+ "subserviceConclusion",
583
+ "subserviceConclusionRationale",
584
+ "subserviceTreatments",
585
+ "subserviceVendorIds",
586
+ "subserviceMethod",
587
+ "signatoryAppointmentIds"
588
+ ];
589
+
590
+ function assessScopeRevision(loaded, audit, v4) {
591
+ if (!v4) {
592
+ return {
593
+ status: "complete",
594
+ message: "The legacy workspace stores the engagement selections directly on the Audit."
595
+ };
596
+ }
597
+ if (!audit.scopeRevision) {
598
+ return {
599
+ status: "action",
600
+ message: "Review the engagement's Program, Systems, criteria, Controls, commitments, subservices, complementary controls, and signatories, then record the reviewed Git revision in scopeRevision."
601
+ };
602
+ }
603
+ if (!hasGitRevision(loaded.root, audit.scopeRevision)) {
604
+ return {
605
+ status: "action",
606
+ message: `${audit.scopeRevision} is not an available Git commit. Commit the reviewed scope and record that exact commit in scopeRevision.`
607
+ };
608
+ }
609
+ const entry = loaded.entries.find(({ record }) => record.id === audit.id);
610
+ const source = entry
611
+ ? getFileAtRevision(loaded.root, audit.scopeRevision, `data/${entry.relativePath}`)
612
+ : null;
613
+ let historicalAudit = null;
614
+ try {
615
+ historicalAudit = source ? JSON.parse(source) : null;
616
+ } catch {
617
+ historicalAudit = null;
618
+ }
619
+ if (historicalAudit?.id !== audit.id || historicalAudit.type !== "audit") {
620
+ return {
621
+ status: "action",
622
+ message: `${audit.scopeRevision} does not contain this Audit record. Commit the reviewed scope and update scopeRevision.`
623
+ };
624
+ }
625
+ if (auditScopeFingerprint(historicalAudit) !== auditScopeFingerprint(audit)) {
626
+ return {
627
+ status: "action",
628
+ message: "The current engagement scope differs from the scope stored at scopeRevision. Review the diff, commit the accepted scope, and update scopeRevision."
629
+ };
630
+ }
631
+ const changedScopeRecords = scopeRecordsChangedSinceRevision(loaded, audit, historicalAudit);
632
+ if (changedScopeRecords === null) {
633
+ return {
634
+ status: "action",
635
+ message: "FileGRC could not compare the current engagement scope with scopeRevision. Confirm Git history is available, then review and record the exact scope commit."
636
+ };
637
+ }
638
+ if (changedScopeRecords.length) {
639
+ return {
640
+ status: "action",
641
+ message: `${changedScopeRecords.length} record${changedScopeRecords.length === 1 ? "" : "s"} within the reviewed engagement scope changed after scopeRevision. Review the scope diff, commit the accepted records, and update scopeRevision.`
642
+ };
643
+ }
644
+ return {
645
+ status: "complete",
646
+ message: `Management reviewed the current engagement scope at Git revision ${audit.scopeRevision}.`
647
+ };
648
+ }
649
+
650
+ function scopeRecordsChangedSinceRevision(loaded, audit, historicalAudit) {
651
+ const currentByPath = new Map(loaded.entries.map((entry) => [`data/${entry.relativePath}`, entry.record]));
652
+ const changedPaths = getChangedDataPathsSinceRevision(loaded.root, audit.scopeRevision);
653
+ if (!changedPaths) return null;
654
+ const candidatePaths = changedPaths.filter((path) => path.endsWith(".json"));
655
+ const changed = [];
656
+ for (const path of candidatePaths) {
657
+ const current = currentByPath.get(path) || null;
658
+ let historical = null;
659
+ const historicalSource = getFileAtRevision(loaded.root, audit.scopeRevision, path);
660
+ try {
661
+ historical = historicalSource ? JSON.parse(historicalSource) : null;
662
+ } catch {
663
+ historical = null;
664
+ }
665
+ if (
666
+ !scopeRecordIsRelevant(current, audit)
667
+ && !scopeRecordIsRelevant(historical, historicalAudit)
668
+ ) continue;
669
+ if (JSON.stringify(canonicalScopeValue(current)) !== JSON.stringify(canonicalScopeValue(historical))) changed.push(path);
670
+ }
671
+ return changed;
672
+ }
673
+
674
+ function scopeRecordIsRelevant(record, audit) {
675
+ if (!record || !audit) return false;
676
+ const selectedSystemIds = new Set(audit.systemIds || []);
677
+ const selectedFrameworkIds = new Set(audit.frameworkIds || []);
678
+ const selectedIds = new Set([
679
+ audit.programId,
680
+ ...(audit.frameworkIds || []),
681
+ ...(audit.systemIds || []),
682
+ ...(audit.requirementIds || []),
683
+ ...(audit.controlIds || []),
684
+ ...(audit.commitmentIds || []),
685
+ ...(audit.complementaryControlIds || []),
686
+ ...(audit.signatoryAppointmentIds || []),
687
+ ...auditSubserviceVendorIds(audit),
688
+ ...auditSubserviceComponentIds(audit)
689
+ ].filter(Boolean));
690
+ if (selectedIds.has(record.id)) return true;
691
+ if (record.type === "requirement") return selectedFrameworkIds.has(record.frameworkId);
692
+ if (["commitment", "complementary-control"].includes(record.type)) {
693
+ return (record.systemIds || []).some((id) => selectedSystemIds.has(id));
694
+ }
695
+ if (record.type === "component") {
696
+ return (record.systemUses || []).some(({ systemId }) => selectedSystemIds.has(systemId));
697
+ }
698
+ return false;
699
+ }
700
+
701
+ function auditSubserviceVendorIds(audit) {
702
+ return [
703
+ ...(audit.subserviceVendorIds || []),
704
+ ...(audit.subserviceTreatments || []).map(({ vendorId }) => vendorId)
705
+ ];
706
+ }
707
+
708
+ function auditSubserviceComponentIds(audit) {
709
+ return (audit.subserviceTreatments || []).flatMap(({ componentIds }) => componentIds || []);
710
+ }
711
+
712
+ function auditScopeFingerprint(audit) {
713
+ return JSON.stringify(Object.fromEntries(AUDIT_SCOPE_REVISION_FIELDS.map((field) => (
714
+ [field, canonicalScopeValue(audit[field] ?? null)]
715
+ ))));
716
+ }
717
+
718
+ function canonicalScopeValue(value) {
719
+ if (Array.isArray(value)) {
720
+ return value
721
+ .map(canonicalScopeValue)
722
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
723
+ }
724
+ if (value && typeof value === "object") {
725
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalScopeValue(value[key])]));
726
+ }
727
+ return value;
728
+ }
729
+
407
730
  function programFoundationStage(programReadiness, workspace) {
408
731
  const ready = programReadiness.evidenceReady;
409
732
  return stage("program", "Program Readiness", "The management program can be prepared and operated without an audit record or CPA firm.", [
@@ -434,11 +757,35 @@ function engagementStage(audit, byId, programReadiness) {
434
757
  ]);
435
758
  }
436
759
  const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
437
- const named = Boolean(auditor);
760
+ const named = auditorWasEngaged(auditor, audit);
438
761
  const currentOwners = [...currentPartyPeople(audit.ownerIds, byId)]
439
762
  .map((id) => byId.get(id))
440
763
  .filter(Boolean);
441
- return stage("engagement", "Engage the Auditor", "Record the independent CPA firm and the current management owner who authorizes and coordinates the engagement.", [
764
+ const engagementTerms = audit.engagementTermsDocumentId ? byId.get(audit.engagementTermsDocumentId) : null;
765
+ const engagementTermsComplete = Boolean(
766
+ engagementTerms?.type === "document"
767
+ && engagementTerms.documentKind === "soc2-engagement-terms"
768
+ && governedDocumentIsOperating(
769
+ engagementTerms,
770
+ programReadiness.asOf,
771
+ { modelVersion: programReadiness.dataModelVersion }
772
+ )
773
+ && engagementTerms.approvedOn
774
+ );
775
+ const acknowledgementPeople = (audit.managementAcknowledgedByIds || [])
776
+ .map((id) => byId.get(id))
777
+ .filter((record) => personWasActiveOn(record, audit.managementAcknowledgedOn));
778
+ const acknowledged = Boolean(
779
+ engagementTermsComplete
780
+ && acknowledgementPeople.length === (audit.managementAcknowledgedByIds || []).length
781
+ && acknowledgementPeople.length
782
+ && audit.managementAcknowledgedOn
783
+ && audit.managementAcknowledgedOn >= engagementTerms.approvedOn
784
+ && (!audit.fieldworkStart || audit.managementAcknowledgedOn <= audit.fieldworkStart)
785
+ );
786
+ const preliminary = audit.status === "planned";
787
+ const programGoalAligned = programReadiness.target.goal === audit.auditKind;
788
+ const items = [
442
789
  item(
443
790
  "engagement-record",
444
791
  "complete",
@@ -446,13 +793,24 @@ function engagementStage(audit, byId, programReadiness) {
446
793
  `${audit.title} tracks the formal scope, dates, requests, fieldwork, and report.`,
447
794
  audit
448
795
  ),
796
+ item(
797
+ "program-goal-alignment",
798
+ programGoalAligned ? "complete" : "action",
799
+ "Align the Program goal with the engagement",
800
+ programGoalAligned
801
+ ? `The Program goal and formal engagement are both ${audit.auditKind === "soc-2-type-2" ? "SOC 2 Type 2" : "SOC 2 Type 1"}.`
802
+ : `Change the Program goal from ${programReadiness.target.label} to ${audit.auditKind === "soc-2-type-2" ? "SOC 2 Type 2" : "SOC 2 Type 1"}, or correct the Audit type if the engagement record is wrong.`,
803
+ audit
804
+ ),
449
805
  item(
450
806
  "auditor",
451
807
  named ? "complete" : "action",
452
808
  "Record the independent CPA firm",
453
809
  named
454
810
  ? `${auditor.title} is recorded for the engagement.`
455
- : "Select the CPA firm and record it here. The independent management policy reviewer is a different role.",
811
+ : auditor
812
+ ? `${auditor.title} was not active for the engagement period. Confirm the CPA firm and its engagement dates.`
813
+ : "Select the CPA firm and record it here. The independent management policy reviewer is a different role.",
456
814
  audit
457
815
  ),
458
816
  item(
@@ -470,7 +828,34 @@ function engagementStage(audit, byId, programReadiness) {
470
828
  ]
471
829
  }
472
830
  )
473
- ]);
831
+ ];
832
+ if (modelSupports(programReadiness.dataModelVersion, "guided-workflow")) {
833
+ items.push(
834
+ item(
835
+ "engagement-terms",
836
+ engagementTermsComplete ? "complete" : preliminary ? "later" : "action",
837
+ "Link the engagement terms",
838
+ engagementTermsComplete
839
+ ? `${engagementTerms.title} records the accepted CPA engagement terms.`
840
+ : preliminary
841
+ ? "Link the accepted CPA engagement terms before moving this Audit to in progress."
842
+ : "Link an active, approved Document with documentKind soc2-engagement-terms that contains the accepted CPA engagement terms.",
843
+ engagementTerms || { type: "document" }
844
+ ),
845
+ item(
846
+ "management-acknowledgement",
847
+ acknowledged ? "complete" : preliminary ? "later" : "action",
848
+ "Record management acknowledgement",
849
+ acknowledged
850
+ ? `Management acknowledged the engagement terms on ${audit.managementAcknowledgedOn}.`
851
+ : preliminary
852
+ ? "Name the responsible management people and the acknowledgement date before moving this Audit to in progress."
853
+ : "Name the management people who acknowledged the approved engagement terms and record the actual acknowledgement date on or after approval and no later than fieldwork start. Each person must have been active on that date.",
854
+ audit
855
+ )
856
+ );
857
+ }
858
+ return stage("engagement", "Engage the Auditor", "Record the independent CPA firm and the current management owner who authorizes and coordinates the engagement.", items);
474
859
  }
475
860
 
476
861
  function fieldworkStage(audit, sections) {
@@ -492,48 +877,160 @@ function fieldworkStage(audit, sections) {
492
877
  );
493
878
  }
494
879
 
495
- async function documentsStage(loaded, audit, byId) {
880
+ async function auditDocumentActivationAssessments(loaded, audit, byId, asOf) {
881
+ const links = new Map();
882
+ const addLink = (documentId, role, definition = null) => {
883
+ if (!documentId) return;
884
+ const current = links.get(documentId) || { documentId, roles: [], definitions: [] };
885
+ current.roles.push(role);
886
+ if (definition) current.definitions.push(definition);
887
+ links.set(documentId, current);
888
+ };
889
+ addLink(audit.engagementTermsDocumentId, "engagement-terms");
890
+ for (const definition of applicableManagementDocuments(audit, loaded.model.auditReadiness || {})) {
891
+ addLink(audit[definition.field], definition.field, definition);
892
+ }
893
+ for (const documentId of audit.supplementalDocumentIds || []) addLink(documentId, "supplemental");
894
+
895
+ const assessments = [];
896
+ for (const link of links.values()) {
897
+ const document = byId.get(link.documentId);
898
+ if (document?.type !== "document") continue;
899
+ if (link.roles.every((role) => role === "supplemental") && document.workflowScope !== "engagement") continue;
900
+ const source = await primaryMarkdown(loaded, document);
901
+ const issues = [];
902
+ if (document.workflowScope !== "engagement") issues.push("Set workflowScope to engagement.");
903
+ if (document.template === true) issues.push("Replace the starter template with the completed engagement Document.");
904
+ if (!substantiveMarkdown(source)) issues.push("Complete the Document Markdown.");
905
+ const placeholders = openPlaceholderCount(source);
906
+ if (placeholders) issues.push(`Resolve ${placeholders} open ${placeholders === 1 ? "placeholder" : "placeholders"}.`);
907
+ if (link.roles.includes("engagement-terms") && document.documentKind !== "soc2-engagement-terms") {
908
+ issues.push("Use documentKind soc2-engagement-terms for the accepted engagement terms.");
909
+ }
910
+ for (const definition of link.definitions) {
911
+ issues.push(...await managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId));
912
+ }
913
+ const approvalComplete = Boolean(
914
+ ["approved", "active"].includes(document.status)
915
+ && document.approvedOn
916
+ && document.approvedContentRevisions
917
+ && (document.activationBasis === "legacy-v4"
918
+ ? (document.ownerIds || []).length
919
+ : currentPartyPeople(document.ownerIds || [], byId).size)
920
+ && (document.approverIds || []).length
921
+ && partiesIndependent(document.ownerIds, document.approverIds, byId)
922
+ );
923
+ if (!approvalComplete) issues.push("Complete the independent approval and bind the approved Markdown revision first.");
924
+ if (document.status === "active" && document.activationBasis !== "legacy-v4") {
925
+ if (document.activationBasis !== "recorded") issues.push("Record the Step 5 activation basis.");
926
+ if (!document.activatedOn) issues.push("Record the separate Step 5 activation date.");
927
+ else if (document.activatedOn > asOf) issues.push(`The activation date ${document.activatedOn} is after ${asOf}.`);
928
+ if (!(document.activatedByIds || []).length) issues.push("Name the active Person who performed activation.");
929
+ else {
930
+ const invalidActorIds = document.activatedByIds.filter((id) => !personWasActiveOn(byId.get(id), document.activatedOn));
931
+ if (invalidActorIds.length) issues.push(`Activation actors were not active on ${document.activatedOn}: ${invalidActorIds.join(", ")}.`);
932
+ }
933
+ if (!document.activatedContentRevisions) issues.push("Bind activation to the exact Document Markdown revision.");
934
+ else if (!contentRevisionBindingsMatch(document.approvedContentRevisions, document.activatedContentRevisions)) {
935
+ issues.push("The activated revision must match the unchanged approved revision.");
936
+ }
937
+ if (!document.effectiveOn) issues.push("Record the effective date.");
938
+ else if (document.effectiveOn > asOf) issues.push(`The Document does not become effective until ${document.effectiveOn}.`);
939
+ }
940
+ const operating = governedDocumentIsOperating(document, asOf, loaded.model) && issues.length === 0;
941
+ const state = operating
942
+ ? "active-and-operating"
943
+ : document.status === "active"
944
+ ? "active-with-gaps"
945
+ : document.status === "approved" && issues.length === 0
946
+ ? "ready-to-activate"
947
+ : document.status === "approved"
948
+ ? "approved-not-ready"
949
+ : "approval-pending";
950
+ assessments.push({
951
+ auditId: audit.id,
952
+ documentId: document.id,
953
+ title: document.title,
954
+ roles: [...new Set(link.roles)],
955
+ state,
956
+ label: auditDocumentActivationLabel(state),
957
+ issues: [...new Set(issues)],
958
+ approvedOn: document.approvedOn || null,
959
+ activatedOn: document.activatedOn || null,
960
+ activatedByIds: document.activatedByIds || [],
961
+ effectiveOn: document.effectiveOn || null,
962
+ approvalRevisionBound: Boolean(document.approvedContentRevisions),
963
+ activationRevisionBound: Boolean(document.activatedContentRevisions),
964
+ gapCount: [...new Set(issues)].length
965
+ });
966
+ }
967
+ return assessments.sort((left, right) => left.title.localeCompare(right.title));
968
+ }
969
+
970
+ function auditDocumentActivationLabel(state) {
971
+ return ({
972
+ "approval-pending": "Approval pending in Step 5",
973
+ "approved-not-ready": "Approved, engagement facts incomplete",
974
+ "ready-to-activate": "Ready to activate in Step 5",
975
+ "active-with-gaps": "Active with lifecycle or engagement gaps",
976
+ "active-and-operating": "Active and ready for the engagement"
977
+ })[state] || state;
978
+ }
979
+
980
+ async function managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId) {
981
+ const issues = managementDocumentContentIssues(source, definition, audit);
982
+ if (document.documentKind !== definition.kind) {
983
+ issues.push(`Use documentKind ${definition.kind} for this Audit field.`);
984
+ }
985
+ const engagementEnd = coverageEnd(audit?.coverage);
986
+ if (document.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
987
+ issues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
988
+ }
989
+ if (definition.kind === "soc2-period-completeness" && document.approvedOn) {
990
+ const latestReconciliation = loaded.resources
991
+ .filter((record) => record.type === "audit-population" && record.auditId === audit.id)
992
+ .map((record) => record.reconciledOn)
993
+ .filter(Boolean)
994
+ .sort()
995
+ .at(-1);
996
+ if (latestReconciliation && document.approvedOn < latestReconciliation) {
997
+ issues.push("Approve the period completeness statement after the last population reconciliation.");
998
+ }
999
+ }
1000
+ if (definition.kind === "soc2-management-representation") {
1001
+ const signedEvidence = (document.evidenceIds || [])
1002
+ .map((id) => byId.get(id))
1003
+ .find((record) => (
1004
+ record?.type === "evidence"
1005
+ && record.status === "verified"
1006
+ && record.artifactKind === "signed-record"
1007
+ && record.artifactSubtype === "signed-management-representation"
1008
+ && (record.filePaths || []).length
1009
+ ));
1010
+ if (!signedEvidence) issues.push("Link verified signed-record Evidence with subtype signed-management-representation and a fixed-format copy of the signed letter.");
1011
+ else {
1012
+ const dateIssue = signedRepresentationDateIssue(signedEvidence, audit, loaded.model.modelVersion);
1013
+ if (dateIssue) issues.push(dateIssue);
1014
+ }
1015
+ }
1016
+ return issues;
1017
+ }
1018
+
1019
+ async function documentsStage(loaded, audit, byId, asOf) {
496
1020
  const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
497
1021
  const items = [];
498
1022
  for (const definition of definitions) {
499
1023
  const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
500
1024
  const source = document ? await primaryMarkdown(loaded, document) : "";
501
- const contentIssues = managementDocumentContentIssues(source, definition, audit);
502
- const engagementEnd = coverageEnd(audit?.coverage);
503
- if (document?.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
504
- contentIssues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
505
- }
506
- if (definition.kind === "soc2-period-completeness" && document?.approvedOn && audit) {
507
- const latestReconciliation = loaded.resources
508
- .filter((record) => record.type === "audit-population" && record.auditId === audit.id)
509
- .map((record) => record.reconciledOn)
510
- .filter(Boolean)
511
- .sort()
512
- .at(-1);
513
- if (latestReconciliation && document.approvedOn < latestReconciliation) {
514
- contentIssues.push("Approve the period completeness statement after the last population reconciliation.");
515
- }
516
- }
517
- if (definition.kind === "soc2-management-representation" && document) {
518
- const signedEvidence = (document.evidenceIds || [])
519
- .map((id) => byId.get(id))
520
- .find((record) => (
521
- record?.type === "evidence"
522
- && record.status === "verified"
523
- && (record.filePaths || []).length
524
- ));
525
- if (!signedEvidence) contentIssues.push("Link a verified fixed-format copy of the signed representation letter as evidence.");
526
- else if (!signedEvidence.collectedOn || (engagementEnd && signedEvidence.collectedOn < engagementEnd)) {
527
- contentIssues.push(`The signed representation must be dated on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
528
- }
529
- }
1025
+ const contentIssues = document
1026
+ ? await managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId)
1027
+ : [];
530
1028
  const complete = Boolean(
531
1029
  document
532
1030
  && document.type === "document"
533
1031
  && document.template !== true
534
- && document.status === "active"
1032
+ && governedDocumentIsOperating(document, asOf, loaded.model)
535
1033
  && document.approvedOn
536
- && document.effectiveOn
537
1034
  && (document.ownerIds || []).length
538
1035
  && (document.approverIds || []).length
539
1036
  && partiesIndependent(document.ownerIds, document.approverIds, byId)
@@ -542,13 +1039,17 @@ async function documentsStage(loaded, audit, byId) {
542
1039
  );
543
1040
  const representationLater = definition.kind === "soc2-management-representation"
544
1041
  && audit
545
- && !["fieldwork", "complete"].includes(audit.status);
1042
+ && ["planned", "in-progress", "fieldwork"].includes(audit.status);
546
1043
  items.push(item(
547
1044
  definition.kind,
548
1045
  complete ? "complete" : representationLater ? "later" : "action",
549
1046
  definition.title,
550
1047
  complete
551
- ? "Linked Markdown is complete, active, approved, and effective."
1048
+ ? document.activationBasis === "legacy-v4"
1049
+ ? "Linked Markdown is complete and effective. Its active state is preserved from model v4, which did not record approval and activation as separate events."
1050
+ : modelSupports(loaded.model, "governed-document-activation")
1051
+ ? "Linked Markdown is complete, independently approved, separately activated by a named Person, revision-bound at both events, and effective."
1052
+ : "Linked Markdown is complete, active, approved, and effective."
552
1053
  : document
553
1054
  ? `${definition.timing} ${contentIssues[0] || "Complete and approve the engagement-specific document."}`
554
1055
  : `Link the starter ${definition.title.toLowerCase()} to this audit. ${definition.timing}`,
@@ -558,6 +1059,24 @@ async function documentsStage(loaded, audit, byId) {
558
1059
  return stage("documents", "Management Documents", "Prepare management's description, assertions, completeness work, and closing representations.", items);
559
1060
  }
560
1061
 
1062
+ export function signedRepresentationDateIssue(evidence, audit, modelVersion) {
1063
+ const engagementEnd = coverageEnd(audit?.coverage);
1064
+ const timingMessage = `The signed representation must be dated on or after the engagement ${audit?.auditKind === "soc-2-type-1" ? "date" : "period end"}.`;
1065
+ if (!modelSupports(modelVersion, "program-scope")) {
1066
+ return !evidence?.collectedOn || (engagementEnd && evidence.collectedOn < engagementEnd)
1067
+ ? timingMessage
1068
+ : null;
1069
+ }
1070
+ const signedOn = evidence?.businessEventAt?.slice(0, 10);
1071
+ if (!signedOn) {
1072
+ return "Record the letter's actual signing timestamp in the signed Evidence businessEventAt field. Collection time does not prove when management made the representations.";
1073
+ }
1074
+ if (audit?.reportDate && signedOn !== audit.reportDate) {
1075
+ return `The written representations are dated ${signedOn}; AT-C 205 requires them to be dated as of the practitioner's report date, ${audit.reportDate}.`;
1076
+ }
1077
+ return engagementEnd && signedOn < engagementEnd ? timingMessage : null;
1078
+ }
1079
+
561
1080
  function evidenceStage(audit, records, byId, model) {
562
1081
  const controls = (audit?.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
563
1082
  const evidence = records.filter((record) => record.type === "evidence");
@@ -617,7 +1136,7 @@ function evidenceStage(audit, records, byId, model) {
617
1136
  externalEvidence[0] || { type: "evidence" }
618
1137
  )
619
1138
  ];
620
- const v4 = String(model.modelVersion) === "4";
1139
+ const v4 = modelSupports(model, "program-scope");
621
1140
  const systems = records.filter((record) => record.type === (v4 ? "component" : "system") && record.status === "active");
622
1141
  const sourceId = (record) => v4 ? record.sourceComponentId : record.sourceSystemId;
623
1142
  for (const source of model.evidenceSourceFamilies || []) {
@@ -710,8 +1229,35 @@ function populationsStage(audit, records, byId, model) {
710
1229
  );
711
1230
  }
712
1231
 
713
- function auditorStage() {
1232
+ function auditorStage(audit, byId, modelVersion) {
1233
+ const subsequentEventsIssue = audit ? subsequentEventsReviewIssue(audit) : null;
1234
+ const subsequentEventsStatus = !audit || ["planned", "in-progress", "fieldwork"].includes(audit.status)
1235
+ ? "later"
1236
+ : subsequentEventsIssue ? "action" : "complete";
1237
+ const signatoryIssue = audit && ["report-draft", "issued", "delivered", "complete"].includes(audit.status)
1238
+ ? signatoryAppointmentIssue(audit, byId)
1239
+ : null;
1240
+ const signatoryStatus = !audit || ["planned", "in-progress", "fieldwork"].includes(audit.status)
1241
+ ? "later"
1242
+ : signatoryIssue ? "action" : "complete";
1243
+ const managementItems = modelSupports(modelVersion, "program-scope") ? [
1244
+ item(
1245
+ "subsequent-events",
1246
+ subsequentEventsStatus,
1247
+ "Review subsequent events through the report date",
1248
+ subsequentEventsIssue?.message || "Management recorded the subsequent-events review through the CPA report date.",
1249
+ audit || { type: "audit" }
1250
+ ),
1251
+ item(
1252
+ "signatory-authority",
1253
+ signatoryStatus,
1254
+ "Confirm signatory authority",
1255
+ signatoryIssue?.message || "The linked Appointments establish the signers' authority on the CPA report date.",
1256
+ audit || { type: "audit" }
1257
+ )
1258
+ ] : [];
714
1259
  return stage("auditor", "Fieldwork and Report", "filegrc prepares the record set but does not make the CPA firm's independent judgments.", [
1260
+ ...managementItems,
715
1261
  item("firm-eligibility", "external", "Firm eligibility and independence", "Confirm directly with the engagement partner that the firm and signing practitioner meet applicable licensing, peer-review, ethics, and independence requirements. Keep the signed engagement terms with the audit record if management needs a copy."),
716
1262
  item("sampling", "external", "Sample selection and independent testing", "The auditor chooses samples, performs tests, evaluates exceptions, and decides whether more work is needed."),
717
1263
  item("report", "external", "Report and opinion", "Management reviews and signs its representations. The auditor issues the final report and opinion."),
@@ -804,8 +1350,9 @@ async function primaryMarkdown(loaded, record) {
804
1350
  if (!item) return "";
805
1351
  try {
806
1352
  return await readFile(resolveDataPath(loaded.root, item.path), "utf8");
807
- } catch {
808
- return "";
1353
+ } catch (error) {
1354
+ if (error.code === "ENOENT") return "";
1355
+ throw error;
809
1356
  }
810
1357
  }
811
1358
 
@@ -988,7 +1535,13 @@ function materializeManagementMarkdown(source, audit, records) {
988
1535
  .replaceAll("[selected categories]", categories || "[selected categories]")
989
1536
  .replaceAll(
990
1537
  "[Carve-out, inclusive, or not applicable]",
991
- audit.subserviceMethod ? displayValue(audit.subserviceMethod) : "[Carve-out, inclusive, or not applicable]"
1538
+ audit.subserviceConclusion
1539
+ ? audit.subserviceConclusion === "not-applicable"
1540
+ ? `Not applicable: ${audit.subserviceConclusionRationale || "[explain the conclusion]"}`
1541
+ : `${[...new Set((audit.subserviceTreatments || []).map(({ method }) => displayValue(method)))].join(" and ") || "[record subservice treatments]"}: ${audit.subserviceConclusionRationale || "[explain the conclusion]"}`
1542
+ : audit.subserviceMethod
1543
+ ? displayValue(audit.subserviceMethod)
1544
+ : "[Carve-out, inclusive, or not applicable]"
992
1545
  );
993
1546
  }
994
1547
 
@@ -1010,14 +1563,15 @@ function stage(id, title, description, items) {
1010
1563
  return { id, title, description, items };
1011
1564
  }
1012
1565
 
1013
- function item(id, status, title, message, resource = {}) {
1566
+ function item(id, status, title, message, resource = {}, options = {}) {
1014
1567
  return {
1015
1568
  id,
1016
1569
  status,
1017
1570
  title,
1018
1571
  message,
1019
1572
  ...(resource.type ? { resourceType: resource.type } : {}),
1020
- ...(resource.id ? { resourceId: resource.id } : {})
1573
+ ...(resource.id ? { resourceId: resource.id } : {}),
1574
+ ...(options.commands?.length ? { commands: options.commands } : {})
1021
1575
  };
1022
1576
  }
1023
1577