filegrc 0.11.0 → 0.12.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,542 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ getDataRecordHistoryIndex,
4
+ getFileAtRevision,
5
+ getFileObjectIdAtRevision,
6
+ getGitSummary,
7
+ getWorkingFileObjectId,
8
+ hasGitRevision,
9
+ isDataHistoryAncestor
10
+ } from "./git.js";
11
+ import { addCalendarDays } from "./recurrence.js";
12
+ import { coverageContains } from "./coverage.js";
13
+ import { appointmentWasAuthorizedOn } from "./soc2.js";
14
+ import { isRfc3339Timestamp, localDateTimeValue, timestampFromLocalDateTime } from "./time.js";
15
+
16
+ const CONTEMPORANEOUS_COMMIT_WINDOW_MS = 86_400_000;
17
+
18
+ export function reportingRouteRevision(record) {
19
+ const effectiveFacts = {
20
+ id: record.id,
21
+ type: record.type,
22
+ title: record.title,
23
+ purpose: record.purpose,
24
+ priority: record.priority,
25
+ channelKind: record.channelKind,
26
+ route: record.route,
27
+ effectiveAt: record.effectiveAt,
28
+ dependencySystemIds: record.dependencySystemIds,
29
+ approvedByIds: record.approvedByIds,
30
+ approvedOn: record.approvedOn,
31
+ sourceResourceIds: record.sourceResourceIds,
32
+ ownerIds: record.ownerIds
33
+ };
34
+ return createHash("sha256").update(JSON.stringify(effectiveFacts)).digest("hex");
35
+ }
36
+
37
+ export function reportingRouteEventAuthorityIssue(records, routeSet, options = {}) {
38
+ const appointment = records.find(({ type, id }) => type === "appointment" && id === options.appointmentId);
39
+ if (!appointment) {
40
+ return { code: "invalid-reporting-route-authority", message: `Approval Appointment "${options.appointmentId || ""}" was not found.` };
41
+ }
42
+ if (appointment.appointmentKind !== routeSet.approvalAppointmentKind) {
43
+ return {
44
+ code: "invalid-reporting-route-authority",
45
+ message: `Approval Appointment "${appointment.id}" must use the authorized ${routeSet.approvalAppointmentKind} Appointment kind.`
46
+ };
47
+ }
48
+ if (appointment.appointmentKind === routeSet.authorityAppointmentKind) {
49
+ return { code: "reporting-route-authority-not-separated", message: "Approval and ongoing route authority must use separate Appointment kinds." };
50
+ }
51
+ let at;
52
+ let timezone;
53
+ try {
54
+ at = instant(options.at, "Reporting Route authority time");
55
+ timezone = timezoneName(options.timezone);
56
+ } catch (error) {
57
+ return { code: "invalid-reporting-route-authority", message: error.message };
58
+ }
59
+ const date = localDateTimeValue(at, timezone).slice(0, 10);
60
+ const byId = new Map(records.map((record) => [record.id, record]));
61
+ if (!appointmentWasAuthorizedOn(appointment, date, byId)) {
62
+ return {
63
+ code: "invalid-reporting-route-authority",
64
+ message: `Approval Appointment "${appointment.id}" and its holder were not authorized at ${at.toISOString()}.`
65
+ };
66
+ }
67
+ if (!arrayValue(appointment.scopeResourceIds).some((id) => [routeSet.id, routeSet.programId, options.workspaceId].includes(id))) {
68
+ return {
69
+ code: "invalid-reporting-route-authority",
70
+ message: `Approval Appointment "${appointment.id}" does not cover this Route Set, Program, or Workspace.`
71
+ };
72
+ }
73
+ if (options.actorId !== appointment.holderId) {
74
+ return { code: "invalid-reporting-route-authority", message: "The approving Person must be the Appointment holder at the asserted event time." };
75
+ }
76
+ if (reportingRouteOngoingAuthorities(records, routeSet, at, options.workspaceId).some(({ holderId }) => holderId === appointment.holderId)) {
77
+ return {
78
+ code: "reporting-route-authority-not-separated",
79
+ message: "The approving Person must be independent from the Person responsible for the reporting channels at the event time."
80
+ };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ export function reportingRouteAssertionTiming(loaded, routeSet, eventName) {
86
+ const event = routeSet[eventName];
87
+ const eventAt = eventName === "approval" ? event?.approvedAt : event?.canceledAt;
88
+ if (!eventAt) return null;
89
+ const history = [...reportingRouteHistory(loaded, routeSet.id)].reverse();
90
+ for (const commit of history) {
91
+ const source = getFileAtRevision(loaded.root, commit.commit, commit.path);
92
+ if (!source) continue;
93
+ try {
94
+ if (JSON.parse(source)[eventName]) {
95
+ return assertionTimingAt(new Date(eventAt), new Date(commit.timestamp));
96
+ }
97
+ } catch {
98
+ continue;
99
+ }
100
+ }
101
+ return assertionTimingAt(new Date(eventAt), new Date());
102
+ }
103
+
104
+ export function reportingRouteFixedEvidence(records, subjectId, evidenceIds, at, timezone = "UTC", options = {}) {
105
+ let date;
106
+ try {
107
+ date = /^\d{4}-\d{2}-\d{2}$/.test(String(at || ""))
108
+ ? String(at)
109
+ : localDateTimeValue(instant(at, "Supported event time"), timezone).slice(0, 10);
110
+ } catch {
111
+ return [];
112
+ }
113
+ const selected = new Set(arrayValue(evidenceIds));
114
+ const personIds = new Set(options.personIds || records.filter(({ type }) => type === "person").map(({ id }) => id));
115
+ return records.filter((evidence) => {
116
+ if (
117
+ !selected.has(evidence.id)
118
+ || evidence.type !== "evidence"
119
+ || evidence.status !== "verified"
120
+ || !arrayValue(evidence.sourceResourceIds).includes(subjectId)
121
+ || !verifiedEvidenceComplete(evidence, personIds)
122
+ ) return false;
123
+ const coversDate = coverageContains(evidence.coverage, date)
124
+ || [evidence.businessEventAt, evidence.sourceGeneratedAt].filter(Boolean).some((value) => {
125
+ try { return localDateTimeValue(new Date(value), timezone).slice(0, 10) === date; } catch { return false; }
126
+ });
127
+ const filePaths = arrayValue(evidence.filePaths);
128
+ const hasFileMaterial = evidence.sourceKind === "file" && filePaths.length && (!options.root || filePaths.every((path) => {
129
+ const relativePath = `data/${path}`;
130
+ if (options.commit) return Boolean(getFileObjectIdAtRevision(options.root, options.commit, relativePath));
131
+ return Boolean(getWorkingFileObjectId(options.root, relativePath));
132
+ }));
133
+ const authoritativeCommit = options.commit || (options.root ? getGitSummary(options.root).commit : null);
134
+ const hasFixedMaterial = hasFileMaterial
135
+ || (
136
+ evidence.sourceKind === "rendered-page"
137
+ && evidence.artifactKind === "rendered-page"
138
+ && evidence.capture
139
+ && coverageContains(evidence.capture.coverage, date)
140
+ && evidence.sourceCommit
141
+ && options.root
142
+ && hasGitRevision(options.root, evidence.sourceCommit)
143
+ && authoritativeCommit
144
+ && isDataHistoryAncestor(options.root, evidence.sourceCommit, authoritativeCommit)
145
+ );
146
+ return coversDate && Boolean(hasFixedMaterial);
147
+ });
148
+ }
149
+
150
+ function verifiedEvidenceComplete(evidence, personIds) {
151
+ const collectors = arrayValue(evidence.collectorIds);
152
+ const verifiers = arrayValue(evidence.verifierIds);
153
+ if (
154
+ typeof evidence.sourceDescription !== "string"
155
+ || !evidence.sourceDescription.trim()
156
+ || !validCalendarDate(evidence.collectedOn)
157
+ || !validCalendarDate(evidence.verifiedOn)
158
+ || evidence.verifiedOn < evidence.collectedOn
159
+ || !collectors.length
160
+ || !verifiers.length
161
+ || !collectors.every((id) => personIds.has(id))
162
+ || !verifiers.every((id) => personIds.has(id))
163
+ ) return false;
164
+ if (evidence.sourceKind !== "rendered-page") return true;
165
+ return evidence.capture
166
+ && typeof evidence.capture.route === "string"
167
+ && evidence.capture.route.trim()
168
+ && evidence.capture.filters
169
+ && typeof evidence.capture.filters === "object"
170
+ && !Array.isArray(evidence.capture.filters)
171
+ && isRfc3339Timestamp(evidence.capture.capturedAt)
172
+ && typeof evidence.capture.method === "string"
173
+ && evidence.capture.method.trim();
174
+ }
175
+
176
+ function validCalendarDate(value) {
177
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ""))) return false;
178
+ const parsed = new Date(`${value}T00:00:00Z`);
179
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
180
+ }
181
+
182
+ export function reportingRouteEventCommit(loaded, routeSet, eventName) {
183
+ const history = [...reportingRouteHistory(loaded, routeSet.id)].reverse();
184
+ for (const commit of history) {
185
+ const source = getFileAtRevision(loaded.root, commit.commit, commit.path);
186
+ try {
187
+ if (source && JSON.parse(source)[eventName]) return commit.commit;
188
+ } catch {
189
+ continue;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+
195
+ export function reportingRouteEventAuthorityIssueAtCommit(loaded, routeSet, eventName) {
196
+ const event = routeSet[eventName];
197
+ const commit = reportingRouteEventCommit(loaded, routeSet, eventName);
198
+ const options = eventName === "approval" ? {
199
+ appointmentId: event?.approvalAppointmentId,
200
+ actorId: event?.approvedById,
201
+ at: event?.approvedAt,
202
+ timezone: event?.timezone,
203
+ workspaceId: loaded.workspace.id
204
+ } : {
205
+ appointmentId: event?.authorityAppointmentId,
206
+ actorId: event?.canceledById,
207
+ at: event?.canceledAt,
208
+ timezone: routeSet.approval?.timezone,
209
+ workspaceId: loaded.workspace.id
210
+ };
211
+ const records = commit ? recordsAtRevision(loaded, commit) : loaded.resources;
212
+ const historicalRoute = records.find(({ id }) => id === routeSet.id) || routeSet;
213
+ const historicalIssue = reportingRouteEventAuthorityIssue(records, historicalRoute, options);
214
+ if (historicalIssue) return historicalIssue;
215
+ const eventAt = instant(options.at, "Reporting Route authority time");
216
+ if (reportingRouteOngoingAuthorities(
217
+ loaded.resources,
218
+ routeSet,
219
+ eventAt,
220
+ loaded.workspace.id
221
+ ).some(({ holderId }) => holderId === options.actorId)) {
222
+ return {
223
+ code: "reporting-route-authority-not-separated",
224
+ message: "The approving Person must be independent from every current or backfilled Appointment responsible for the reporting channels at the event time."
225
+ };
226
+ }
227
+ return null;
228
+ }
229
+
230
+ export function reportingRouteOngoingAuthorities(records, routeSet, at, workspaceId) {
231
+ const date = localDateTimeValue(at, routeSet.approval?.timezone || "UTC").slice(0, 10);
232
+ const byId = new Map(records.map((record) => [record.id, record]));
233
+ return records.filter((record) => (
234
+ record.type === "appointment"
235
+ && record.appointmentKind === routeSet.authorityAppointmentKind
236
+ && arrayValue(record.scopeResourceIds).some((id) => [routeSet.id, routeSet.programId, workspaceId].includes(id))
237
+ && appointmentWasAuthorizedOn(record, date, byId)
238
+ ));
239
+ }
240
+
241
+ export function effectiveReportingRouteRequirements(records, at = new Date(), programId, timezone = "UTC") {
242
+ const when = instant(at, "Requirement assessment time");
243
+ const program = programId ? records.find(({ type, id }) => type === "program" && id === programId) : null;
244
+ return records.flatMap((source) => {
245
+ try {
246
+ if (!reportingRouteSourceEffective(source, when, timezone)) return [];
247
+ } catch {
248
+ return [];
249
+ }
250
+ if (programId && !program) return [];
251
+ const requirements = Array.isArray(source.reportingRouteRequirements) ? source.reportingRouteRequirements : [];
252
+ return requirements.filter((requirement) => (
253
+ requirement && typeof requirement === "object" && !Array.isArray(requirement)
254
+ && typeof requirement.effectiveAt === "string"
255
+ && (!programId || reportingRouteRequirementAppliesToProgram(requirement, programId))
256
+ &&
257
+ new Date(requirement.effectiveAt) <= when
258
+ && (!requirement.endsAt || new Date(requirement.endsAt) > when)
259
+ )).map((requirement) => ({ ...requirement, sourceId: source.id, sourceType: source.type }));
260
+ });
261
+ }
262
+
263
+ export function reportingRouteSourceAppliesToProgram(source, program, records) {
264
+ if (!program || !source) return false;
265
+ return (Array.isArray(source.reportingRouteRequirements) ? source.reportingRouteRequirements : [])
266
+ .some((requirement) => reportingRouteRequirementAppliesToProgram(requirement, program.id));
267
+ }
268
+
269
+ export function reportingRouteRequirementAppliesToProgram(requirement, programId) {
270
+ if (!requirement || typeof requirement !== "object" || Array.isArray(requirement)) return false;
271
+ if (!programId) return true;
272
+ if (requirement.programScope === "all-programs") return true;
273
+ return requirement.programScope === "selected-programs"
274
+ && arrayValue(requirement.programIds).includes(programId);
275
+ }
276
+
277
+ export function reportingRouteSourceEffective(source, at, timezone = "UTC") {
278
+ const when = instant(at, "Source assessment time");
279
+ if (!reportingRouteSourceMayApply(source)) return false;
280
+ if (["policy", "document", "commitment"].includes(source.type) && source.effectiveOn) {
281
+ const startsAt = new Date(timestampFromLocalDateTime(`${source.effectiveOn}T00:00:00`, timezone));
282
+ if (startsAt > when) return false;
283
+ }
284
+ if (["superseded", "retired", "closed", "archived"].includes(source.status)) {
285
+ const changedOn = source.statusTransition?.changedOn;
286
+ if (!changedOn) return false;
287
+ const endsAt = new Date(timestampFromLocalDateTime(`${changedOn}T00:00:00`, timezone));
288
+ if (endsAt <= when) return false;
289
+ }
290
+ return true;
291
+ }
292
+
293
+ export function reportingRouteSourceMayApply(source) {
294
+ if (source?.type === "policy" || source?.type === "document") {
295
+ return ["active", "superseded", "retired"].includes(source.status);
296
+ }
297
+ if (source?.type === "commitment") return ["active", "superseded", "retired"].includes(source.status);
298
+ if (source?.type === "risk") return ["open", "monitoring", "closed", "archived"].includes(source.status);
299
+ return false;
300
+ }
301
+
302
+ export function reportingRouteSetInterval(routeSet) {
303
+ if (!["approved", "canceled"].includes(routeSet?.status) || !routeSet.approval?.effectiveAt) return null;
304
+ return {
305
+ start: new Date(routeSet.approval.effectiveAt),
306
+ end: routeSet.status === "canceled" && routeSet.cancellation?.canceledAt
307
+ ? new Date(routeSet.cancellation.canceledAt)
308
+ : null
309
+ };
310
+ }
311
+
312
+ export function governingReportingRouteSetsAt(records, at, options = {}) {
313
+ const when = instant(at, "Reporting Route assessment time");
314
+ return records.filter((record) => {
315
+ if (record.type !== "reporting-route-set" || record.purposeKey !== options.purposeKey) return false;
316
+ if (options.programId && record.programId !== options.programId) return false;
317
+ const interval = reportingRouteSetInterval(record);
318
+ return interval && interval.start <= when && (!interval.end || interval.end > when);
319
+ });
320
+ }
321
+
322
+ export function reportingRouteBindingExpectation(loaded, attestation) {
323
+ return reportingRouteBindingExpectationFromState(loaded, attestation, loaded.resources, getGitSummary(loaded.root).commit);
324
+ }
325
+
326
+ export function reportingRouteBindingExpectationForValidation(loaded, attestation) {
327
+ const index = getDataRecordHistoryIndex(loaded.root);
328
+ const history = [...(index.historiesById.get(attestation.id) || [])].reverse();
329
+ for (const summary of history) {
330
+ const source = getFileAtRevision(loaded.root, summary.commit, summary.path);
331
+ let historicalAttestation;
332
+ try { historicalAttestation = source ? JSON.parse(source) : null; } catch { continue; }
333
+ if (historicalAttestation?.type !== "attestation" || historicalAttestation.status !== "completed") continue;
334
+ return reportingRouteBindingExpectationFromState(
335
+ loaded,
336
+ historicalAttestation,
337
+ recordsAtRevision(loaded, summary.commit),
338
+ summary.commit
339
+ );
340
+ }
341
+ return reportingRouteBindingExpectation(loaded, attestation);
342
+ }
343
+
344
+ function reportingRouteBindingExpectationFromState(loaded, attestation, records, head) {
345
+ const date = attestation.assignedOn || attestation.completedOn;
346
+ if (!date) return { required: false, routeSet: null, commit: null };
347
+ const timezone = records.find(({ type }) => type === "workspace")?.timezone || loaded.workspace.timezone;
348
+ if (records.some((source) => (
349
+ source.reportingRouteRequirements !== undefined
350
+ && !Array.isArray(source.reportingRouteRequirements)
351
+ ))) {
352
+ return { required: true, routeSet: null, commit: null, error: "Historical Reporting Route requirements must be arrays before an Attestation binding can be verified." };
353
+ }
354
+ let dayStart;
355
+ let dayEnd;
356
+ try {
357
+ dayStart = new Date(timestampFromLocalDateTime(`${date}T00:00:00`, timezone));
358
+ dayEnd = new Date(timestampFromLocalDateTime(`${addCalendarDays(date, 1)}T00:00:00`, timezone));
359
+ } catch {
360
+ return { required: true, routeSet: null, commit: null, error: "The Attestation assignment date is invalid." };
361
+ }
362
+ const rawBoundaries = records.flatMap((source) => (Array.isArray(source.reportingRouteRequirements) ? source.reportingRouteRequirements : [])
363
+ .filter((requirement) => requirement?.purposeKey === "security-reporting")
364
+ .flatMap((requirement) => [requirement.effectiveAt, requirement.endsAt]))
365
+ .map((value) => new Date(value))
366
+ .filter((value) => !Number.isNaN(value.getTime()) && value > dayStart && value < dayEnd);
367
+ const samples = [dayStart, new Date(dayEnd.getTime() - 1), ...rawBoundaries];
368
+ let anyRequired = false;
369
+ try {
370
+ anyRequired = samples.some((at) => effectiveReportingRouteRequirements(records, at, undefined, timezone)
371
+ .some(({ purposeKey }) => purposeKey === "security-reporting"));
372
+ } catch {
373
+ return { required: true, routeSet: null, commit: null, error: "Reporting Route requirements contain an invalid effective interval." };
374
+ }
375
+ if (!anyRequired) return { required: false, routeSet: null, commit: null };
376
+ const program = records.find(({ type, id }) => type === "program" && id === attestation.programId);
377
+ if (!program) {
378
+ return {
379
+ required: true,
380
+ routeSet: null,
381
+ commit: null,
382
+ error: "A completed Attestation subject to security-reporting requirements must name its Program."
383
+ };
384
+ }
385
+ const programId = program.id;
386
+ let dayRequired = false;
387
+ try {
388
+ dayRequired = samples.some((at) => effectiveReportingRouteRequirements(records, at, programId, timezone)
389
+ .some(({ purposeKey }) => purposeKey === "security-reporting"));
390
+ } catch {
391
+ return { required: true, routeSet: null, commit: null, error: "Reporting Route requirements contain an invalid effective interval." };
392
+ }
393
+ if (!dayRequired) return { required: false, routeSet: null, commit: null };
394
+ let cutoff;
395
+ if (attestation.assignedAt) {
396
+ try {
397
+ cutoff = instant(attestation.assignedAt, "Attestation assignment time");
398
+ if (localDateTimeValue(cutoff, timezone).slice(0, 10) !== date) {
399
+ return { required: true, routeSet: null, commit: null, error: "Attestation assignedAt must fall on assignedOn in the Workspace timezone." };
400
+ }
401
+ } catch (error) {
402
+ return { required: true, routeSet: null, commit: null, error: error.message };
403
+ }
404
+ } else {
405
+ const scopedSources = records.filter((source) => (
406
+ reportingRouteSourceAppliesToProgram(source, program, records)
407
+ && (Array.isArray(source.reportingRouteRequirements) ? source.reportingRouteRequirements : [])
408
+ .some((requirement) => requirement?.purposeKey === "security-reporting")
409
+ ));
410
+ const boundaryInsideDay = [
411
+ ...scopedSources.flatMap((source) => (Array.isArray(source.reportingRouteRequirements) ? source.reportingRouteRequirements : [])
412
+ .flatMap((requirement) => [requirement?.effectiveAt, requirement?.endsAt])),
413
+ ...records.filter(({ type, programId: routeProgramId, purposeKey }) => (
414
+ type === "reporting-route-set" && routeProgramId === programId && purposeKey === "security-reporting"
415
+ )).flatMap((route) => [route.approval?.effectiveAt, route.cancellation?.canceledAt])
416
+ ].map((value) => new Date(value)).some((value) => (
417
+ !Number.isNaN(value.getTime()) && value > dayStart && value < dayEnd
418
+ ));
419
+ if (boundaryInsideDay) {
420
+ return {
421
+ required: true,
422
+ routeSet: null,
423
+ commit: null,
424
+ error: `Attestation assignedAt is required because reporting requirements or channels changed during ${date}.`
425
+ };
426
+ }
427
+ cutoff = new Date(dayEnd.getTime() - 1);
428
+ }
429
+ const required = effectiveReportingRouteRequirements(records, cutoff, programId, timezone)
430
+ .some(({ purposeKey }) => purposeKey === "security-reporting");
431
+ if (!required) return { required: false, routeSet: null, commit: null };
432
+ const routeSets = governingReportingRouteSetsAt(records, cutoff, {
433
+ purposeKey: "security-reporting",
434
+ programId
435
+ });
436
+ if (routeSets.length !== 1) {
437
+ return {
438
+ required: true,
439
+ routeSet: null,
440
+ commit: null,
441
+ error: routeSets.length
442
+ ? `More than one Reporting Channel Set governed security reporting on ${date}.`
443
+ : `No Reporting Channel Set governed security reporting on ${date}.`
444
+ };
445
+ }
446
+ const routeSet = routeSets[0];
447
+ const entry = loaded.entries.find(({ record }) => record.id === routeSet.id);
448
+ const commit = entry && head
449
+ ? approvedRouteSetCommit(loaded, entry, head)
450
+ : null;
451
+ return {
452
+ required: true,
453
+ routeSet,
454
+ commit,
455
+ ...(!commit ? { error: `The approved revision for Reporting Channel Set "${routeSet.id}" is not available in authoritative Git history.` } : {})
456
+ };
457
+ }
458
+
459
+ export function bindAttestationReportingRouteSet(loaded, attestation) {
460
+ const bound = { ...attestation };
461
+ delete bound.reportingRouteSetId;
462
+ delete bound.reportingRouteSetCommit;
463
+ const expectation = reportingRouteBindingExpectation(loaded, attestation);
464
+ if (!expectation.required) return bound;
465
+ const repository = getGitSummary(loaded.root);
466
+ if (!repository.available || !repository.clean || !repository.commit) {
467
+ throw new Error("Commit all Reporting Channel Set facts before completing an Attestation that must bind delivery proof.");
468
+ }
469
+ if (expectation.error) throw new Error(expectation.error);
470
+ return {
471
+ ...bound,
472
+ reportingRouteSetId: expectation.routeSet.id,
473
+ reportingRouteSetCommit: expectation.commit
474
+ };
475
+ }
476
+
477
+ export function assertionTimingAt(eventAt, recordedAt) {
478
+ const elapsed = recordedAt.getTime() - eventAt.getTime();
479
+ if (elapsed < 0) return "git-recorded-before-event";
480
+ return elapsed >= 0 && elapsed <= CONTEMPORANEOUS_COMMIT_WINDOW_MS
481
+ ? "git-recorded-within-day"
482
+ : "git-recorded-later";
483
+ }
484
+
485
+ function instant(value, label) {
486
+ const result = value instanceof Date ? value : new Date(value);
487
+ if (Number.isNaN(result.getTime())) throw new Error(`${label} must be an RFC 3339 timestamp.`);
488
+ return result;
489
+ }
490
+
491
+ function timezoneName(value) {
492
+ try { new Intl.DateTimeFormat("en", { timeZone: value }).format(); } catch { throw new Error("An IANA timezone is required."); }
493
+ return value;
494
+ }
495
+
496
+ function arrayValue(value) {
497
+ return Array.isArray(value) ? value : [];
498
+ }
499
+
500
+ function approvedRouteSetCommit(loaded, entry, head) {
501
+ const history = reportingRouteHistory(loaded, entry.record.id);
502
+ for (const summary of history) {
503
+ if (!isDataHistoryAncestor(loaded, summary.commit, head)) continue;
504
+ const source = getFileAtRevision(loaded.root, summary.commit, summary.path);
505
+ try {
506
+ const route = source ? JSON.parse(source) : null;
507
+ if (route?.status === "approved") return summary.commit;
508
+ } catch {
509
+ continue;
510
+ }
511
+ }
512
+ return null;
513
+ }
514
+
515
+ export function reportingRouteRecordAtRevision(loaded, entry, commit) {
516
+ const identity = reportingRouteHistory(loaded, entry.record.id)
517
+ .find(({ commit: changedAt }) => isDataHistoryAncestor(loaded, changedAt, commit));
518
+ const path = identity?.path;
519
+ const source = path ? getFileAtRevision(loaded.root, commit, path) : null;
520
+ try { return source ? JSON.parse(source) : null; } catch { return null; }
521
+ }
522
+
523
+ export function reportingRouteHistory(loaded, routeSetId) {
524
+ return getDataRecordHistoryIndex(loaded.root).historiesById.get(routeSetId) || [];
525
+ }
526
+
527
+ export function recordsAtRevision(loaded, commit) {
528
+ const index = getDataRecordHistoryIndex(loaded.root);
529
+ const records = [];
530
+ for (const [id, history] of index.historiesById) {
531
+ const identity = history.find(({ commit: changedAt }) => isDataHistoryAncestor(loaded, changedAt, commit));
532
+ if (!identity) continue;
533
+ const source = getFileAtRevision(loaded.root, commit, identity.path);
534
+ try {
535
+ const record = source ? JSON.parse(source) : null;
536
+ if (record?.id === id) records.push(record);
537
+ } catch {
538
+ continue;
539
+ }
540
+ }
541
+ return records;
542
+ }