filegrc 0.1.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.
@@ -0,0 +1,906 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createResource, createResources, deleteResource, updateResource } from "./files.js";
4
+ import { createResourceId } from "./id.js";
5
+ import { resolveDataPath } from "./paths.js";
6
+ import { markdownEntries } from "./resource-markdown.js";
7
+ import { loadWorkspace } from "./workspace.js";
8
+
9
+ export async function assessAuditPreparation(input, options = {}) {
10
+ const loaded = input?.resources && input?.model && input?.entries
11
+ ? input
12
+ : await loadWorkspace(input);
13
+ const records = loaded.resources;
14
+ const byId = new Map(records.map((record) => [record.id, record]));
15
+ const audits = records.filter((record) => record.type === "audit");
16
+ const audit = options.auditId
17
+ ? audits.find((record) => record.id === options.auditId)
18
+ : options.selectDefault === false
19
+ ? null
20
+ : audits.find((record) => !["complete", "closed", "canceled"].includes(record.status)) || audits[0];
21
+ if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
22
+
23
+ const stages = [];
24
+ stages.push(scopeStage(audit, records, byId));
25
+ stages.push(programStage(audit, records, byId));
26
+ stages.push(await documentsStage(loaded, audit, byId));
27
+ stages.push(evidenceStage(audit, records, byId, loaded.model));
28
+ stages.push(populationsStage(audit, records, byId, loaded.model));
29
+ stages.push(auditorStage(loaded.model));
30
+
31
+ for (const stage of stages) {
32
+ stage.counts = countStatuses(stage.items);
33
+ stage.status = stage.counts.action ? "action" : stage.counts.later ? "later" : "complete";
34
+ }
35
+ const items = stages.flatMap((stage) => stage.items);
36
+ const counts = countStatuses(items);
37
+ const managedItems = items.filter((item) => !["external", "info", "later"].includes(item.status));
38
+ const completedManagedItems = managedItems.filter((item) => item.status === "complete");
39
+ return {
40
+ schemaVersion: 1,
41
+ generatedAt: options.generatedAt || new Date().toISOString(),
42
+ audit: audit ? auditSummary(audit) : null,
43
+ status: counts.action ? "needs-work" : audit ? "management-ready" : "not-started",
44
+ progress: {
45
+ complete: completedManagedItems.length,
46
+ total: managedItems.length,
47
+ percent: managedItems.length ? Math.round((completedManagedItems.length / managedItems.length) * 100) : 0
48
+ },
49
+ counts,
50
+ canInitialize: Boolean(audit
51
+ && ["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)
52
+ && (audit.auditKind === "soc-2-type-1"
53
+ ? audit.typeOneAsOf
54
+ : audit.periodStart && audit.periodEnd)
55
+ && initializationNeeded(audit, records, loaded.model)),
56
+ stages
57
+ };
58
+ }
59
+
60
+ export async function prepareAuditWorkspace(input, options = {}) {
61
+ const loaded = await loadWorkspace(input);
62
+ const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
63
+ if (!audit) throw new Error(`Audit "${options.auditId || ""}" was not found.`);
64
+ if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
65
+ throw new Error("Audit preparation requires a SOC 2 Type 1 or Type 2 engagement.");
66
+ }
67
+ if (audit.auditKind === "soc-2-type-2" && (!audit.periodStart || !audit.periodEnd)) {
68
+ throw new Error("Set the Type 2 audit period before initializing audit preparation.");
69
+ }
70
+ if (audit.auditKind === "soc-2-type-1" && !audit.typeOneAsOf) {
71
+ throw new Error("Set the Type 1 as-of date before initializing audit preparation.");
72
+ }
73
+
74
+ const model = loaded.model.auditReadiness || {};
75
+ const auditEntry = loaded.entries.find((entry) => entry.record.id === audit.id);
76
+ const auditRevision = createHash("sha256").update(auditEntry.source).digest("hex");
77
+ const documents = loaded.resources.filter((record) => record.type === "document");
78
+ const nextAudit = { ...audit };
79
+ const linkedDocuments = [];
80
+ const createdDocuments = [];
81
+ const existingIds = loaded.resources.map((record) => record.id);
82
+ try {
83
+ for (const definition of applicableManagementDocuments(audit, model)) {
84
+ const linked = nextAudit[definition.field]
85
+ ? documents.find((record) => record.id === nextAudit[definition.field])
86
+ : null;
87
+ if (linked && linked.template !== true) continue;
88
+ const template = linked
89
+ || documents.find((record) => record.documentKind === definition.kind && record.template === true)
90
+ || documents.find((record) => record.documentKind === definition.kind);
91
+ if (!template) continue;
92
+ const source = await primaryMarkdown(loaded, template);
93
+ if (!source) throw new Error(`The ${definition.title} template has no Markdown content.`);
94
+ const id = createResourceId("document", `${audit.id} ${definition.kind}`, existingIds);
95
+ existingIds.push(id);
96
+ const document = engagementDocument(template, definition, audit, id);
97
+ const content = materializeManagementMarkdown(source, audit, loaded.resources);
98
+ await createResource(loaded.root, document, { content: { content } });
99
+ documents.push(document);
100
+ createdDocuments.push(document);
101
+ nextAudit[definition.field] = id;
102
+ linkedDocuments.push(id);
103
+ }
104
+ } catch (error) {
105
+ for (const document of createdDocuments.reverse()) {
106
+ await deleteResource(loaded.root, document.type, document.id).catch(() => {});
107
+ }
108
+ throw error;
109
+ }
110
+
111
+ const existingKinds = new Set(loaded.resources
112
+ .filter((record) => record.type === "audit-population" && record.auditId === audit.id)
113
+ .map((record) => record.populationKind));
114
+ const selectedControls = (audit.controlIds || [])
115
+ .map((id) => loaded.resources.find((record) => record.id === id))
116
+ .filter(Boolean);
117
+ const sourceSystems = loaded.resources.filter((record) => record.type === "system");
118
+ const populations = (audit.auditKind === "soc-2-type-2" ? model.populationTemplates || [] : [])
119
+ .filter((template) => !existingKinds.has(template.kind))
120
+ .map((template) => {
121
+ const id = createResourceId(
122
+ "audit-population",
123
+ `${template.kind} ${audit.id}`,
124
+ existingIds
125
+ );
126
+ existingIds.push(id);
127
+ const controlIds = selectedControls
128
+ .filter((control) => (template.controlCodes || []).includes(control.code))
129
+ .map((control) => control.id);
130
+ const matchingSources = sourceSystems.filter((system) => (
131
+ (system.evidenceSourceKinds || []).includes(template.sourceKind)
132
+ ));
133
+ return {
134
+ schemaVersion: 1,
135
+ id,
136
+ type: "audit-population",
137
+ title: template.title,
138
+ status: "planned",
139
+ auditId: audit.id,
140
+ populationKind: template.kind,
141
+ periodStart: audit.periodStart,
142
+ periodEnd: audit.periodEnd,
143
+ ownerIds: [...audit.ownerIds],
144
+ ...(controlIds.length ? { controlIds } : {}),
145
+ ...(matchingSources.length === 1 ? { sourceSystemId: matchingSources[0].id } : {}),
146
+ reconciliationSummary: `Authoritative source to confirm: ${template.sourcePrompt}. ${template.timing || ""}`.trim()
147
+ };
148
+ });
149
+
150
+ try {
151
+ if (populations.length) await createResources(loaded.root, populations);
152
+ if (JSON.stringify(nextAudit) !== JSON.stringify(audit)) {
153
+ await updateResource(loaded.root, "audit", audit.id, nextAudit, { expectedRevision: auditRevision });
154
+ }
155
+ } catch (error) {
156
+ for (const population of populations.reverse()) {
157
+ await deleteResource(loaded.root, population.type, population.id).catch(() => {});
158
+ }
159
+ for (const document of createdDocuments.reverse()) {
160
+ await deleteResource(loaded.root, document.type, document.id).catch(() => {});
161
+ }
162
+ throw error;
163
+ }
164
+ return {
165
+ auditId: audit.id,
166
+ linkedDocumentIds: linkedDocuments,
167
+ createdDocumentIds: createdDocuments.map((record) => record.id),
168
+ createdPopulationIds: populations.map((record) => record.id)
169
+ };
170
+ }
171
+
172
+ function scopeStage(audit, records, byId) {
173
+ const items = [];
174
+ items.push(item(
175
+ "engagement",
176
+ audit ? "complete" : "action",
177
+ "Create the engagement",
178
+ audit
179
+ ? `${audit.title} is the audit record used for scope, dates, the CPA firm, requests, and the final report.`
180
+ : "Create a SOC 2 Type 2 audit record before collecting period evidence.",
181
+ audit || { type: "audit" }
182
+ ));
183
+ if (!audit) {
184
+ return stage("scope", "Scope and Engagement", "Define the report, service boundary, criteria, dependencies, and CPA firm.", items);
185
+ }
186
+
187
+ const periodComplete = audit.auditKind === "soc-2-type-2"
188
+ ? audit.periodStart && audit.periodEnd
189
+ : audit.auditKind === "soc-2-type-1"
190
+ ? audit.typeOneAsOf
191
+ : false;
192
+ items.push(item(
193
+ "period",
194
+ periodComplete ? "complete" : "action",
195
+ "Set the report type and date",
196
+ periodComplete
197
+ ? audit.auditKind === "soc-2-type-2"
198
+ ? `Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
199
+ : `Type 1 as-of date: ${audit.typeOneAsOf}.`
200
+ : audit.auditKind === "soc-2-type-1"
201
+ ? "Set the Type 1 as-of date."
202
+ : audit.auditKind === "soc-2-type-2"
203
+ ? "Set the exact Type 2 start and end dates."
204
+ : "Change this readiness record to a Type 1 or Type 2 engagement before planning the report.",
205
+ audit
206
+ ));
207
+
208
+ const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
209
+ const completeSystems = systems.filter((system) => (
210
+ system.status === "active"
211
+ && system.inScope === true
212
+ && system.description
213
+ && system.dataClassification
214
+ && (system.ownerIds || []).length
215
+ ));
216
+ items.push(item(
217
+ "systems",
218
+ systems.length && completeSystems.length === systems.length ? "complete" : "action",
219
+ "Define the service boundary",
220
+ systems.length
221
+ ? `${completeSystems.length} of ${systems.length} selected systems are active, explicitly in scope, owned, classified, and described.`
222
+ : "Select every in-scope service and supporting system, then describe its owner, environment, data, vendors, and boundary.",
223
+ systems[0] || { type: "system" }
224
+ ));
225
+
226
+ const engagementStart = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodStart;
227
+ const commitments = records.filter((record) => record.type === "commitment"
228
+ && record.status === "active"
229
+ && systems.some((system) => (
230
+ (system.commitmentIds || []).includes(record.id) || (record.systemIds || []).includes(system.id)
231
+ )));
232
+ const completeCommitments = commitments.filter((commitment) => (
233
+ commitment.statement
234
+ && (commitment.ownerIds || []).length
235
+ && commitment.effectiveOn
236
+ && (!engagementStart || commitment.effectiveOn <= engagementStart)
237
+ && (commitment.requirementIds || []).length
238
+ && (commitment.controlIds || []).length
239
+ ));
240
+ const systemsWithoutCommitments = systems.filter((system) => !completeCommitments.some((commitment) => (
241
+ (system.commitmentIds || []).includes(commitment.id) || (commitment.systemIds || []).includes(system.id)
242
+ )));
243
+ items.push(item(
244
+ "commitments",
245
+ systems.length && !systemsWithoutCommitments.length ? "complete" : "action",
246
+ "Record commitments and system requirements",
247
+ systems.length
248
+ ? `${completeCommitments.length} complete active commitments cover ${systems.length - systemsWithoutCommitments.length} of ${systems.length} in-scope systems.`
249
+ : "Record customer commitments and internal system requirements after defining the service boundary.",
250
+ commitments[0] || { type: "commitment" }
251
+ ));
252
+
253
+ const frameworkRequirementIds = records
254
+ .filter((record) => record.type === "requirement" && (audit.frameworkIds || []).includes(record.frameworkId))
255
+ .map((record) => record.id);
256
+ const unresolvedRequirements = frameworkRequirementIds
257
+ .map((id) => byId.get(id))
258
+ .filter((requirement) => (
259
+ requirement.applicability === "undetermined"
260
+ || (requirement.applicability === "not-applicable" && !requirement.applicabilityRationale)
261
+ ));
262
+ const applicableRequirementIds = frameworkRequirementIds.filter((id) => byId.get(id)?.applicability === "applicable");
263
+ const missingApplicableRequirements = applicableRequirementIds.filter((id) => !(audit.requirementIds || []).includes(id));
264
+ const selectedRequirements = (audit.requirementIds || []).map((id) => byId.get(id)).filter(Boolean);
265
+ const unexpectedRequirements = selectedRequirements.filter((requirement) => (
266
+ !(audit.frameworkIds || []).includes(requirement.frameworkId)
267
+ || requirement.applicability !== "applicable"
268
+ ));
269
+ const descriptionCriteriaSelected = selectedRequirements.some((requirement) => (
270
+ (requirement.tags || []).includes("description-criteria")
271
+ || /^DC\d+/i.test(requirement.reference || "")
272
+ ));
273
+ const criteriaComplete = (audit.frameworkIds || []).length
274
+ && (audit.requirementIds || []).length
275
+ && (audit.controlIds || []).length
276
+ && !unresolvedRequirements.length
277
+ && !missingApplicableRequirements.length
278
+ && !unexpectedRequirements.length
279
+ && descriptionCriteriaSelected;
280
+ items.push(item(
281
+ "criteria",
282
+ criteriaComplete ? "complete" : "action",
283
+ "Confirm criteria and controls in scope",
284
+ criteriaComplete
285
+ ? `${audit.requirementIds.length} applicable criteria and ${audit.controlIds.length} controls are selected, with applicability resolved for the selected frameworks.`
286
+ : unresolvedRequirements.length
287
+ ? `Resolve applicability and record a rationale for ${unresolvedRequirements.length} selected-framework criteria.`
288
+ : missingApplicableRequirements.length
289
+ ? `Add ${missingApplicableRequirements.length} applicable selected-framework criteria to the engagement.`
290
+ : unexpectedRequirements.length
291
+ ? `Remove ${unexpectedRequirements.length} criteria that are not applicable members of the selected frameworks.`
292
+ : !descriptionCriteriaSelected
293
+ ? "Select the applicable SOC 2 description criteria as well as the Trust Services Criteria."
294
+ : "Select the Security criteria, any optional Trust Services Categories, and the controls included in this report.",
295
+ audit
296
+ ));
297
+
298
+ const expectedSubserviceVendorIds = new Set(systems.flatMap((system) => system.subserviceVendorIds || []));
299
+ const missingSubserviceVendorIds = [...expectedSubserviceVendorIds].filter((id) => !(audit.subserviceVendorIds || []).includes(id));
300
+ const inclusiveSystemIds = records
301
+ .filter((record) => record.type === "system" && (audit.subserviceVendorIds || []).includes(record.vendorId))
302
+ .map((record) => record.id);
303
+ const inclusiveControlCount = (audit.controlIds || [])
304
+ .map((id) => byId.get(id))
305
+ .filter((control) => (control?.systemIds || []).some((id) => inclusiveSystemIds.includes(id)))
306
+ .length;
307
+ const subserviceComplete = Boolean(audit.subserviceMethod)
308
+ && !((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable")
309
+ && !missingSubserviceVendorIds.length
310
+ && !(audit.subserviceMethod === "inclusive" && (!inclusiveSystemIds.length || !inclusiveControlCount));
311
+ items.push(item(
312
+ "subservices",
313
+ subserviceComplete ? "complete" : "action",
314
+ "Decide how subservice organizations are presented",
315
+ subserviceComplete
316
+ ? `${displayValue(audit.subserviceMethod)} method selected for ${(audit.subserviceVendorIds || []).length} subservice organizations${audit.subserviceMethod === "inclusive" ? `, with ${inclusiveControlCount} included controls` : ""}.`
317
+ : missingSubserviceVendorIds.length
318
+ ? `Add ${missingSubserviceVendorIds.length} subservice organizations already identified by the in-scope systems.`
319
+ : audit.subserviceMethod === "inclusive"
320
+ ? "For the inclusive method, catalog the subservice systems and include the subservice controls the auditor will examine."
321
+ : "Identify relevant infrastructure and service providers, then agree on carve-out, inclusive, or not-applicable treatment.",
322
+ audit
323
+ ));
324
+
325
+ const relevantComplementaryControls = records.filter((record) => (
326
+ record.type === "complementary-control"
327
+ && record.status === "active"
328
+ && (record.systemIds || []).some((id) => (audit.systemIds || []).includes(id))
329
+ ));
330
+ const selectedComplementaryControls = (audit.complementaryControlIds || []).map((id) => byId.get(id)).filter(Boolean);
331
+ const complementaryComplete = audit.complementaryControlsConclusion === "not-applicable"
332
+ ? !relevantComplementaryControls.length
333
+ : audit.complementaryControlsConclusion === "identified"
334
+ && selectedComplementaryControls.length
335
+ && selectedComplementaryControls.every((control) => (
336
+ control.status === "active"
337
+ && (control.systemIds || []).some((id) => (audit.systemIds || []).includes(id))
338
+ ));
339
+ items.push(item(
340
+ "complementary-controls",
341
+ complementaryComplete ? "complete" : "action",
342
+ "Resolve customer and subservice dependencies",
343
+ complementaryComplete
344
+ ? audit.complementaryControlsConclusion === "identified"
345
+ ? `${audit.complementaryControlIds.length} complementary controls are selected.`
346
+ : "Management recorded that no complementary controls are needed for the described service."
347
+ : audit.complementaryControlsConclusion === "not-applicable" && relevantComplementaryControls.length
348
+ ? `${relevantComplementaryControls.length} active complementary controls apply to the in-scope systems, which conflicts with the not-applicable conclusion.`
349
+ : "State whether customers or subservice organizations must operate complementary controls. If they do, record and select each one.",
350
+ audit
351
+ ));
352
+
353
+ const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
354
+ const auditorNamed = Boolean(auditor || hasMeaningfulValue(audit.auditor));
355
+ items.push(item(
356
+ "auditor",
357
+ auditorNamed ? "complete" : "action",
358
+ "Engage an independent CPA firm",
359
+ auditorNamed
360
+ ? `${auditor?.title || "An independent CPA firm"} is recorded for the engagement.`
361
+ : "Select a CPA firm and agree on scope, timing, subservice treatment, and evidence expectations early.",
362
+ audit
363
+ ));
364
+ return stage("scope", "Scope and Engagement", "Define the report, service boundary, criteria, dependencies, and CPA firm.", items);
365
+ }
366
+
367
+ function programStage(audit, records, byId) {
368
+ const selectedControls = audit?.controlIds?.length
369
+ ? audit.controlIds.map((id) => byId.get(id)).filter(Boolean)
370
+ : records.filter((record) => record.type === "control" && record.status !== "retired");
371
+ const policyIds = new Set(selectedControls.flatMap((control) => control.policyIds || []));
372
+ const policies = (policyIds.size
373
+ ? [...policyIds].map((id) => byId.get(id)).filter(Boolean)
374
+ : records.filter((record) => record.type === "policy" && !["superseded", "retired"].includes(record.status)));
375
+ const approvedPolicies = policies.filter((policy) => (
376
+ ["approved", "active"].includes(policy.status)
377
+ && policy.approvedOn
378
+ && policy.effectiveOn
379
+ && (policy.ownerIds || []).length
380
+ && (policy.approverIds || []).length
381
+ && partiesIndependent(policy.ownerIds, policy.approverIds, byId)
382
+ ));
383
+ const implementedControls = selectedControls.filter((control) => (
384
+ control.status === "implemented"
385
+ && control.effectiveOn
386
+ && control.activity
387
+ && control.operationMode
388
+ && control.frequency
389
+ && (control.ownerIds || []).length
390
+ && (control.requirementIds || []).length
391
+ && (control.policyIds || []).length
392
+ && (!audit?.systemIds?.length || (control.systemIds || []).some((id) => audit.systemIds.includes(id)))
393
+ ));
394
+ const assessment = records.find((record) => (
395
+ record.type === "risk-assessment"
396
+ && record.status === "complete"
397
+ && record.methodology
398
+ && record.approvedOn
399
+ && partiesIndependent(record.assessorIds, record.reviewerIds, byId)
400
+ && (!audit?.periodEnd || (record.assessmentDate <= audit.periodEnd && record.assessmentDate >= shiftYear(audit.periodEnd, -1)))
401
+ && (!audit?.systemIds?.length || !(record.systemIds || []).length || record.systemIds.some((id) => audit.systemIds.includes(id)))
402
+ ));
403
+ return stage("program", "Adopt and Implement", "Approve the rules, confirm the controls match actual operation, and assess risk.", [
404
+ item(
405
+ "policies",
406
+ policies.length && approvedPolicies.length === policies.length ? "complete" : "action",
407
+ "Approve the applicable policies",
408
+ policies.length
409
+ ? `${approvedPolicies.length} of ${policies.length} applicable policies have an owner, separate approver, approval date, and effective date.`
410
+ : "Link the controls to the policies management will adopt.",
411
+ approvedPolicies[0] || policies[0] || { type: "policy" }
412
+ ),
413
+ item(
414
+ "controls",
415
+ selectedControls.length && implementedControls.length === selectedControls.length ? "complete" : "action",
416
+ "Confirm every control is implemented",
417
+ selectedControls.length
418
+ ? `${implementedControls.length} of ${selectedControls.length} selected controls are implemented, effective, owned, mapped, scoped, and define their activity, mode, and frequency.`
419
+ : "Select the controls for the engagement, then confirm each statement matches current practice.",
420
+ implementedControls[0] || selectedControls[0] || { type: "control" }
421
+ ),
422
+ item(
423
+ "risk-assessment",
424
+ assessment ? "complete" : "action",
425
+ "Complete the in-scope risk assessment",
426
+ assessment
427
+ ? `${assessment.title} is the most recent completed, approved, and independently reviewed assessment found for the service.`
428
+ : "Complete an in-scope risk assessment during the year ending with the report date or period, using the approved method and a reviewer separate from the assessor.",
429
+ assessment || { type: "risk-assessment" }
430
+ )
431
+ ]);
432
+ }
433
+
434
+ async function documentsStage(loaded, audit, byId) {
435
+ const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
436
+ const items = [];
437
+ for (const definition of definitions) {
438
+ const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
439
+ const source = document ? await primaryMarkdown(loaded, document) : "";
440
+ const contentIssues = managementDocumentContentIssues(source, definition, audit);
441
+ const engagementEnd = audit?.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit?.periodEnd;
442
+ if (document?.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
443
+ contentIssues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
444
+ }
445
+ if (definition.kind === "soc2-period-completeness" && document?.approvedOn && audit) {
446
+ const latestReconciliation = loaded.resources
447
+ .filter((record) => record.type === "audit-population" && record.auditId === audit.id)
448
+ .map((record) => record.reconciledOn)
449
+ .filter(Boolean)
450
+ .sort()
451
+ .at(-1);
452
+ if (latestReconciliation && document.approvedOn < latestReconciliation) {
453
+ contentIssues.push("Approve the period completeness statement after the last population reconciliation.");
454
+ }
455
+ }
456
+ if (definition.kind === "soc2-management-representation" && document) {
457
+ const signedEvidence = (document.evidenceIds || [])
458
+ .map((id) => byId.get(id))
459
+ .find((record) => (
460
+ record?.type === "evidence"
461
+ && record.status === "verified"
462
+ && (record.filePaths || []).length
463
+ ));
464
+ if (!signedEvidence) contentIssues.push("Link a verified fixed-format copy of the signed representation letter as evidence.");
465
+ else if (!signedEvidence.collectedOn || (engagementEnd && signedEvidence.collectedOn < engagementEnd)) {
466
+ contentIssues.push(`The signed representation must be dated on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
467
+ }
468
+ }
469
+ const complete = Boolean(
470
+ document
471
+ && document.type === "document"
472
+ && document.template !== true
473
+ && document.status === "active"
474
+ && document.approvedOn
475
+ && document.effectiveOn
476
+ && (document.ownerIds || []).length
477
+ && (document.approverIds || []).length
478
+ && partiesIndependent(document.ownerIds, document.approverIds, byId)
479
+ && source
480
+ && !contentIssues.length
481
+ );
482
+ const representationLater = definition.kind === "soc2-management-representation"
483
+ && audit
484
+ && !["fieldwork", "complete"].includes(audit.status);
485
+ items.push(item(
486
+ definition.kind,
487
+ complete ? "complete" : representationLater ? "later" : "action",
488
+ definition.title,
489
+ complete
490
+ ? "Linked Markdown is complete, active, approved, and effective."
491
+ : document
492
+ ? `${definition.timing} ${contentIssues[0] || "Complete and approve the engagement-specific document."}`
493
+ : `Link the starter ${definition.title.toLowerCase()} to this audit. ${definition.timing}`,
494
+ document || { type: "document" }
495
+ ));
496
+ }
497
+ return stage("documents", "Management Documents", "Prepare management's description, assertions, completeness work, and closing representations.", items);
498
+ }
499
+
500
+ function evidenceStage(audit, records, byId, model) {
501
+ const controls = (audit?.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
502
+ const evidence = records.filter((record) => record.type === "evidence");
503
+ const periodEvidence = evidence.filter((record) => (
504
+ record.status === "verified"
505
+ && (record.evidenceKind !== "rendered-record" || record.sourceCommit)
506
+ && evidenceRelevantToAuditDate(record, audit)
507
+ ));
508
+ const controlsWithEvidence = controls.filter((control) => periodEvidence.some((record) => (
509
+ controlIdsForRecord(record, byId).has(control.id)
510
+ )));
511
+ const items = [
512
+ item(
513
+ "control-evidence",
514
+ controls.length && controlsWithEvidence.length === controls.length ? "complete" : "action",
515
+ "Collect operating evidence from authoritative systems",
516
+ controls.length
517
+ ? `${controlsWithEvidence.length} of ${controls.length} selected controls have verified evidence for the period.`
518
+ : "Select the engagement controls before checking their operating evidence.",
519
+ periodEvidence[0] || { type: "evidence" }
520
+ )
521
+ ];
522
+ const systems = records.filter((record) => record.type === "system" && record.status === "active");
523
+ for (const source of model.auditReadiness?.externalEvidence || []) {
524
+ const relevantControls = controls.filter((control) => (source.controlCodes || []).includes(control.code));
525
+ if (!relevantControls.length) {
526
+ items.push(item(
527
+ `source-${source.id}`,
528
+ "info",
529
+ source.title,
530
+ `No mapped controls from this source category are selected. ${source.timing}`
531
+ ));
532
+ continue;
533
+ }
534
+ const sourceSystems = systems.filter((system) => (
535
+ (system.evidenceSourceKinds || []).some((kind) => (source.sourceKinds || []).includes(kind))
536
+ ));
537
+ const coveredControls = relevantControls.filter((control) => periodEvidence.some((record) => (
538
+ sourceSystems.some((system) => system.id === record.sourceSystemId)
539
+ && controlIdsForRecord(record, byId).has(control.id)
540
+ )));
541
+ const status = sourceSystems.length && coveredControls.length === relevantControls.length ? "complete" : "action";
542
+ const message = !sourceSystems.length
543
+ ? `${source.description} Add the authoritative systems to Systems and assign these evidence source roles: ${(source.sourceKinds || []).map(displayValue).join(", ")}. ${source.timing}`
544
+ : coveredControls.length !== relevantControls.length
545
+ ? `${sourceSystems.map((system) => system.title).join(", ")} ${sourceSystems.length === 1 ? "is" : "are"} cataloged, but verified source evidence covers ${coveredControls.length} of ${relevantControls.length} mapped controls. ${source.timing}`
546
+ : `${sourceSystems.map((system) => system.title).join(", ")} provide verified source evidence for ${relevantControls.length} mapped controls. ${source.timing}`;
547
+ items.push(item(
548
+ `source-${source.id}`,
549
+ status,
550
+ source.title,
551
+ message,
552
+ periodEvidence.find((record) => sourceSystems.some((system) => system.id === record.sourceSystemId))
553
+ || sourceSystems[0]
554
+ || { type: "system" }
555
+ ));
556
+ }
557
+ return stage("evidence", "Operating Evidence Sources", "Catalog each authoritative system, record when to extract its evidence, and bind verified exports to the controls. FileGRC packages the proof but does not replace those systems.", items);
558
+ }
559
+
560
+ function populationsStage(audit, records, byId, model) {
561
+ if (audit && audit.auditKind !== "soc-2-type-2") {
562
+ return stage(
563
+ "populations",
564
+ "Population Completeness",
565
+ "Complete period populations apply to Type 2 operating-effectiveness testing.",
566
+ [item("type-2-only", "info", "No Type 2 population plan required", "A Type 1 report evaluates design and implementation as of one date. The auditor may still request specific inventories or evidence.")]
567
+ );
568
+ }
569
+ const populations = audit
570
+ ? records.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
571
+ : [];
572
+ const templates = model.auditReadiness?.populationTemplates || [];
573
+ const items = templates.map((template) => {
574
+ const population = populations.find((record) => record.populationKind === template.kind);
575
+ const result = populationResult(population, audit, byId);
576
+ return item(
577
+ `population-${template.kind}`,
578
+ result.status,
579
+ template.title,
580
+ result.message || `Use ${template.sourcePrompt} as the starting point, then record the exact authoritative source.`,
581
+ population || { type: "audit-population" }
582
+ );
583
+ });
584
+ return stage(
585
+ "populations",
586
+ "Population Completeness",
587
+ "Management reconciles complete populations for the exact period. A zero count still needs a source export and recorded query.",
588
+ items
589
+ );
590
+ }
591
+
592
+ function auditorStage(model) {
593
+ return stage("auditor", "Auditor-Owned Work", "FileGRC prepares the record set but does not make the CPA firm's independent judgments.", [
594
+ 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."),
595
+ item("sampling", "external", "Sample selection and independent testing", "The auditor chooses samples, performs tests, evaluates exceptions, and decides whether more work is needed."),
596
+ item("report", "external", "Report and opinion", "Management reviews and signs its representations. The auditor issues the final report and opinion."),
597
+ item("criteria", "external", "Authoritative criteria and examination guidance", "FileGRC stores reference IDs and orientation text. Use the publisher's current official criteria and the engagement team's examination guidance for scope, evaluation, and reporting.")
598
+ ]);
599
+ }
600
+
601
+ function populationResult(population, audit, byId) {
602
+ if (!population) return { status: "action", message: "Initialize this population for the engagement." };
603
+ if (population.periodStart !== audit?.periodStart || population.periodEnd !== audit?.periodEnd) {
604
+ return { status: "action", message: "The population period does not match the exact audit period." };
605
+ }
606
+ if (population.status === "not-applicable") {
607
+ if ((population.controlIds || []).length) {
608
+ return { status: "action", message: "This population is linked to in-scope controls, so it cannot be marked not applicable. Reconcile it or remove the incorrect control links." };
609
+ }
610
+ return population.notApplicableReason
611
+ ? { status: "complete", message: `Not applicable: ${population.notApplicableReason}` }
612
+ : { status: "action", message: "Document why this population does not apply." };
613
+ }
614
+ if (population.status !== "reconciled") {
615
+ return { status: "action", message: `${displayValue(population.status)}. Export and reconcile the complete population, even when its count is zero.` };
616
+ }
617
+ const evidence = byId.get(population.sourceEvidenceId);
618
+ const requiredEvidence = [
619
+ "generatedAt",
620
+ "timezone",
621
+ "queryDescription",
622
+ "populationCount",
623
+ "completenessValidation",
624
+ "accuracyValidation"
625
+ ];
626
+ const evidenceComplete = evidence
627
+ && evidence.type === "evidence"
628
+ && evidence.evidenceKind === "population-export"
629
+ && evidence.status === "verified"
630
+ && population.sourceSystemId
631
+ && evidence.sourceSystemId === population.sourceSystemId
632
+ && evidence.periodStart === audit.periodStart
633
+ && evidence.periodEnd === audit.periodEnd
634
+ && requiredEvidence.every((field) => evidence[field] !== undefined && evidence[field] !== null && evidence[field] !== "");
635
+ const reconciliationComplete = (population.reconciledByIds || []).length
636
+ && population.reconciledOn
637
+ && ["complete", "complete-with-exceptions"].includes(population.conclusion)
638
+ && (population.conclusion !== "complete-with-exceptions" || population.reconciliationSummary);
639
+ const generatedOn = timestampDate(evidence?.generatedAt, evidence?.timezone);
640
+ const sequenceComplete = Number.isInteger(evidence?.populationCount)
641
+ && evidence.populationCount >= 0
642
+ && generatedOn > audit.periodEnd
643
+ && population.reconciledOn >= generatedOn;
644
+ if (!evidenceComplete || !reconciliationComplete || !sequenceComplete) {
645
+ return { status: "action", message: "Finish the reconciliation and link a verified population export with its exact query, timezone, count, completeness check, and accuracy check." };
646
+ }
647
+ return {
648
+ status: "complete",
649
+ message: `${evidence.populationCount} items reconciled from ${evidence.source || "the authoritative source"}${population.conclusion === "complete-with-exceptions" ? " with documented exceptions" : ""}.`
650
+ };
651
+ }
652
+
653
+ function timestampDate(value, timezone) {
654
+ if (!value || !timezone) return null;
655
+ const instant = new Date(value);
656
+ if (Number.isNaN(instant.getTime())) return null;
657
+ try {
658
+ const parts = new Intl.DateTimeFormat("en-US", {
659
+ timeZone: timezone,
660
+ year: "numeric",
661
+ month: "2-digit",
662
+ day: "2-digit"
663
+ }).formatToParts(instant);
664
+ const fields = Object.fromEntries(parts.map(({ type, value: item }) => [type, item]));
665
+ return `${fields.year}-${fields.month}-${fields.day}`;
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+
671
+ async function primaryMarkdown(loaded, record) {
672
+ const definition = loaded.model.resources[record.type];
673
+ const item = markdownEntries(loaded.model, record).find((entry) => (
674
+ definition.markdown?.[entry.name]?.primary || entry.name === loaded.model.recordContent?.slot
675
+ ));
676
+ if (!item) return "";
677
+ try {
678
+ return await readFile(resolveDataPath(loaded.root, item.path), "utf8");
679
+ } catch {
680
+ return "";
681
+ }
682
+ }
683
+
684
+ function initializationNeeded(audit, records, model) {
685
+ const readiness = model.auditReadiness || {};
686
+ const needsDocumentLink = applicableManagementDocuments(audit, readiness)
687
+ .some((definition) => {
688
+ const linked = records.find((record) => record.id === audit[definition.field]);
689
+ return (!linked || linked.template === true) && records.some((record) => (
690
+ record.type === "document" && record.documentKind === definition.kind
691
+ ));
692
+ });
693
+ const populationKinds = new Set(records
694
+ .filter((record) => record.type === "audit-population" && record.auditId === audit.id)
695
+ .map((record) => record.populationKind));
696
+ const needsPopulation = audit.auditKind === "soc-2-type-2" && (readiness.populationTemplates || [])
697
+ .some((template) => !populationKinds.has(template.kind));
698
+ return needsDocumentLink || needsPopulation;
699
+ }
700
+
701
+ function applicableManagementDocuments(audit, readiness) {
702
+ return (readiness.managementDocuments || []).filter((definition) => (
703
+ !audit || !definition.engagementKinds?.length || definition.engagementKinds.includes(audit.auditKind)
704
+ ));
705
+ }
706
+
707
+ function evidenceOverlaps(record, start, end) {
708
+ return (record.periodStart && record.periodEnd && record.periodStart <= end && record.periodEnd >= start)
709
+ || (record.collectedOn && record.collectedOn >= start && record.collectedOn <= end);
710
+ }
711
+
712
+ function evidenceRelevantToAuditDate(record, audit) {
713
+ if (!audit) return false;
714
+ if (audit.auditKind === "soc-2-type-1") {
715
+ const date = audit.typeOneAsOf;
716
+ return Boolean(date) && (
717
+ (record.periodStart && record.periodEnd && record.periodStart <= date && record.periodEnd >= date)
718
+ || record.collectedOn === date
719
+ );
720
+ }
721
+ return Boolean(audit.periodStart && audit.periodEnd)
722
+ && evidenceOverlaps(record, audit.periodStart, audit.periodEnd);
723
+ }
724
+
725
+ function controlIdsForRecord(record, byId, seen = new Set()) {
726
+ const ids = new Set();
727
+ if (!record || seen.has(record.id)) return ids;
728
+ seen.add(record.id);
729
+ if (record.type === "control") ids.add(record.id);
730
+ for (const id of record.controlIds || []) ids.add(id);
731
+ if (record.controlId) ids.add(record.controlId);
732
+ if (record.obligationId) {
733
+ for (const id of byId.get(record.obligationId)?.controlIds || []) ids.add(id);
734
+ }
735
+ for (const sourceId of record.sourceResourceIds || []) {
736
+ for (const id of controlIdsForRecord(byId.get(sourceId), byId, seen)) ids.add(id);
737
+ }
738
+ if (record.sourceResourceId) {
739
+ for (const id of controlIdsForRecord(byId.get(record.sourceResourceId), byId, seen)) ids.add(id);
740
+ }
741
+ return ids;
742
+ }
743
+
744
+ function hasMeaningfulValue(value) {
745
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
746
+ return Object.values(value).some((item) => (
747
+ typeof item === "string" ? item.trim() : item && typeof item === "object" ? hasMeaningfulValue(item) : false
748
+ ));
749
+ }
750
+
751
+ function containsOpenPlaceholder(source) {
752
+ return /\[[^\]\n]{2,}\](?!\()/u.test(source);
753
+ }
754
+
755
+ function managementDocumentContentIssues(source, definition, audit) {
756
+ if (!source) return ["Add the required Markdown content."];
757
+ if (containsOpenPlaceholder(source)) return ["Replace every bracketed preparation placeholder before approval."];
758
+ const words = source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || [];
759
+ if (definition.minimumWords && words.length < definition.minimumWords) {
760
+ return [`Add substantive content; this document has ${words.length} words and the preparation check expects at least ${definition.minimumWords}.`];
761
+ }
762
+ const missingHeadings = (definition.requiredHeadings || []).filter((heading) => (
763
+ !new RegExp(`^#{1,6}\\s+.*\\b${escapeRegExp(heading)}\\b`, "imu").test(source)
764
+ ));
765
+ if (missingHeadings.length) return [`Add the missing description sections: ${missingHeadings.join(", ")}.`];
766
+ if (definition.dateBinding === "engagement" && audit) {
767
+ const dates = audit.auditKind === "soc-2-type-1"
768
+ ? [audit.typeOneAsOf]
769
+ : [audit.periodStart, audit.periodEnd];
770
+ if (dates.some((date) => date && !source.includes(date))) {
771
+ return ["Name the exact engagement date or period in the document."];
772
+ }
773
+ }
774
+ return [];
775
+ }
776
+
777
+ function engagementDocument(template, definition, audit, id) {
778
+ const {
779
+ approvedOn,
780
+ effectiveOn,
781
+ evidenceIds,
782
+ id: ignoredId,
783
+ status: ignoredStatus,
784
+ supersedesId,
785
+ template: ignoredTemplate,
786
+ title: ignoredTitle,
787
+ ...shared
788
+ } = template;
789
+ return {
790
+ ...shared,
791
+ id,
792
+ title: `${audit.title}: ${definition.title}`,
793
+ status: "draft",
794
+ ownerIds: [...audit.ownerIds],
795
+ ...(audit.systemIds?.length ? { systemIds: [...audit.systemIds] } : {}),
796
+ version: "0.1"
797
+ };
798
+ }
799
+
800
+ function materializeManagementMarkdown(source, audit, records) {
801
+ const keep = audit.auditKind === "soc-2-type-1" ? "type-1" : "type-2";
802
+ const discard = keep === "type-1" ? "type-2" : "type-1";
803
+ const withoutDiscarded = source.replace(
804
+ new RegExp(`<!-- ${discard}:start -->[\\s\\S]*?<!-- ${discard}:end -->\\s*`, "g"),
805
+ ""
806
+ );
807
+ const systems = (audit.systemIds || [])
808
+ .map((id) => records.find((record) => record.id === id)?.title)
809
+ .filter(Boolean)
810
+ .join(", ");
811
+ const categoryTags = new Set(["security", "availability", "processing-integrity", "confidentiality", "privacy"]);
812
+ const categories = [...new Set((audit.requirementIds || [])
813
+ .flatMap((id) => records.find((record) => record.id === id)?.tags || [])
814
+ .filter((tag) => categoryTags.has(tag))
815
+ .map(displayValue))]
816
+ .join(", ");
817
+ const period = audit.auditKind === "soc-2-type-1"
818
+ ? audit.typeOneAsOf || "[as-of date]"
819
+ : audit.periodStart && audit.periodEnd
820
+ ? `${audit.periodStart} through ${audit.periodEnd}`
821
+ : "[start date] through [end date]";
822
+ return withoutDiscarded
823
+ .replaceAll(`<!-- ${keep}:start -->`, "")
824
+ .replaceAll(`<!-- ${keep}:end -->`, "")
825
+ .replaceAll("[as-of date]", audit.typeOneAsOf || "[as-of date]")
826
+ .replaceAll("[start date]", audit.periodStart || "[start date]")
827
+ .replaceAll("[end date]", audit.periodEnd || "[end date]")
828
+ .replaceAll("[engagement date or period]", period)
829
+ .replaceAll("[engagement scope]", audit.scope || "[engagement scope]")
830
+ .replaceAll("[in-scope systems]", systems || "[in-scope systems]")
831
+ .replaceAll("[selected categories]", categories || "[selected categories]")
832
+ .replaceAll(
833
+ "[Carve-out, inclusive, or not applicable]",
834
+ audit.subserviceMethod ? displayValue(audit.subserviceMethod) : "[Carve-out, inclusive, or not applicable]"
835
+ );
836
+ }
837
+
838
+ function escapeRegExp(value) {
839
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
840
+ }
841
+
842
+ function partiesIndependent(ownerIds, approverIds, byId) {
843
+ const owners = partyPeople(ownerIds, byId);
844
+ const approvers = partyPeople(approverIds, byId);
845
+ return owners.size > 0
846
+ && approvers.size > 0
847
+ && ![...owners].some((id) => approvers.has(id));
848
+ }
849
+
850
+ function partyPeople(ids = [], byId, seen = new Set()) {
851
+ const people = new Set();
852
+ for (const id of ids) {
853
+ if (seen.has(id)) continue;
854
+ seen.add(id);
855
+ const record = byId.get(id);
856
+ if (record?.type === "person") people.add(id);
857
+ if (record?.type === "team") {
858
+ for (const personId of partyPeople([...(record.memberIds || []), ...(record.chairIds || [])], byId, seen)) {
859
+ people.add(personId);
860
+ }
861
+ }
862
+ }
863
+ return people;
864
+ }
865
+
866
+ function auditSummary(audit) {
867
+ return {
868
+ id: audit.id,
869
+ title: audit.title,
870
+ status: audit.status,
871
+ kind: audit.auditKind,
872
+ periodStart: audit.periodStart || null,
873
+ periodEnd: audit.periodEnd || null
874
+ };
875
+ }
876
+
877
+ function stage(id, title, description, items) {
878
+ return { id, title, description, items };
879
+ }
880
+
881
+ function item(id, status, title, message, resource = {}) {
882
+ return {
883
+ id,
884
+ status,
885
+ title,
886
+ message,
887
+ ...(resource.type ? { resourceType: resource.type } : {}),
888
+ ...(resource.id ? { resourceId: resource.id } : {})
889
+ };
890
+ }
891
+
892
+ function countStatuses(items) {
893
+ const counts = { complete: 0, action: 0, later: 0, external: 0, info: 0 };
894
+ for (const item of items) counts[item.status] = (counts[item.status] || 0) + 1;
895
+ return counts;
896
+ }
897
+
898
+ function displayValue(value) {
899
+ return String(value || "not started").replaceAll("-", " ").replace(/\b\w/g, (character) => character.toUpperCase());
900
+ }
901
+
902
+ function shiftYear(value, offset) {
903
+ const date = new Date(`${value}T00:00:00Z`);
904
+ date.setUTCFullYear(date.getUTCFullYear() + offset);
905
+ return date.toISOString().slice(0, 10);
906
+ }