filegrc 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/Sunpeak-AI/FileGRC.git"
8
+ "url": "git+https://github.com/Sunpeak-AI/filegrc.git"
9
9
  },
10
10
  "type": "module",
11
11
  "bin": {
package/src/agent.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createResourceId } from "./id.js";
2
2
  import { markdownEntries } from "./resource-markdown.js";
3
+ import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
3
4
 
4
5
  const STARTING_STATUS_ORDER = [
5
6
  "draft",
@@ -73,7 +74,10 @@ export function buildAgentGuide(loaded, type, options = {}) {
73
74
  type,
74
75
  title: definition.title,
75
76
  pluralTitle: definition.pluralTitle,
77
+ instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
78
+ use: definition.description,
76
79
  purpose: definition.description,
80
+ programStep: resourceProgramContext(type),
77
81
  policyBasis: definition.guidance.policyBasis,
78
82
  cadence: definition.guidance.cadence,
79
83
  policySourceIds: definition.guidance.sourceResourceIds ?? [],
@@ -2,10 +2,36 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { createResource, createResources, deleteResource, updateResource } from "./files.js";
4
4
  import { createResourceId } from "./id.js";
5
+ import { partiesIndependent } from "./parties.js";
5
6
  import { resolveDataPath } from "./paths.js";
7
+ import { assessProgramReadiness } from "./program-readiness.js";
6
8
  import { markdownEntries } from "./resource-markdown.js";
7
9
  import { loadWorkspace } from "./workspace.js";
8
10
 
11
+ const NON_EVIDENCE_RECORD_TYPES = new Set([
12
+ "audit",
13
+ "audit-population",
14
+ "audit-request",
15
+ "commitment",
16
+ "complementary-control",
17
+ "control",
18
+ "control-test",
19
+ "document",
20
+ "evidence",
21
+ "framework",
22
+ "obligation",
23
+ "organization",
24
+ "person",
25
+ "policy",
26
+ "renderer-settings",
27
+ "requirement",
28
+ "system",
29
+ "team",
30
+ "training",
31
+ "vendor",
32
+ "workspace"
33
+ ]);
34
+
9
35
  export async function assessAuditPreparation(input, options = {}) {
10
36
  const loaded = input?.resources && input?.model && input?.entries
11
37
  ? input
@@ -20,13 +46,23 @@ export async function assessAuditPreparation(input, options = {}) {
20
46
  : audits.find((record) => !["complete", "closed", "canceled"].includes(record.status)) || audits[0];
21
47
  if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
22
48
 
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));
49
+ const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
50
+ generatedAt: options.generatedAt
51
+ });
52
+ const stages = [
53
+ programFoundationStage(programReadiness, loaded.workspace),
54
+ engagementStage(audit, byId, programReadiness),
55
+ scopeStage(audit, records, byId, programReadiness)
56
+ ];
57
+ const fieldworkSections = audit
58
+ ? [
59
+ await documentsStage(loaded, audit, byId),
60
+ evidenceStage(audit, records, byId, loaded.model),
61
+ populationsStage(audit, records, byId, loaded.model)
62
+ ]
63
+ : [];
64
+ stages.push(fieldworkStage(audit, fieldworkSections));
65
+ stages.push(auditorStage());
30
66
 
31
67
  for (const stage of stages) {
32
68
  stage.counts = countStatuses(stage.items);
@@ -40,7 +76,7 @@ export async function assessAuditPreparation(input, options = {}) {
40
76
  schemaVersion: 1,
41
77
  generatedAt: options.generatedAt || new Date().toISOString(),
42
78
  audit: audit ? auditSummary(audit) : null,
43
- status: counts.action ? "needs-work" : audit ? "management-ready" : "not-started",
79
+ status: !audit ? "not-started" : counts.action ? "needs-work" : "management-ready",
44
80
  progress: {
45
81
  complete: completedManagedItems.length,
46
82
  total: managedItems.length,
@@ -169,19 +205,17 @@ export async function prepareAuditWorkspace(input, options = {}) {
169
205
  };
170
206
  }
171
207
 
172
- function scopeStage(audit, records, byId) {
208
+ function scopeStage(audit, records, byId, programReadiness) {
173
209
  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
210
  if (!audit) {
184
- return stage("scope", "Scope and Engagement", "Define the report, service boundary, criteria, dependencies, and CPA firm.", items);
211
+ items.push(item(
212
+ "formal-period",
213
+ "later",
214
+ "Confirm the formal report scope and period",
215
+ "Create the engagement after selecting a CPA firm. Keep operating the management program and preserving evidence in the meantime.",
216
+ { type: "audit" }
217
+ ));
218
+ return stage("period", "Confirm the Formal Period", "Record the auditor-agreed report scope and dates without overwriting management's candidate period.", items);
185
219
  }
186
220
 
187
221
  const periodComplete = audit.auditKind === "soc-2-type-2"
@@ -192,11 +226,11 @@ function scopeStage(audit, records, byId) {
192
226
  items.push(item(
193
227
  "period",
194
228
  periodComplete ? "complete" : "action",
195
- "Set the report type and date",
229
+ "Set the auditor-agreed report type and date",
196
230
  periodComplete
197
231
  ? audit.auditKind === "soc-2-type-2"
198
- ? `Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
199
- : `Type 1 as-of date: ${audit.typeOneAsOf}.`
232
+ ? `Auditor-agreed Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
233
+ : `Auditor-agreed Type 1 as-of date: ${audit.typeOneAsOf}.`
200
234
  : audit.auditKind === "soc-2-type-1"
201
235
  ? "Set the Type 1 as-of date."
202
236
  : audit.auditKind === "soc-2-type-2"
@@ -204,6 +238,21 @@ function scopeStage(audit, records, byId) {
204
238
  : "Change this readiness record to a Type 1 or Type 2 engagement before planning the report.",
205
239
  audit
206
240
  ));
241
+ if (audit.auditKind === "soc-2-type-2" && programReadiness.target.candidatePeriodStart) {
242
+ const candidate = [programReadiness.target.candidatePeriodStart, programReadiness.target.candidatePeriodEnd]
243
+ .filter(Boolean)
244
+ .join(" through ");
245
+ const agreed = [audit.periodStart, audit.periodEnd].filter(Boolean).join(" through ");
246
+ items.push(item(
247
+ "candidate-period-comparison",
248
+ "info",
249
+ "Compare candidate and auditor-agreed periods",
250
+ agreed
251
+ ? `Management candidate: ${candidate}. Auditor agreed: ${agreed}. Preserve both sets of dates when they differ.`
252
+ : `Management candidate: ${candidate}. The formal period remains unset until the CPA firm agrees to it.`,
253
+ audit
254
+ ));
255
+ }
207
256
 
208
257
  const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
209
258
  const completeSystems = systems.filter((system) => (
@@ -350,87 +399,79 @@ function scopeStage(audit, records, byId) {
350
399
  audit
351
400
  ));
352
401
 
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);
402
+ return stage("period", "Confirm the Formal Period", "Record the auditor-agreed report type, date or period, scope, criteria, systems, and dependency treatment.", items);
365
403
  }
366
404
 
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.", [
405
+ function programFoundationStage(programReadiness, workspace) {
406
+ const ready = programReadiness.evidenceReady;
407
+ return stage("program", "Program Readiness", "The management program can be prepared and operated without an audit record or CPA firm.", [
404
408
  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
- ),
409
+ "evidence-ready",
410
+ ready ? "complete" : "action",
411
+ "Reach the Evidence Ready gate",
412
+ ready
413
+ ? `${programReadiness.target.label} is evidence-ready. ${programReadiness.operating ? "Evidence collection is running." : "Management can begin the candidate period."}`
414
+ : `${programReadiness.counts.action} program-readiness actions remain across scope, policies, controls, and evidence preparation.`,
415
+ workspace || { type: "workspace" }
416
+ )
417
+ ]);
418
+ }
419
+
420
+ function engagementStage(audit, byId, programReadiness) {
421
+ if (!audit) {
422
+ return stage("engagement", "Engage the Auditor", "Select a CPA firm after the program is evidence-ready, or earlier when a customer deadline makes coordination urgent.", [
423
+ item(
424
+ "engagement",
425
+ programReadiness.evidenceReady ? "action" : "later",
426
+ "Create the CPA engagement",
427
+ programReadiness.evidenceReady
428
+ ? "Select the independent CPA firm, sign the engagement, then create the audit record with the firm and contacts."
429
+ : "Finish the management program first. Early auditor engagement remains available when a customer deadline requires it.",
430
+ { type: "audit" }
431
+ )
432
+ ]);
433
+ }
434
+ const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
435
+ const named = Boolean(auditor || hasMeaningfulValue(audit.auditor));
436
+ return stage("engagement", "Engage the Auditor", "Record the independent CPA firm and engagement contacts before treating the audit as active.", [
413
437
  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" }
438
+ "engagement-record",
439
+ "complete",
440
+ "Create the engagement record",
441
+ `${audit.title} tracks the formal scope, dates, requests, fieldwork, and report.`,
442
+ audit
421
443
  ),
422
444
  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" }
445
+ "auditor",
446
+ named ? "complete" : "action",
447
+ "Record the independent CPA firm",
448
+ named
449
+ ? `${auditor?.title || audit.auditor?.firm || "The independent CPA firm"} is recorded for the engagement.`
450
+ : "Select the CPA firm and record it here. The independent management policy reviewer is a different role.",
451
+ audit
430
452
  )
431
453
  ]);
432
454
  }
433
455
 
456
+ function fieldworkStage(audit, sections) {
457
+ if (!audit) {
458
+ return stage("fieldwork", "Prepare Fieldwork", "Build engagement-specific documents, exact-period evidence, and Type 2 populations after the firm and period are recorded.", [
459
+ item("fieldwork-later", "later", "Prepare engagement-specific fieldwork", "This work starts after the CPA engagement and formal period exist.", { type: "audit" })
460
+ ]);
461
+ }
462
+ const items = sections.flatMap((section) => section.items.map((current) => ({
463
+ ...current,
464
+ id: `${section.id}-${current.id}`,
465
+ section: section.title
466
+ })));
467
+ return stage(
468
+ "fieldwork",
469
+ "Prepare Fieldwork",
470
+ "Complete management documents, exact-period operating evidence, and Type 2 population reconciliations for the engagement.",
471
+ items
472
+ );
473
+ }
474
+
434
475
  async function documentsStage(loaded, audit, byId) {
435
476
  const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
436
477
  const items = [];
@@ -500,27 +541,55 @@ async function documentsStage(loaded, audit, byId) {
500
541
  function evidenceStage(audit, records, byId, model) {
501
542
  const controls = (audit?.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
502
543
  const evidence = records.filter((record) => record.type === "evidence");
503
- const periodEvidence = evidence.filter((record) => (
544
+ const externalEvidence = evidence.filter((record) => (
504
545
  record.status === "verified"
505
546
  && (record.evidenceKind !== "rendered-record" || record.sourceCommit)
506
547
  && evidenceRelevantToAuditDate(record, audit)
507
548
  ));
508
- const controlsWithEvidence = controls.filter((control) => periodEvidence.some((record) => (
549
+ const managedFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.collectionTestRequired === false);
550
+ const externalFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.collectionTestRequired !== false);
551
+ const evidenceFamiliesFor = (control) => (model.evidenceSourceFamilies || []).filter((family) => (
552
+ (family.controlCodes || []).includes(control.code)
553
+ ));
554
+ const filegrcRecords = records.filter((record) => (
555
+ !NON_EVIDENCE_RECORD_TYPES.has(record.type)
556
+ && controlIdsForRecord(record, byId).size
557
+ && recordRelevantToAuditDate(record, audit, model)
558
+ ));
559
+ const managedControls = controls.filter((control) => managedFamilies.some((family) => (
560
+ (family.controlCodes || []).includes(control.code)
561
+ )));
562
+ const controlsWithFilegrcRecords = managedControls.filter((control) => filegrcRecords.some((record) => (
563
+ controlIdsForRecord(record, byId).has(control.id)
564
+ )));
565
+ const externalControls = controls.filter((control) => externalFamilies.some((family) => (
566
+ (family.controlCodes || []).includes(control.code)
567
+ )) || !evidenceFamiliesFor(control).length);
568
+ const controlsWithExternalEvidence = externalControls.filter((control) => externalEvidence.some((record) => (
509
569
  controlIdsForRecord(record, byId).has(control.id)
510
570
  )));
511
571
  const items = [
512
572
  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" }
573
+ "filegrc-evidence",
574
+ managedControls.length && controlsWithFilegrcRecords.length === managedControls.length ? "complete" : managedControls.length ? "action" : "info",
575
+ "Review filegrc Evidence",
576
+ managedControls.length
577
+ ? `${controlsWithFilegrcRecords.length} of ${managedControls.length} selected controls that use filegrc workflows have a dated operating record for the formal period. Complete each Step 5 record, link it to the control, and add results in its structured fields or Markdown.`
578
+ : "No selected controls use a dedicated filegrc operating record.",
579
+ filegrcRecords[0] || { type: managedFamilies[0]?.operationRecordTypes?.[0] || "control" }
580
+ ),
581
+ item(
582
+ "external-evidence",
583
+ externalControls.length && controlsWithExternalEvidence.length === externalControls.length ? "complete" : externalControls.length ? "action" : "info",
584
+ "Review External Evidence",
585
+ externalControls.length
586
+ ? `${controlsWithExternalEvidence.length} of ${externalControls.length} selected controls that rely on external systems have verified External Evidence for the formal period. Confirm the source System, date or period, control links, collector, verifier, and retained artifact or approved external reference.`
587
+ : "No selected controls require a separate External Evidence record.",
588
+ externalEvidence[0] || { type: "evidence" }
520
589
  )
521
590
  ];
522
591
  const systems = records.filter((record) => record.type === "system" && record.status === "active");
523
- for (const source of model.auditReadiness?.externalEvidence || []) {
592
+ for (const source of model.evidenceSourceFamilies || []) {
524
593
  const relevantControls = controls.filter((control) => (source.controlCodes || []).includes(control.code));
525
594
  if (!relevantControls.length) {
526
595
  items.push(item(
@@ -531,10 +600,28 @@ function evidenceStage(audit, records, byId, model) {
531
600
  ));
532
601
  continue;
533
602
  }
603
+ if (source.collectionTestRequired === false) {
604
+ const sourceRecords = filegrcRecords.filter((record) => (
605
+ relevantControls.some((control) => controlIdsForRecord(record, byId).has(control.id))
606
+ ));
607
+ const coveredControls = relevantControls.filter((control) => sourceRecords.some((record) => (
608
+ controlIdsForRecord(record, byId).has(control.id)
609
+ )));
610
+ items.push(item(
611
+ `filegrc-${source.id}`,
612
+ coveredControls.length === relevantControls.length ? "complete" : "action",
613
+ source.title,
614
+ coveredControls.length === relevantControls.length
615
+ ? `${sourceRecords.length} dated filegrc ${sourceRecords.length === 1 ? "record" : "records"} cover ${relevantControls.length} mapped controls. External artifacts needed to support those results are linked from the operating records.`
616
+ : `${coveredControls.length} of ${relevantControls.length} mapped controls have a dated ${source.operationRecordTypes.map(displayValue).join(" or ")} record for the formal period. Complete the Step 5 work and attach or reference any supporting external artifact on that record.`,
617
+ sourceRecords[0] || { type: source.operationRecordTypes[0] }
618
+ ));
619
+ continue;
620
+ }
534
621
  const sourceSystems = systems.filter((system) => (
535
622
  (system.evidenceSourceKinds || []).some((kind) => (source.sourceKinds || []).includes(kind))
536
623
  ));
537
- const coveredControls = relevantControls.filter((control) => periodEvidence.some((record) => (
624
+ const coveredControls = relevantControls.filter((control) => externalEvidence.some((record) => (
538
625
  sourceSystems.some((system) => system.id === record.sourceSystemId)
539
626
  && controlIdsForRecord(record, byId).has(control.id)
540
627
  )));
@@ -549,12 +636,12 @@ function evidenceStage(audit, records, byId, model) {
549
636
  status,
550
637
  source.title,
551
638
  message,
552
- periodEvidence.find((record) => sourceSystems.some((system) => system.id === record.sourceSystemId))
639
+ externalEvidence.find((record) => sourceSystems.some((system) => system.id === record.sourceSystemId))
553
640
  || sourceSystems[0]
554
641
  || { type: "system" }
555
642
  ));
556
643
  }
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);
644
+ return stage("evidence", "Audit Evidence", "Review both evidence paths: dated filegrc operating records and verified External Evidence from authoritative systems. filegrc includes both in the audit packet.", items);
558
645
  }
559
646
 
560
647
  function populationsStage(audit, records, byId, model) {
@@ -589,12 +676,12 @@ function populationsStage(audit, records, byId, model) {
589
676
  );
590
677
  }
591
678
 
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.", [
679
+ function auditorStage() {
680
+ return stage("auditor", "Fieldwork and Report", "filegrc prepares the record set but does not make the CPA firm's independent judgments.", [
594
681
  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
682
  item("sampling", "external", "Sample selection and independent testing", "The auditor chooses samples, performs tests, evaluates exceptions, and decides whether more work is needed."),
596
683
  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.")
684
+ 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
685
  ]);
599
686
  }
600
687
 
@@ -722,6 +809,27 @@ function evidenceRelevantToAuditDate(record, audit) {
722
809
  && evidenceOverlaps(record, audit.periodStart, audit.periodEnd);
723
810
  }
724
811
 
812
+ function recordRelevantToAuditDate(record, audit, model) {
813
+ if (!audit) return false;
814
+ const start = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodStart;
815
+ const end = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodEnd;
816
+ if (!start || !end) return false;
817
+ if (record.periodStart && record.periodEnd && record.periodStart <= end && record.periodEnd >= start) {
818
+ return true;
819
+ }
820
+ const definition = model.resources[record.type];
821
+ const fields = { ...model.commonFields, ...(definition?.fields || {}) };
822
+ return Object.entries(fields).some(([name, field]) => {
823
+ const value = record[name];
824
+ if (field.type === "date") return value >= start && value <= end;
825
+ if (field.type === "timestamp" && typeof value === "string") {
826
+ const date = value.slice(0, 10);
827
+ return date >= start && date <= end;
828
+ }
829
+ return false;
830
+ });
831
+ }
832
+
725
833
  function controlIdsForRecord(record, byId, seen = new Set()) {
726
834
  const ids = new Set();
727
835
  if (!record || seen.has(record.id)) return ids;
@@ -839,30 +947,6 @@ function escapeRegExp(value) {
839
947
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
840
948
  }
841
949
 
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
950
  function auditSummary(audit) {
867
951
  return {
868
952
  id: audit.id,
@@ -898,9 +982,3 @@ function countStatuses(items) {
898
982
  function displayValue(value) {
899
983
  return String(value || "not started").replaceAll("-", " ").replace(/\b\w/g, (character) => character.toUpperCase());
900
984
  }
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
- }