filegrc 0.3.4 → 0.4.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.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -66,6 +66,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
66
66
  const requiredAtCreation = fieldList.filter(({ required: isRequired }) => isRequired);
67
67
  const conditionalRequirements = fieldList.filter(({ requiredWhen, required: isRequired }) => requiredWhen && !isRequired);
68
68
  const optionalFields = fieldList.filter(({ required, requiredWhen }) => !required && !requiredWhen);
69
+ const recommendedMarkdown = markdown.filter(({ recommended }) => recommended);
69
70
  const location = definition.singleton
70
71
  ? `data/${definition.singleton}`
71
72
  : `data/${definition.collection}/${(definition.recordPath ?? "{id}.json").replaceAll("{id}", options.id || "{id}")}`;
@@ -91,15 +92,21 @@ export function buildAgentGuide(loaded, type, options = {}) {
91
92
  markdown,
92
93
  workflow: [
93
94
  "Inspect existing records and relation candidates before writing.",
94
- "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
95
- "Keep stable metadata in JSON and put the work performed, inputs, results, decisions, exceptions, and follow-up in the recommended Markdown companion.",
95
+ definition.singleton
96
+ ? "Open the existing singleton record, then replace every null value and empty required array with facts from an authoritative source."
97
+ : "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
98
+ recommendedMarkdown.length
99
+ ? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
100
+ : "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
96
101
  "Run npx filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
97
102
  ],
98
103
  completionChecks: [
99
- "The record describes work that actually occurred; planned work is not marked complete.",
104
+ "Required and status-dependent fields are complete, and the lifecycle status matches the facts.",
100
105
  "Every relationship resolves to the intended existing record.",
101
106
  "Dates describe the business event in the workspace time zone, not the file edit time.",
102
- "Results, exceptions, decisions, evidence, and follow-up records are linked where applicable.",
107
+ ...(recommendedMarkdown.length
108
+ ? ["Required or recommended Markdown explains the work, decisions, results, exceptions, and follow-up that apply."]
109
+ : ["The structured fields state the current fact clearly; optional Record Markdown is added only when needed."]),
103
110
  "No secrets or personal data that may need erasure were added to Git."
104
111
  ]
105
112
  };
@@ -131,14 +138,22 @@ export function scaffoldResourceMutation(loaded, type, title, options = {}) {
131
138
  ...(definition.required ?? [])
132
139
  ]);
133
140
  const record = {
134
- schemaVersion: 1,
135
141
  id,
136
142
  type,
137
143
  title: normalizedTitle
138
144
  };
139
145
  for (const name of required) {
140
146
  if (record[name] !== undefined) continue;
141
- record[name] = scaffoldValue(name, fields[name]);
147
+ record[name] = scaffoldValue(name, fields[name], loaded.model);
148
+ }
149
+ for (const [name, field] of Object.entries(fields)) {
150
+ if (
151
+ record[name] === undefined
152
+ && field.requiredWhen
153
+ && conditionMatches(record, field.requiredWhen)
154
+ ) {
155
+ record[name] = scaffoldValue(name, field, loaded.model);
156
+ }
142
157
  }
143
158
 
144
159
  const slots = markdownEntries(loaded.model, record).filter((slot) => (
@@ -210,17 +225,30 @@ function allowedValues(model, field) {
210
225
  return null;
211
226
  }
212
227
 
213
- function scaffoldValue(name, field = {}) {
228
+ function scaffoldValue(name, field = {}, model) {
214
229
  if (field.const !== undefined) return field.const;
215
230
  if (name === "status" && field.values) {
216
231
  return STARTING_STATUS_ORDER.find((value) => field.values.includes(value)) ?? field.values[0] ?? null;
217
232
  }
218
233
  if (field.type === "array") return [];
219
- if (field.type === "object") return {};
234
+ if (field.type === "object") {
235
+ const schema = model?.objectTypes?.[field.objectType];
236
+ if (!schema) return {};
237
+ return Object.fromEntries((schema.required || []).map((propertyName) => [
238
+ propertyName,
239
+ scaffoldValue(propertyName, schema.properties?.[propertyName], model)
240
+ ]));
241
+ }
220
242
  if (field.type === "boolean") return false;
221
243
  return null;
222
244
  }
223
245
 
246
+ function conditionMatches(record, condition) {
247
+ return Object.entries(condition || {}).every(([name, expected]) => (
248
+ Array.isArray(expected) ? expected.includes(record[name]) : record[name] === expected
249
+ ));
250
+ }
251
+
224
252
  function markdownScaffold(title, type, slot) {
225
253
  const heading = slot.label === "Record" ? title : `${title}: ${slot.label}`;
226
254
  const sections = slot.name === "agenda"
@@ -1,5 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
+ import {
4
+ coverageContains,
5
+ coverageEnd,
6
+ coverageLabel,
7
+ coverageMatches,
8
+ coverageOverlaps,
9
+ coverageStart
10
+ } from "./coverage.js";
3
11
  import { createResource, createResources, deleteResource, updateResource } from "./files.js";
4
12
  import { createResourceId } from "./id.js";
5
13
  import { partiesIndependent } from "./parties.js";
@@ -85,9 +93,8 @@ export async function assessAuditPreparation(input, options = {}) {
85
93
  counts,
86
94
  canInitialize: Boolean(audit
87
95
  && ["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)
88
- && (audit.auditKind === "soc-2-type-1"
89
- ? audit.typeOneAsOf
90
- : audit.periodStart && audit.periodEnd)
96
+ && coverageStart(audit.coverage)
97
+ && coverageEnd(audit.coverage)
91
98
  && initializationNeeded(audit, records, loaded.model)),
92
99
  stages
93
100
  };
@@ -100,10 +107,10 @@ export async function prepareAuditWorkspace(input, options = {}) {
100
107
  if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
101
108
  throw new Error("Audit preparation requires a SOC 2 Type 1 or Type 2 engagement.");
102
109
  }
103
- if (audit.auditKind === "soc-2-type-2" && (!audit.periodStart || !audit.periodEnd)) {
110
+ if (audit.auditKind === "soc-2-type-2" && audit.coverage?.kind !== "range") {
104
111
  throw new Error("Set the Type 2 audit period before initializing audit preparation.");
105
112
  }
106
- if (audit.auditKind === "soc-2-type-1" && !audit.typeOneAsOf) {
113
+ if (audit.auditKind === "soc-2-type-1" && audit.coverage?.kind !== "as-of") {
107
114
  throw new Error("Set the Type 1 as-of date before initializing audit preparation.");
108
115
  }
109
116
 
@@ -167,15 +174,13 @@ export async function prepareAuditWorkspace(input, options = {}) {
167
174
  (system.evidenceSourceKinds || []).includes(template.sourceKind)
168
175
  ));
169
176
  return {
170
- schemaVersion: 1,
171
177
  id,
172
178
  type: "audit-population",
173
179
  title: template.title,
174
180
  status: "planned",
175
181
  auditId: audit.id,
176
182
  populationKind: template.kind,
177
- periodStart: audit.periodStart,
178
- periodEnd: audit.periodEnd,
183
+ coverage: structuredClone(audit.coverage),
179
184
  ownerIds: [...audit.ownerIds],
180
185
  ...(controlIds.length ? { controlIds } : {}),
181
186
  ...(matchingSources.length === 1 ? { sourceSystemId: matchingSources[0].id } : {}),
@@ -219,9 +224,9 @@ function scopeStage(audit, records, byId, programReadiness) {
219
224
  }
220
225
 
221
226
  const periodComplete = audit.auditKind === "soc-2-type-2"
222
- ? audit.periodStart && audit.periodEnd
227
+ ? audit.coverage?.kind === "range"
223
228
  : audit.auditKind === "soc-2-type-1"
224
- ? audit.typeOneAsOf
229
+ ? audit.coverage?.kind === "as-of"
225
230
  : false;
226
231
  items.push(item(
227
232
  "period",
@@ -229,8 +234,8 @@ function scopeStage(audit, records, byId, programReadiness) {
229
234
  "Set the auditor-agreed report type and date",
230
235
  periodComplete
231
236
  ? audit.auditKind === "soc-2-type-2"
232
- ? `Auditor-agreed Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
233
- : `Auditor-agreed Type 1 as-of date: ${audit.typeOneAsOf}.`
237
+ ? `Auditor-agreed Type 2 period: ${coverageLabel(audit.coverage)}.`
238
+ : `Auditor-agreed Type 1 as-of date: ${coverageLabel(audit.coverage)}.`
234
239
  : audit.auditKind === "soc-2-type-1"
235
240
  ? "Set the Type 1 as-of date."
236
241
  : audit.auditKind === "soc-2-type-2"
@@ -238,11 +243,9 @@ function scopeStage(audit, records, byId, programReadiness) {
238
243
  : "Change this readiness record to a Type 1 or Type 2 engagement before planning the report.",
239
244
  audit
240
245
  ));
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
+ if (audit.auditKind === "soc-2-type-2" && programReadiness.target.candidateCoverage) {
247
+ const candidate = coverageLabel(programReadiness.target.candidateCoverage);
248
+ const agreed = coverageLabel(audit.coverage);
246
249
  items.push(item(
247
250
  "candidate-period-comparison",
248
251
  "info",
@@ -257,9 +260,8 @@ function scopeStage(audit, records, byId, programReadiness) {
257
260
  const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
258
261
  const completeSystems = systems.filter((system) => (
259
262
  system.status === "active"
260
- && system.inScope === true
261
263
  && system.description
262
- && system.dataClassification
264
+ && system.classificationId
263
265
  && (system.ownerIds || []).length
264
266
  ));
265
267
  items.push(item(
@@ -272,12 +274,10 @@ function scopeStage(audit, records, byId, programReadiness) {
272
274
  systems[0] || { type: "system" }
273
275
  ));
274
276
 
275
- const engagementStart = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodStart;
277
+ const engagementStart = coverageStart(audit.coverage);
276
278
  const commitments = records.filter((record) => record.type === "commitment"
277
279
  && record.status === "active"
278
- && systems.some((system) => (
279
- (system.commitmentIds || []).includes(record.id) || (record.systemIds || []).includes(system.id)
280
- )));
280
+ && systems.some((system) => (record.systemIds || []).includes(system.id)));
281
281
  const completeCommitments = commitments.filter((commitment) => (
282
282
  commitment.statement
283
283
  && (commitment.ownerIds || []).length
@@ -287,7 +287,7 @@ function scopeStage(audit, records, byId, programReadiness) {
287
287
  && (commitment.controlIds || []).length
288
288
  ));
289
289
  const systemsWithoutCommitments = systems.filter((system) => !completeCommitments.some((commitment) => (
290
- (system.commitmentIds || []).includes(commitment.id) || (commitment.systemIds || []).includes(system.id)
290
+ (commitment.systemIds || []).includes(system.id)
291
291
  )));
292
292
  items.push(item(
293
293
  "commitments",
@@ -432,7 +432,7 @@ function engagementStage(audit, byId, programReadiness) {
432
432
  ]);
433
433
  }
434
434
  const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
435
- const named = Boolean(auditor || hasMeaningfulValue(audit.auditor));
435
+ const named = Boolean(auditor);
436
436
  return stage("engagement", "Engage the Auditor", "Record the independent CPA firm and engagement contacts before treating the audit as active.", [
437
437
  item(
438
438
  "engagement-record",
@@ -446,7 +446,7 @@ function engagementStage(audit, byId, programReadiness) {
446
446
  named ? "complete" : "action",
447
447
  "Record the independent CPA firm",
448
448
  named
449
- ? `${auditor?.title || audit.auditor?.firm || "The independent CPA firm"} is recorded for the engagement.`
449
+ ? `${auditor.title} is recorded for the engagement.`
450
450
  : "Select the CPA firm and record it here. The independent management policy reviewer is a different role.",
451
451
  audit
452
452
  )
@@ -479,7 +479,7 @@ async function documentsStage(loaded, audit, byId) {
479
479
  const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
480
480
  const source = document ? await primaryMarkdown(loaded, document) : "";
481
481
  const contentIssues = managementDocumentContentIssues(source, definition, audit);
482
- const engagementEnd = audit?.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit?.periodEnd;
482
+ const engagementEnd = coverageEnd(audit?.coverage);
483
483
  if (document?.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
484
484
  contentIssues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
485
485
  }
@@ -543,11 +543,11 @@ function evidenceStage(audit, records, byId, model) {
543
543
  const evidence = records.filter((record) => record.type === "evidence");
544
544
  const externalEvidence = evidence.filter((record) => (
545
545
  record.status === "verified"
546
- && (record.evidenceKind !== "rendered-record" || record.sourceCommit)
546
+ && (record.artifactKind !== "rendered-page" || record.sourceCommit)
547
547
  && evidenceRelevantToAuditDate(record, audit)
548
548
  ));
549
- const managedFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.collectionTestRequired === false);
550
- const externalFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.collectionTestRequired !== false);
549
+ const managedFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.filegrcManaged === true);
550
+ const externalFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.filegrcManaged !== true);
551
551
  const evidenceFamiliesFor = (control) => (model.evidenceSourceFamilies || []).filter((family) => (
552
552
  (family.controlCodes || []).includes(control.code)
553
553
  ));
@@ -574,7 +574,7 @@ function evidenceStage(audit, records, byId, model) {
574
574
  managedControls.length && controlsWithFilegrcRecords.length === managedControls.length ? "complete" : managedControls.length ? "action" : "info",
575
575
  "Review filegrc Evidence",
576
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.`
577
+ ? `${controlsWithFilegrcRecords.length} of ${managedControls.length} selected controls that use filegrc workflows have a dated operating record for the formal period. Complete each Step 4 record, link it to the control, and add results in its structured fields or Markdown.`
578
578
  : "No selected controls use a dedicated filegrc operating record.",
579
579
  filegrcRecords[0] || { type: managedFamilies[0]?.operationRecordTypes?.[0] || "control" }
580
580
  ),
@@ -600,7 +600,7 @@ function evidenceStage(audit, records, byId, model) {
600
600
  ));
601
601
  continue;
602
602
  }
603
- if (source.collectionTestRequired === false) {
603
+ if (source.filegrcManaged === true) {
604
604
  const sourceRecords = filegrcRecords.filter((record) => (
605
605
  relevantControls.some((control) => controlIdsForRecord(record, byId).has(control.id))
606
606
  ));
@@ -613,7 +613,7 @@ function evidenceStage(audit, records, byId, model) {
613
613
  source.title,
614
614
  coveredControls.length === relevantControls.length
615
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.`,
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 4 work and attach or reference any supporting external artifact on that record.`,
617
617
  sourceRecords[0] || { type: source.operationRecordTypes[0] }
618
618
  ));
619
619
  continue;
@@ -687,7 +687,11 @@ function auditorStage() {
687
687
 
688
688
  function populationResult(population, audit, byId) {
689
689
  if (!population) return { status: "action", message: "Initialize this population for the engagement." };
690
- if (population.periodStart !== audit?.periodStart || population.periodEnd !== audit?.periodEnd) {
690
+ if (!coverageMatches(
691
+ population.coverage,
692
+ coverageStart(audit?.coverage),
693
+ coverageEnd(audit?.coverage)
694
+ )) {
691
695
  return { status: "action", message: "The population period does not match the exact audit period." };
692
696
  }
693
697
  if (population.status === "not-applicable") {
@@ -712,12 +716,15 @@ function populationResult(population, audit, byId) {
712
716
  ];
713
717
  const evidenceComplete = evidence
714
718
  && evidence.type === "evidence"
715
- && evidence.evidenceKind === "population-export"
719
+ && evidence.artifactKind === "population-export"
716
720
  && evidence.status === "verified"
717
721
  && population.sourceSystemId
718
722
  && evidence.sourceSystemId === population.sourceSystemId
719
- && evidence.periodStart === audit.periodStart
720
- && evidence.periodEnd === audit.periodEnd
723
+ && coverageMatches(
724
+ evidence.coverage,
725
+ coverageStart(audit.coverage),
726
+ coverageEnd(audit.coverage)
727
+ )
721
728
  && requiredEvidence.every((field) => evidence[field] !== undefined && evidence[field] !== null && evidence[field] !== "");
722
729
  const reconciliationComplete = (population.reconciledByIds || []).length
723
730
  && population.reconciledOn
@@ -726,14 +733,14 @@ function populationResult(population, audit, byId) {
726
733
  const generatedOn = timestampDate(evidence?.generatedAt, evidence?.timezone);
727
734
  const sequenceComplete = Number.isInteger(evidence?.populationCount)
728
735
  && evidence.populationCount >= 0
729
- && generatedOn > audit.periodEnd
736
+ && generatedOn > coverageEnd(audit.coverage)
730
737
  && population.reconciledOn >= generatedOn;
731
738
  if (!evidenceComplete || !reconciliationComplete || !sequenceComplete) {
732
739
  return { status: "action", message: "Finish the reconciliation and link a verified population export with its exact query, timezone, count, completeness check, and accuracy check." };
733
740
  }
734
741
  return {
735
742
  status: "complete",
736
- message: `${evidence.populationCount} items reconciled from ${evidence.source || "the authoritative source"}${population.conclusion === "complete-with-exceptions" ? " with documented exceptions" : ""}.`
743
+ message: `${evidence.populationCount} items reconciled from ${evidence.sourceDescription || "the authoritative source"}${population.conclusion === "complete-with-exceptions" ? " with documented exceptions" : ""}.`
737
744
  };
738
745
  }
739
746
 
@@ -792,31 +799,30 @@ function applicableManagementDocuments(audit, readiness) {
792
799
  }
793
800
 
794
801
  function evidenceOverlaps(record, start, end) {
795
- return (record.periodStart && record.periodEnd && record.periodStart <= end && record.periodEnd >= start)
802
+ return coverageOverlaps(record.coverage, start, end)
796
803
  || (record.collectedOn && record.collectedOn >= start && record.collectedOn <= end);
797
804
  }
798
805
 
799
806
  function evidenceRelevantToAuditDate(record, audit) {
800
807
  if (!audit) return false;
801
808
  if (audit.auditKind === "soc-2-type-1") {
802
- const date = audit.typeOneAsOf;
809
+ const date = coverageStart(audit.coverage);
803
810
  return Boolean(date) && (
804
- (record.periodStart && record.periodEnd && record.periodStart <= date && record.periodEnd >= date)
811
+ coverageContains(record.coverage, date)
805
812
  || record.collectedOn === date
806
813
  );
807
814
  }
808
- return Boolean(audit.periodStart && audit.periodEnd)
809
- && evidenceOverlaps(record, audit.periodStart, audit.periodEnd);
815
+ const start = coverageStart(audit.coverage);
816
+ const end = coverageEnd(audit.coverage);
817
+ return Boolean(start && end) && evidenceOverlaps(record, start, end);
810
818
  }
811
819
 
812
820
  function recordRelevantToAuditDate(record, audit, model) {
813
821
  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;
822
+ const start = coverageStart(audit.coverage);
823
+ const end = coverageEnd(audit.coverage);
816
824
  if (!start || !end) return false;
817
- if (record.periodStart && record.periodEnd && record.periodStart <= end && record.periodEnd >= start) {
818
- return true;
819
- }
825
+ if (coverageOverlaps(record.coverage, start, end)) return true;
820
826
  const definition = model.resources[record.type];
821
827
  const fields = { ...model.commonFields, ...(definition?.fields || {}) };
822
828
  return Object.entries(fields).some(([name, field]) => {
@@ -872,9 +878,7 @@ function managementDocumentContentIssues(source, definition, audit) {
872
878
  ));
873
879
  if (missingHeadings.length) return [`Add the missing description sections: ${missingHeadings.join(", ")}.`];
874
880
  if (definition.dateBinding === "engagement" && audit) {
875
- const dates = audit.auditKind === "soc-2-type-1"
876
- ? [audit.typeOneAsOf]
877
- : [audit.periodStart, audit.periodEnd];
881
+ const dates = [coverageStart(audit.coverage), coverageEnd(audit.coverage)];
878
882
  if (dates.some((date) => date && !source.includes(date))) {
879
883
  return ["Name the exact engagement date or period in the document."];
880
884
  }
@@ -923,16 +927,16 @@ function materializeManagementMarkdown(source, audit, records) {
923
927
  .map(displayValue))]
924
928
  .join(", ");
925
929
  const period = audit.auditKind === "soc-2-type-1"
926
- ? audit.typeOneAsOf || "[as-of date]"
927
- : audit.periodStart && audit.periodEnd
928
- ? `${audit.periodStart} through ${audit.periodEnd}`
930
+ ? coverageStart(audit.coverage) || "[as-of date]"
931
+ : coverageStart(audit.coverage) && coverageEnd(audit.coverage)
932
+ ? coverageLabel(audit.coverage)
929
933
  : "[start date] through [end date]";
930
934
  return withoutDiscarded
931
935
  .replaceAll(`<!-- ${keep}:start -->`, "")
932
936
  .replaceAll(`<!-- ${keep}:end -->`, "")
933
- .replaceAll("[as-of date]", audit.typeOneAsOf || "[as-of date]")
934
- .replaceAll("[start date]", audit.periodStart || "[start date]")
935
- .replaceAll("[end date]", audit.periodEnd || "[end date]")
937
+ .replaceAll("[as-of date]", coverageStart(audit.coverage) || "[as-of date]")
938
+ .replaceAll("[start date]", coverageStart(audit.coverage) || "[start date]")
939
+ .replaceAll("[end date]", coverageEnd(audit.coverage) || "[end date]")
936
940
  .replaceAll("[engagement date or period]", period)
937
941
  .replaceAll("[engagement scope]", audit.scope || "[engagement scope]")
938
942
  .replaceAll("[in-scope systems]", systems || "[in-scope systems]")
@@ -953,8 +957,7 @@ function auditSummary(audit) {
953
957
  title: audit.title,
954
958
  status: audit.status,
955
959
  kind: audit.auditKind,
956
- periodStart: audit.periodStart || null,
957
- periodEnd: audit.periodEnd || null
960
+ coverage: audit.coverage || null
958
961
  };
959
962
  }
960
963