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