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.
- package/model/index.js +9 -5
- package/model/v10.json +12122 -0
- package/model/v9.json +11647 -0
- package/package.json +1 -1
- package/src/agent.js +3 -0
- package/src/audit-populations.js +88 -0
- package/src/audit-preparation.js +7 -2
- package/src/cli.js +186 -13
- package/src/collection-review-integrity.js +118 -0
- package/src/collection-review.js +71 -7
- package/src/collection-scope.js +8 -0
- package/src/evidence-packet.js +328 -46
- package/src/files.js +364 -6
- package/src/git.js +1064 -149
- package/src/index.js +17 -1
- package/src/model-migration.js +221 -5
- package/src/obligations.js +834 -66
- package/src/policy-library/information-security-policy-v2.md +1 -1
- package/src/policy-library.js +85 -8
- package/src/program-path.js +11 -5
- package/src/program-readiness.js +84 -6
- package/src/reconciliation.js +332 -81
- package/src/reporting-route-integrity.js +542 -0
- package/src/reporting-route-sets.js +745 -0
- package/src/server.js +160 -47
- package/src/state.js +30 -8
- package/src/time.js +55 -0
- package/src/validate.js +978 -4
- package/src/web.js +491 -49
- package/src/workflow-history-integrity.js +872 -0
- package/src/workflow.js +36 -10
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import { getRepositorySnapshot, isDataHistoryAncestor } from "./git.js";
|
|
2
|
+
import { applyResourceBatch, INTERNAL_WORKFLOW_CAPABILITIES, updateResource } from "./files.js";
|
|
3
|
+
import {
|
|
4
|
+
effectiveReportingRouteRequirements as deriveEffectiveReportingRouteRequirements,
|
|
5
|
+
reportingRouteAssertionTiming,
|
|
6
|
+
reportingRouteEventCommit,
|
|
7
|
+
reportingRouteEventAuthorityIssue,
|
|
8
|
+
reportingRouteEventAuthorityIssueAtCommit,
|
|
9
|
+
reportingRouteFixedEvidence,
|
|
10
|
+
reportingRouteOngoingAuthorities,
|
|
11
|
+
reportingRouteRecordAtRevision,
|
|
12
|
+
reportingRouteSourceEffective,
|
|
13
|
+
reportingRouteSourceMayApply,
|
|
14
|
+
reportingRouteSourceAppliesToProgram
|
|
15
|
+
} from "./reporting-route-integrity.js";
|
|
16
|
+
import { localDateTimeValue } from "./time.js";
|
|
17
|
+
import { timestampFromLocalDateTime } from "./time.js";
|
|
18
|
+
import { addCalendarDays } from "./recurrence.js";
|
|
19
|
+
import { loadWorkspace } from "./workspace.js";
|
|
20
|
+
|
|
21
|
+
const COMMIT_REQUIRED_STATUSES = new Set(["proposed", "approved", "canceled"]);
|
|
22
|
+
const SOURCE_TYPES = new Set(["policy", "document", "commitment", "risk"]);
|
|
23
|
+
const MAX_PERIOD_BOUNDARIES = 512;
|
|
24
|
+
const PERIOD_REPOSITORY_SNAPSHOT = Symbol("filegrc.reportingRoutePeriodRepositorySnapshot");
|
|
25
|
+
|
|
26
|
+
export async function assessReportingRouteSets(input = process.cwd(), options = {}) {
|
|
27
|
+
const loaded = typeof input === "object" && input?.resources ? input : await loadWorkspace(input);
|
|
28
|
+
if (!loaded.model.resources?.["reporting-route-set"]) {
|
|
29
|
+
return { supported: false, requirements: [], routeSets: [], issues: [], counts: { complete: 0, action: 0, later: 0, inactive: 0 } };
|
|
30
|
+
}
|
|
31
|
+
const at = instant(options.at || new Date().toISOString(), "Assessment time");
|
|
32
|
+
const programId = options.programId || loaded.resources.find(({ type }) => type === "program")?.id;
|
|
33
|
+
const timezone = options.timezone || loaded.workspace?.timezone || "UTC";
|
|
34
|
+
if (programId && !loaded.resources.some(({ type, id }) => type === "program" && id === programId)) {
|
|
35
|
+
return {
|
|
36
|
+
supported: true,
|
|
37
|
+
at: at.toISOString(),
|
|
38
|
+
programId,
|
|
39
|
+
requirements: [],
|
|
40
|
+
proposedRequirements: [],
|
|
41
|
+
routeSets: [],
|
|
42
|
+
issues: [issue("invalid-program", programId, `Program "${programId}" was not found.`)],
|
|
43
|
+
counts: { complete: 0, action: 0, later: 0, inactive: 0 }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const requirements = deriveEffectiveReportingRouteRequirements(loaded.resources, at, programId, timezone);
|
|
47
|
+
const proposedRequirements = loaded.resources.flatMap((source) => (
|
|
48
|
+
SOURCE_TYPES.has(source.type)
|
|
49
|
+
&& (!programId || reportingRouteSourceAppliesToProgram(
|
|
50
|
+
source,
|
|
51
|
+
loaded.resources.find(({ type, id }) => type === "program" && id === programId),
|
|
52
|
+
loaded.resources
|
|
53
|
+
))
|
|
54
|
+
? (Array.isArray(source.reportingRouteRequirements) ? source.reportingRouteRequirements : [])
|
|
55
|
+
.filter((requirement) => requirement && typeof requirement === "object" && !Array.isArray(requirement))
|
|
56
|
+
.map((requirement) => ({
|
|
57
|
+
...requirement,
|
|
58
|
+
sourceId: source.id,
|
|
59
|
+
sourceType: source.type,
|
|
60
|
+
sourceStatus: source.status
|
|
61
|
+
}))
|
|
62
|
+
: []
|
|
63
|
+
));
|
|
64
|
+
const routeSets = loaded.resources.filter((record) => (
|
|
65
|
+
record.type === "reporting-route-set" && (!programId || record.programId === programId)
|
|
66
|
+
));
|
|
67
|
+
const repository = options[PERIOD_REPOSITORY_SNAPSHOT]
|
|
68
|
+
|| await getRepositorySnapshot(loaded.root, { fresh: true });
|
|
69
|
+
const assessments = routeSets.map((record) => assessRouteSet(record, loaded, at, repository));
|
|
70
|
+
const issues = loaded.resources.flatMap((source) => {
|
|
71
|
+
if (!SOURCE_TYPES.has(source.type) || !Object.hasOwn(source, "reportingRouteRequirements")) return [];
|
|
72
|
+
if (!reportingRouteSourceMayApply(source)) return [];
|
|
73
|
+
try {
|
|
74
|
+
if (!reportingRouteSourceEffective(source, at, timezone)) return [];
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return [issue("invalid-reporting-route-requirement", source.id, error.message)];
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(source.reportingRouteRequirements)) {
|
|
79
|
+
return [issue("invalid-reporting-route-requirement", source.id, "reportingRouteRequirements must be an array.")];
|
|
80
|
+
}
|
|
81
|
+
return source.reportingRouteRequirements.flatMap((requirement) => {
|
|
82
|
+
if (!requirement || typeof requirement !== "object" || Array.isArray(requirement)) {
|
|
83
|
+
return [issue("invalid-reporting-route-requirement", source.id, "Each Reporting Route requirement must be an object.")];
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
programId
|
|
87
|
+
&& requirement.programScope === "selected-programs"
|
|
88
|
+
&& Array.isArray(requirement.programIds)
|
|
89
|
+
&& !requirement.programIds.includes(programId)
|
|
90
|
+
) return [];
|
|
91
|
+
return validateRequirementForAssessment(requirement, source.id, loaded.resources);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
for (const requirement of requirements) {
|
|
95
|
+
if (!Array.isArray(requirement.requiredLanes)) {
|
|
96
|
+
issues.push(issue("invalid-reporting-route-requirement", requirement.sourceId, `Reporting Route requirements for ${requirement.purposeKey || "this purpose"} need a requiredLanes array.`));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const candidates = assessments.filter(({ record, effective, canceled }) => (
|
|
100
|
+
record.purposeKey === requirement.purposeKey && effective && !canceled
|
|
101
|
+
));
|
|
102
|
+
if (!candidates.length) {
|
|
103
|
+
issues.push(issue("uncovered-reporting-route-interval", requirement.sourceId, `No committed approved Reporting Channel Set covers ${requirement.purposeKey} at ${at.toISOString()}.`));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (candidates.length > 1) {
|
|
107
|
+
issues.push(issue(
|
|
108
|
+
"overlapping-reporting-route-sets",
|
|
109
|
+
candidates[1].record.id,
|
|
110
|
+
`More than one committed approved Reporting Channel Set covers ${requirement.purposeKey} at ${at.toISOString()}.`
|
|
111
|
+
));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const route = candidates[0].record;
|
|
115
|
+
if (requirement.requiredLanes.includes("alternate") && !route.alternateLane) {
|
|
116
|
+
issues.push(issue("missing-alternate-reporting-route", route.id, `${requirement.sourceId} requires a fallback reporting channel for ${requirement.purposeKey}.`));
|
|
117
|
+
}
|
|
118
|
+
if (requirement.distinctChannels && route.alternateLane?.channelKind === route.primaryLane?.channelKind) {
|
|
119
|
+
issues.push(issue("reporting-route-channel-not-distinct", route.id, `${requirement.sourceId} requires different normal and fallback channel types.`));
|
|
120
|
+
}
|
|
121
|
+
if (requirement.independentDependencies && !reportingRouteLanesIndependent(route, loaded.resources, at)) {
|
|
122
|
+
issues.push(issue("reporting-route-dependencies-not-independent", route.id, `${requirement.sourceId} requires independent channel dependencies or an applicable Exception.`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const assessment of assessments) issues.push(...assessment.issues);
|
|
126
|
+
return {
|
|
127
|
+
supported: true,
|
|
128
|
+
at: at.toISOString(),
|
|
129
|
+
programId,
|
|
130
|
+
requirements,
|
|
131
|
+
proposedRequirements,
|
|
132
|
+
routeSets: assessments,
|
|
133
|
+
issues,
|
|
134
|
+
counts: {
|
|
135
|
+
complete: assessments.filter(({ state }) => state === "complete").length,
|
|
136
|
+
action: assessments.filter(({ state }) => state === "action").length,
|
|
137
|
+
later: assessments.filter(({ state }) => state === "later").length,
|
|
138
|
+
inactive: assessments.filter(({ state }) => state === "inactive").length
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateRequirementForAssessment(requirement, sourceId, records) {
|
|
144
|
+
const invalid = (message) => [issue("invalid-reporting-route-requirement", sourceId, message)];
|
|
145
|
+
if (!String(requirement.purposeKey || "").trim()) {
|
|
146
|
+
return invalid("Each Reporting Route requirement needs a purpose key.");
|
|
147
|
+
}
|
|
148
|
+
if (!["all-programs", "selected-programs"].includes(requirement.programScope)) {
|
|
149
|
+
return invalid("Each Reporting Route requirement must cover all Programs or name selected Programs.");
|
|
150
|
+
}
|
|
151
|
+
if (requirement.programScope === "all-programs" && Object.hasOwn(requirement, "programIds")) {
|
|
152
|
+
return invalid("An all-Programs Reporting Route requirement cannot also list selected Programs.");
|
|
153
|
+
}
|
|
154
|
+
if (requirement.programScope === "selected-programs") {
|
|
155
|
+
if (!Array.isArray(requirement.programIds) || !requirement.programIds.length) {
|
|
156
|
+
return invalid("A selected-Programs Reporting Route requirement must name at least one Program.");
|
|
157
|
+
}
|
|
158
|
+
const programIds = new Set(records.filter(({ type }) => type === "program").map(({ id }) => id));
|
|
159
|
+
if (requirement.programIds.some((id) => typeof id !== "string" || !programIds.has(id))) {
|
|
160
|
+
return invalid("A selected-Programs Reporting Route requirement must name existing Program IDs.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
!Array.isArray(requirement.requiredLanes)
|
|
165
|
+
|| !requirement.requiredLanes.length
|
|
166
|
+
|| requirement.requiredLanes.some((lane) => !["primary", "alternate"].includes(lane))
|
|
167
|
+
) {
|
|
168
|
+
return invalid("Each Reporting Route requirement must name one or more valid required lanes.");
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
(Object.hasOwn(requirement, "distinctChannels") && typeof requirement.distinctChannels !== "boolean")
|
|
172
|
+
|| (Object.hasOwn(requirement, "independentDependencies") && typeof requirement.independentDependencies !== "boolean")
|
|
173
|
+
) {
|
|
174
|
+
return invalid("Reporting Route channel and dependency requirements must be true or false.");
|
|
175
|
+
}
|
|
176
|
+
let effectiveAt;
|
|
177
|
+
let endsAt;
|
|
178
|
+
let requirementTimezone;
|
|
179
|
+
try {
|
|
180
|
+
effectiveAt = instant(requirement.effectiveAt, "Requirement start");
|
|
181
|
+
endsAt = requirement.endsAt ? instant(requirement.endsAt, "Requirement end") : null;
|
|
182
|
+
requirementTimezone = timezoneName(requirement.timezone);
|
|
183
|
+
assertTimestampZone(requirement.effectiveAt, requirementTimezone, "Requirement start");
|
|
184
|
+
if (requirement.endsAt) assertTimestampZone(requirement.endsAt, requirementTimezone, "Requirement end");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return invalid(error.message);
|
|
187
|
+
}
|
|
188
|
+
if (endsAt && endsAt <= effectiveAt) {
|
|
189
|
+
return invalid("A Reporting Route requirement must end after it starts.");
|
|
190
|
+
}
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function assessReportingRoutePeriod(input = process.cwd(), options = {}) {
|
|
195
|
+
const loaded = typeof input === "object" && input?.resources ? input : await loadWorkspace(input);
|
|
196
|
+
if (!loaded.model.resources?.["reporting-route-set"]) return { supported: false, snapshots: [], issues: [] };
|
|
197
|
+
const timezone = options.timezone || loaded.workspace?.timezone || "UTC";
|
|
198
|
+
if (options.programId && !loaded.resources.some(({ type, id }) => type === "program" && id === options.programId)) {
|
|
199
|
+
return {
|
|
200
|
+
supported: true,
|
|
201
|
+
start: options.start,
|
|
202
|
+
end: options.end,
|
|
203
|
+
timezone,
|
|
204
|
+
snapshots: [],
|
|
205
|
+
issues: [issue("invalid-program", options.programId, `Program "${options.programId}" was not found.`)]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
let startsAt;
|
|
209
|
+
let endsAt;
|
|
210
|
+
try {
|
|
211
|
+
startsAt = new Date(timestampFromLocalDateTime(`${options.start}T00:00:00`, timezone));
|
|
212
|
+
endsAt = new Date(timestampFromLocalDateTime(`${addCalendarDays(options.end, 1)}T00:00:00`, timezone));
|
|
213
|
+
} catch {
|
|
214
|
+
return {
|
|
215
|
+
supported: true,
|
|
216
|
+
start: options.start,
|
|
217
|
+
end: options.end,
|
|
218
|
+
timezone,
|
|
219
|
+
snapshots: [],
|
|
220
|
+
issues: [issue("invalid-reporting-route-period", loaded.workspace.id, "Reporting Route period dates and timezone must be valid.")]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const boundaries = new Set([startsAt.toISOString()]);
|
|
224
|
+
const boundaryIssues = [];
|
|
225
|
+
const invalidBoundary = (resourceId, label) => {
|
|
226
|
+
boundaryIssues.push(issue("invalid-reporting-route-boundary", resourceId, `${label} must use a valid date, timestamp, and IANA timezone.`));
|
|
227
|
+
};
|
|
228
|
+
const addDateBoundary = (date, afterInclusiveEnd = false, boundaryTimezone = timezone, resourceId = loaded.workspace.id, label = "Reporting Route boundary") => {
|
|
229
|
+
if (!date) return;
|
|
230
|
+
try {
|
|
231
|
+
const boundaryDate = afterInclusiveEnd ? addCalendarDays(date, 1) : date;
|
|
232
|
+
const boundary = new Date(timestampFromLocalDateTime(`${boundaryDate}T00:00:00`, boundaryTimezone));
|
|
233
|
+
if (Number.isNaN(boundary.getTime())) throw new Error("Invalid boundary");
|
|
234
|
+
if (boundary > startsAt && boundary < endsAt) boundaries.add(boundary.toISOString());
|
|
235
|
+
} catch {
|
|
236
|
+
invalidBoundary(resourceId, label);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const addInstantBoundary = (value, resourceId, label) => {
|
|
240
|
+
if (!value) return;
|
|
241
|
+
const boundary = new Date(value);
|
|
242
|
+
if (Number.isNaN(boundary.getTime())) {
|
|
243
|
+
invalidBoundary(resourceId, label);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (boundary > startsAt && boundary < endsAt) boundaries.add(boundary.toISOString());
|
|
247
|
+
};
|
|
248
|
+
for (const record of loaded.resources) {
|
|
249
|
+
if (["policy", "document", "commitment"].includes(record.type) && record.effectiveOn) {
|
|
250
|
+
addDateBoundary(record.effectiveOn, false, timezone, record.id, `${record.title || record.id} effective date`);
|
|
251
|
+
}
|
|
252
|
+
for (const requirement of Array.isArray(record.reportingRouteRequirements) ? record.reportingRouteRequirements : []) {
|
|
253
|
+
if (!requirement || typeof requirement !== "object" || Array.isArray(requirement)) continue;
|
|
254
|
+
for (const value of [requirement.effectiveAt, requirement.endsAt]) {
|
|
255
|
+
addInstantBoundary(value, record.id, `${record.title || record.id} requirement boundary`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (["policy", "document", "commitment", "risk"].includes(record.type)) {
|
|
259
|
+
addDateBoundary(record.statusTransition?.changedOn, false, timezone, record.id, `${record.title || record.id} lifecycle date`);
|
|
260
|
+
}
|
|
261
|
+
if (record.type !== "reporting-route-set") continue;
|
|
262
|
+
for (const value of [record.approval?.effectiveAt, record.cancellation?.canceledAt]) {
|
|
263
|
+
addInstantBoundary(value, record.id, `${record.title || record.id} lifecycle timestamp`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
267
|
+
const appointmentsByKind = new Map();
|
|
268
|
+
for (const appointment of loaded.resources.filter(({ type }) => type === "appointment")) {
|
|
269
|
+
const appointments = appointmentsByKind.get(appointment.appointmentKind) || [];
|
|
270
|
+
appointments.push(appointment);
|
|
271
|
+
appointmentsByKind.set(appointment.appointmentKind, appointments);
|
|
272
|
+
}
|
|
273
|
+
const authorityContexts = new Map();
|
|
274
|
+
for (const routeSet of loaded.resources.filter(({ type }) => type === "reporting-route-set")) {
|
|
275
|
+
const authorityTimezone = routeSet.approval?.timezone || timezone;
|
|
276
|
+
const key = `${routeSet.authorityAppointmentKind}\0${authorityTimezone}`;
|
|
277
|
+
const context = authorityContexts.get(key) || {
|
|
278
|
+
appointmentKind: routeSet.authorityAppointmentKind,
|
|
279
|
+
timezone: authorityTimezone,
|
|
280
|
+
scopeIds: new Set([loaded.workspace.id])
|
|
281
|
+
};
|
|
282
|
+
context.scopeIds.add(routeSet.id);
|
|
283
|
+
context.scopeIds.add(routeSet.programId);
|
|
284
|
+
authorityContexts.set(key, context);
|
|
285
|
+
}
|
|
286
|
+
for (const context of authorityContexts.values()) {
|
|
287
|
+
for (const appointment of appointmentsByKind.get(context.appointmentKind) || []) {
|
|
288
|
+
if (!appointment.scopeResourceIds?.some((id) => context.scopeIds.has(id))) continue;
|
|
289
|
+
addDateBoundary(appointment.startsOn, false, context.timezone, appointment.id, `${appointment.title || appointment.id} start date`);
|
|
290
|
+
addDateBoundary(appointment.endsOn, true, context.timezone, appointment.id, `${appointment.title || appointment.id} end date`);
|
|
291
|
+
const holder = byId.get(appointment.holderId);
|
|
292
|
+
addDateBoundary(holder?.startDate, false, context.timezone, holder?.id || appointment.id, `${holder?.title || appointment.holderId} start date`);
|
|
293
|
+
addDateBoundary(holder?.endDate || holder?.statusTransition?.changedOn, true, context.timezone, holder?.id || appointment.id, `${holder?.title || appointment.holderId} end date`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (boundaries.size > MAX_PERIOD_BOUNDARIES) {
|
|
297
|
+
return {
|
|
298
|
+
supported: true,
|
|
299
|
+
start: options.start,
|
|
300
|
+
end: options.end,
|
|
301
|
+
timezone,
|
|
302
|
+
snapshots: [],
|
|
303
|
+
issues: [...boundaryIssues, issue(
|
|
304
|
+
"reporting-route-period-too-complex",
|
|
305
|
+
loaded.workspace.id,
|
|
306
|
+
`Reporting Route period assessment is limited to ${MAX_PERIOD_BOUNDARIES} distinct change boundaries. Split the audit period or consolidate duplicate lifecycle facts.`
|
|
307
|
+
)]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const repository = await getRepositorySnapshot(loaded.root, { fresh: true });
|
|
311
|
+
const snapshots = [];
|
|
312
|
+
for (const at of [...boundaries].sort()) {
|
|
313
|
+
snapshots.push(await assessReportingRouteSets(loaded, {
|
|
314
|
+
...options,
|
|
315
|
+
at,
|
|
316
|
+
[PERIOD_REPOSITORY_SNAPSHOT]: repository
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
const issues = [...new Map([...boundaryIssues, ...snapshots.flatMap((snapshot) => snapshot.issues)].map((item) => [
|
|
320
|
+
`${item.code}\0${item.resourceId}\0${item.message}`,
|
|
321
|
+
item
|
|
322
|
+
])).values()];
|
|
323
|
+
return { supported: true, start: options.start, end: options.end, timezone, snapshots, issues };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function effectiveReportingRouteRequirements(records, at = new Date(), programId, timezone = "UTC") {
|
|
327
|
+
return deriveEffectiveReportingRouteRequirements(records, at, programId, timezone);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function proposeReportingRouteSet(input, options = {}) {
|
|
331
|
+
const loaded = await loadWorkspace(input);
|
|
332
|
+
const entry = routeEntry(loaded, options.routeSetId);
|
|
333
|
+
if (entry.record.status !== "draft") throw new Error(`Reporting Channel Set "${entry.record.id}" must be draft before it is proposed.`);
|
|
334
|
+
const result = await updateResource(loaded.root, entry.record.type, entry.record.id, {
|
|
335
|
+
...entry.record,
|
|
336
|
+
status: "proposed"
|
|
337
|
+
}, {
|
|
338
|
+
expectedRevision: options.expectedRevision || entry.revision,
|
|
339
|
+
workflowCapability: INTERNAL_WORKFLOW_CAPABILITIES.reportingRouteSetProposal
|
|
340
|
+
});
|
|
341
|
+
return { ...result, commitRequired: true, nextAction: "Commit the proposal, then approve that exact commit." };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function approveReportingRouteSet(input, options = {}) {
|
|
345
|
+
const loaded = await loadWorkspace(input);
|
|
346
|
+
const entry = routeEntry(loaded, options.routeSetId);
|
|
347
|
+
if (entry.record.status !== "proposed") throw new Error(`Reporting Channel Set "${entry.record.id}" must be proposed before approval.`);
|
|
348
|
+
const proposalCommit = fullCommit(options.proposalCommit, "Proposal commit");
|
|
349
|
+
assertProposalMatches(loaded, entry, proposalCommit);
|
|
350
|
+
const approvedAt = pastOrPresentTimestamp(options.approvedAt, "Approval time");
|
|
351
|
+
const effectiveAt = instant(options.effectiveAt, "Effective time");
|
|
352
|
+
const timezone = timezoneName(options.timezone);
|
|
353
|
+
assertTimestampZone(options.approvedAt, timezone, "Approval time");
|
|
354
|
+
assertTimestampZone(options.effectiveAt, timezone, "Effective time");
|
|
355
|
+
if (effectiveAt < approvedAt) throw new Error("A Reporting Channel Set cannot become effective before it is approved.");
|
|
356
|
+
const authority = approvalAuthority(loaded, entry.record, options, approvedAt, timezone);
|
|
357
|
+
const approvalEvidenceIds = [...new Set(options.evidenceIds || [])];
|
|
358
|
+
if (!reportingRouteFixedEvidence(
|
|
359
|
+
loaded.resources,
|
|
360
|
+
entry.record.id,
|
|
361
|
+
approvalEvidenceIds,
|
|
362
|
+
approvedAt,
|
|
363
|
+
timezone,
|
|
364
|
+
{ root: loaded.root }
|
|
365
|
+
).length) {
|
|
366
|
+
throw new Error("Reporting Route approval requires linked verified, fixed Evidence covering the approval event.");
|
|
367
|
+
}
|
|
368
|
+
const next = {
|
|
369
|
+
...entry.record,
|
|
370
|
+
status: "approved",
|
|
371
|
+
proposalCommit,
|
|
372
|
+
approval: {
|
|
373
|
+
proposalCommit,
|
|
374
|
+
approvedById: authority.holderId,
|
|
375
|
+
approvalAppointmentId: authority.id,
|
|
376
|
+
approvedAt: String(options.approvedAt),
|
|
377
|
+
effectiveAt: String(options.effectiveAt),
|
|
378
|
+
timezone,
|
|
379
|
+
evidenceIds: approvalEvidenceIds
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const predecessorEntry = entry.record.predecessorId
|
|
383
|
+
? routeEntry(loaded, entry.record.predecessorId)
|
|
384
|
+
: null;
|
|
385
|
+
if (predecessorEntry?.record.status === "approved") {
|
|
386
|
+
if (effectiveAt > new Date()) {
|
|
387
|
+
throw new Error("A successor that replaces an approved Route Set must become effective now or in the past so both lifecycle changes can be recorded atomically.");
|
|
388
|
+
}
|
|
389
|
+
const predecessorAppointmentId = options.predecessorCancellationAppointmentId || options.approvalAppointmentId;
|
|
390
|
+
const predecessorAuthority = approvalAuthority(loaded, predecessorEntry.record, {
|
|
391
|
+
approvalAppointmentId: predecessorAppointmentId,
|
|
392
|
+
approvedById: options.predecessorCanceledById || options.approvedById
|
|
393
|
+
}, effectiveAt, timezone);
|
|
394
|
+
const predecessorEvidenceIds = [...new Set(options.predecessorCancellationEvidenceIds || [])];
|
|
395
|
+
if (!reportingRouteFixedEvidence(
|
|
396
|
+
loaded.resources,
|
|
397
|
+
predecessorEntry.record.id,
|
|
398
|
+
predecessorEvidenceIds,
|
|
399
|
+
effectiveAt,
|
|
400
|
+
timezone,
|
|
401
|
+
{ root: loaded.root }
|
|
402
|
+
).length) {
|
|
403
|
+
throw new Error("Replacing an approved Reporting Route Set requires linked verified, fixed Evidence covering the predecessor cancellation event.");
|
|
404
|
+
}
|
|
405
|
+
const predecessor = {
|
|
406
|
+
...predecessorEntry.record,
|
|
407
|
+
status: "canceled",
|
|
408
|
+
cancellation: {
|
|
409
|
+
canceledAt: String(options.effectiveAt),
|
|
410
|
+
canceledById: predecessorAuthority.holderId,
|
|
411
|
+
authorityAppointmentId: predecessorAuthority.id,
|
|
412
|
+
reason: `Superseded by ${entry.record.id}.`,
|
|
413
|
+
evidenceIds: predecessorEvidenceIds
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const batch = await applyResourceBatch(loaded.root, {
|
|
417
|
+
update: [next, predecessor],
|
|
418
|
+
expectedRevisions: {
|
|
419
|
+
[entry.record.id]: options.expectedRevision || entry.revision,
|
|
420
|
+
[predecessor.id]: options.predecessorExpectedRevision || predecessorEntry.revision
|
|
421
|
+
},
|
|
422
|
+
validateWholeWorkspace: true,
|
|
423
|
+
workflowCapability: INTERNAL_WORKFLOW_CAPABILITIES.reportingRouteSetApproval
|
|
424
|
+
});
|
|
425
|
+
return {
|
|
426
|
+
...batch,
|
|
427
|
+
record: next,
|
|
428
|
+
replacedRouteSet: predecessor,
|
|
429
|
+
commitRequired: true,
|
|
430
|
+
nextAction: "Commit the approval and predecessor cancellation together before the channel cutover can be relied on."
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const result = await updateResource(loaded.root, entry.record.type, entry.record.id, next, {
|
|
434
|
+
expectedRevision: options.expectedRevision || entry.revision,
|
|
435
|
+
workflowCapability: INTERNAL_WORKFLOW_CAPABILITIES.reportingRouteSetApproval
|
|
436
|
+
});
|
|
437
|
+
return { ...result, commitRequired: true, nextAction: "Commit this approval before the Reporting Channel Set can become effective." };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function cancelReportingRouteSet(input, options = {}) {
|
|
441
|
+
const loaded = await loadWorkspace(input);
|
|
442
|
+
const entry = routeEntry(loaded, options.routeSetId);
|
|
443
|
+
if (entry.record.status !== "approved") throw new Error(`Reporting Channel Set "${entry.record.id}" must be approved before cancellation.`);
|
|
444
|
+
const canceledAt = pastOrPresentTimestamp(options.canceledAt, "Cancellation time");
|
|
445
|
+
const timezone = timezoneName(options.timezone || entry.record.approval?.timezone);
|
|
446
|
+
assertTimestampZone(options.canceledAt, timezone, "Cancellation time");
|
|
447
|
+
if (canceledAt < new Date(entry.record.approval.effectiveAt)) {
|
|
448
|
+
throw new Error("A Reporting Channel Set cannot be canceled before it becomes effective.");
|
|
449
|
+
}
|
|
450
|
+
const authority = approvalAuthority(loaded, entry.record, options, canceledAt, timezone);
|
|
451
|
+
const evidenceIds = [...new Set(options.evidenceIds || [])];
|
|
452
|
+
if (!reportingRouteFixedEvidence(
|
|
453
|
+
loaded.resources,
|
|
454
|
+
entry.record.id,
|
|
455
|
+
evidenceIds,
|
|
456
|
+
canceledAt,
|
|
457
|
+
timezone,
|
|
458
|
+
{ root: loaded.root }
|
|
459
|
+
).length) {
|
|
460
|
+
throw new Error("Reporting Route cancellation requires linked verified, fixed Evidence covering the cancellation event.");
|
|
461
|
+
}
|
|
462
|
+
const result = await updateResource(loaded.root, entry.record.type, entry.record.id, {
|
|
463
|
+
...entry.record,
|
|
464
|
+
status: "canceled",
|
|
465
|
+
cancellation: {
|
|
466
|
+
canceledAt: String(options.canceledAt),
|
|
467
|
+
canceledById: authority.holderId,
|
|
468
|
+
authorityAppointmentId: authority.id,
|
|
469
|
+
reason: requiredText(options.reason, "Cancellation reason"),
|
|
470
|
+
evidenceIds
|
|
471
|
+
}
|
|
472
|
+
}, {
|
|
473
|
+
expectedRevision: options.expectedRevision || entry.revision,
|
|
474
|
+
workflowCapability: INTERNAL_WORKFLOW_CAPABILITIES.reportingRouteSetCancellation
|
|
475
|
+
});
|
|
476
|
+
return { ...result, commitRequired: true, nextAction: "Commit this cancellation before it changes route coverage." };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function scaffoldReportingRouteSet(input = {}) {
|
|
480
|
+
const action = input.action || "approve";
|
|
481
|
+
if (!["approve", "cancel", "successor"].includes(action)) {
|
|
482
|
+
throw new Error('Reporting Channel Set scaffold action must be "approve", "cancel", or "successor".');
|
|
483
|
+
}
|
|
484
|
+
if (action === "cancel") {
|
|
485
|
+
return {
|
|
486
|
+
routeSetId: input.routeSetId || "REPORTING_ROUTE_SET_ID",
|
|
487
|
+
approvalAppointmentId: input.approvalAppointmentId || "CANCELLATION_APPOINTMENT_ID",
|
|
488
|
+
canceledAt: input.canceledAt || new Date().toISOString(),
|
|
489
|
+
timezone: input.timezone || "IANA_TIMEZONE",
|
|
490
|
+
reason: input.reason || "CANCELLATION_REASON",
|
|
491
|
+
evidenceIds: [],
|
|
492
|
+
expectedRevision: input.expectedRevision || "CURRENT_ROUTE_SET_REVISION"
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const effectiveAt = input.effectiveAt || null;
|
|
496
|
+
const scaffold = {
|
|
497
|
+
routeSetId: input.routeSetId || "REPORTING_ROUTE_SET_ID",
|
|
498
|
+
proposalCommit: input.proposalCommit || "FULL_PROPOSAL_COMMIT",
|
|
499
|
+
approvalAppointmentId: input.approvalAppointmentId || "APPROVAL_APPOINTMENT_ID",
|
|
500
|
+
approvedAt: input.approvedAt || new Date().toISOString(),
|
|
501
|
+
effectiveAt,
|
|
502
|
+
timezone: input.timezone || "IANA_TIMEZONE",
|
|
503
|
+
evidenceIds: [],
|
|
504
|
+
expectedRevision: input.expectedRevision || "CURRENT_ROUTE_SET_REVISION"
|
|
505
|
+
};
|
|
506
|
+
if (action === "successor") {
|
|
507
|
+
scaffold.predecessorCancellationAppointmentId = input.predecessorCancellationAppointmentId || "PREDECESSOR_CANCELLATION_APPOINTMENT_ID";
|
|
508
|
+
scaffold.predecessorCancellationEvidenceIds = [];
|
|
509
|
+
scaffold.predecessorExpectedRevision = input.predecessorExpectedRevision || "CURRENT_PREDECESSOR_REVISION";
|
|
510
|
+
}
|
|
511
|
+
return scaffold;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function assessRouteSet(record, loaded, at, repository) {
|
|
515
|
+
const issues = [];
|
|
516
|
+
const approvalAssertionTiming = record.approval
|
|
517
|
+
? reportingRouteAssertionTiming(loaded, record, "approval")
|
|
518
|
+
: null;
|
|
519
|
+
const cancellationAssertionTiming = record.cancellation
|
|
520
|
+
? reportingRouteAssertionTiming(loaded, record, "cancellation")
|
|
521
|
+
: null;
|
|
522
|
+
const validEffectiveAt = typeof record.approval?.effectiveAt === "string"
|
|
523
|
+
&& !Number.isNaN(new Date(record.approval.effectiveAt).getTime());
|
|
524
|
+
const committed = COMMIT_REQUIRED_STATUSES.has(record.status)
|
|
525
|
+
? repository.available && repository.commit && committedRecordMatches(loaded, record, repository.commit)
|
|
526
|
+
: null;
|
|
527
|
+
if (COMMIT_REQUIRED_STATUSES.has(record.status) && !committed) {
|
|
528
|
+
issues.push(issue("reporting-route-commit-required", record.id, `${record.title} must be committed before this lifecycle state can be relied on.`));
|
|
529
|
+
}
|
|
530
|
+
if (record.status === "approved" && record.approval && repository.commit && !isDataHistoryAncestor(loaded, record.proposalCommit, repository.commit)) {
|
|
531
|
+
issues.push(issue("invalid-reporting-route-proposal-lineage", record.id, "The approved revision must descend from the exact committed proposal."));
|
|
532
|
+
}
|
|
533
|
+
if (["approved", "canceled"].includes(record.status)) {
|
|
534
|
+
issues.push(...reportingRouteLifecycleIssues(loaded, record));
|
|
535
|
+
}
|
|
536
|
+
if (["approved", "canceled"].includes(record.status) && !validEffectiveAt) {
|
|
537
|
+
issues.push(issue("invalid-reporting-route-approval", record.id, `${record.title} needs a valid approval and effective timestamp.`));
|
|
538
|
+
}
|
|
539
|
+
const effective = ["approved", "canceled"].includes(record.status)
|
|
540
|
+
&& committed
|
|
541
|
+
&& validEffectiveAt
|
|
542
|
+
&& new Date(record.approval.effectiveAt) <= at;
|
|
543
|
+
const validCanceledAt = typeof record.cancellation?.canceledAt === "string"
|
|
544
|
+
&& !Number.isNaN(new Date(record.cancellation.canceledAt).getTime());
|
|
545
|
+
const canceled = record.status === "canceled" && committed && validCanceledAt && new Date(record.cancellation.canceledAt) <= at;
|
|
546
|
+
let authorities = [];
|
|
547
|
+
if (effective && !canceled) {
|
|
548
|
+
try {
|
|
549
|
+
authorities = reportingRouteOngoingAuthorities(loaded.resources, record, at, loaded.workspace?.id);
|
|
550
|
+
} catch {
|
|
551
|
+
issues.push(issue("invalid-reporting-route-timezone", record.id, `${record.title} has an invalid approval timezone.`));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (effective && !canceled && !authorities.length) {
|
|
555
|
+
issues.push(issue("missing-reporting-route-authority", record.id, `${record.title} has no current ${record.authorityAppointmentKind} Appointment.`));
|
|
556
|
+
}
|
|
557
|
+
const state = issues.length
|
|
558
|
+
? "action"
|
|
559
|
+
: record.status === "draft" || (record.status === "approved" && !effective)
|
|
560
|
+
? "later"
|
|
561
|
+
: record.status === "proposed"
|
|
562
|
+
? "action"
|
|
563
|
+
: effective && !canceled
|
|
564
|
+
? "complete"
|
|
565
|
+
: "inactive";
|
|
566
|
+
return {
|
|
567
|
+
record,
|
|
568
|
+
committed,
|
|
569
|
+
effective,
|
|
570
|
+
canceled,
|
|
571
|
+
authorities,
|
|
572
|
+
approvalAssertionTiming,
|
|
573
|
+
cancellationAssertionTiming,
|
|
574
|
+
issues,
|
|
575
|
+
state
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function committedRecordMatches(loaded, record, commit) {
|
|
580
|
+
const entry = loaded.entries.find(({ record: candidate }) => candidate.id === record.id);
|
|
581
|
+
const historical = entry ? reportingRouteRecordAtRevision(loaded, entry, commit) : null;
|
|
582
|
+
return historical ? JSON.stringify(historical) === JSON.stringify(record) : false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function reportingRouteLifecycleIssues(loaded, record) {
|
|
586
|
+
const issues = [];
|
|
587
|
+
const entry = loaded.entries.find(({ record: candidate }) => candidate.id === record.id);
|
|
588
|
+
if (new Date(record.approval?.approvedAt) > new Date()) {
|
|
589
|
+
issues.push(issue("future-reporting-route-approval", record.id, "Approval time must be an actual nonfuture event time."));
|
|
590
|
+
}
|
|
591
|
+
const proposal = entry && /^[a-f0-9]{40}$/i.test(String(record.proposalCommit || ""))
|
|
592
|
+
? reportingRouteRecordAtRevision(loaded, entry, record.proposalCommit)
|
|
593
|
+
: null;
|
|
594
|
+
if (!proposal || proposal.status !== "proposed" || !sameRouteProposal(proposal, record)) {
|
|
595
|
+
issues.push(issue("changed-reporting-route-proposal", record.id, "The approved Route Set facts do not match the exact committed proposal."));
|
|
596
|
+
}
|
|
597
|
+
if (record.approval?.proposalCommit !== record.proposalCommit) {
|
|
598
|
+
issues.push(issue("invalid-reporting-route-proposal", record.id, "Approval proposalCommit does not match the Route Set proposalCommit."));
|
|
599
|
+
}
|
|
600
|
+
const approvalCommit = reportingRouteEventCommit(loaded, record, "approval");
|
|
601
|
+
if (approvalCommit && (
|
|
602
|
+
approvalCommit === record.proposalCommit
|
|
603
|
+
|| !isDataHistoryAncestor(loaded, record.proposalCommit, approvalCommit)
|
|
604
|
+
)) {
|
|
605
|
+
issues.push(issue("invalid-reporting-route-order", record.id, "The approval commit must descend from the exact proposal commit."));
|
|
606
|
+
}
|
|
607
|
+
const approvalAuthorityIssue = reportingRouteEventAuthorityIssueAtCommit(loaded, record, "approval");
|
|
608
|
+
if (approvalAuthorityIssue) issues.push(issue(approvalAuthorityIssue.code, record.id, approvalAuthorityIssue.message));
|
|
609
|
+
if (
|
|
610
|
+
!reportingRouteFixedEvidence(
|
|
611
|
+
loaded.resources,
|
|
612
|
+
record.id,
|
|
613
|
+
record.approval?.evidenceIds,
|
|
614
|
+
record.approval?.approvedAt,
|
|
615
|
+
record.approval?.timezone,
|
|
616
|
+
{ root: loaded.root }
|
|
617
|
+
).length
|
|
618
|
+
) {
|
|
619
|
+
issues.push(issue("missing-reporting-route-event-evidence", record.id, "Reporting Route approval needs linked verified, fixed Evidence covering the approval event."));
|
|
620
|
+
}
|
|
621
|
+
if (record.status === "canceled") {
|
|
622
|
+
const cancellationCommit = reportingRouteEventCommit(loaded, record, "cancellation");
|
|
623
|
+
if (approvalCommit && cancellationCommit && (
|
|
624
|
+
cancellationCommit === approvalCommit
|
|
625
|
+
|| !isDataHistoryAncestor(loaded, approvalCommit, cancellationCommit)
|
|
626
|
+
)) {
|
|
627
|
+
issues.push(issue("invalid-reporting-route-order", record.id, "The cancellation commit must descend from the approval commit."));
|
|
628
|
+
}
|
|
629
|
+
if (new Date(record.cancellation?.canceledAt) > new Date()) {
|
|
630
|
+
issues.push(issue("future-reporting-route-cancellation", record.id, "Cancellation time must be an actual nonfuture event time."));
|
|
631
|
+
}
|
|
632
|
+
const cancellationAuthorityIssue = reportingRouteEventAuthorityIssueAtCommit(loaded, record, "cancellation");
|
|
633
|
+
if (cancellationAuthorityIssue) issues.push(issue(cancellationAuthorityIssue.code, record.id, cancellationAuthorityIssue.message));
|
|
634
|
+
if (new Date(record.cancellation?.canceledAt) < new Date(record.approval?.effectiveAt)) {
|
|
635
|
+
issues.push(issue("invalid-reporting-route-order", record.id, "The Route Set was canceled before it became effective."));
|
|
636
|
+
}
|
|
637
|
+
if (
|
|
638
|
+
!reportingRouteFixedEvidence(
|
|
639
|
+
loaded.resources,
|
|
640
|
+
record.id,
|
|
641
|
+
record.cancellation?.evidenceIds,
|
|
642
|
+
record.cancellation?.canceledAt,
|
|
643
|
+
record.approval?.timezone,
|
|
644
|
+
{ root: loaded.root }
|
|
645
|
+
).length
|
|
646
|
+
) {
|
|
647
|
+
issues.push(issue("missing-reporting-route-event-evidence", record.id, "Reporting Route cancellation needs linked verified, fixed Evidence covering the cancellation event."));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return issues;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function sameRouteProposal(proposal, finalized) {
|
|
654
|
+
const allowed = new Set(["status", "proposalCommit", "approval", "cancellation"]);
|
|
655
|
+
const keys = new Set([...Object.keys(proposal), ...Object.keys(finalized)]);
|
|
656
|
+
return [...keys].every((key) => allowed.has(key) || JSON.stringify(proposal[key]) === JSON.stringify(finalized[key]));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function assertProposalMatches(loaded, entry, commit) {
|
|
660
|
+
const historical = reportingRouteRecordAtRevision(loaded, entry, commit);
|
|
661
|
+
if (!historical) throw new Error(`Proposal commit ${commit} does not contain valid JSON for Reporting Channel Set "${entry.record.id}".`);
|
|
662
|
+
if (JSON.stringify(historical) !== JSON.stringify(entry.record)) {
|
|
663
|
+
throw new Error("The current JSON does not exactly match the supplied proposal commit. Restore or repropose the changed Route Set.");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function approvalAuthority(loaded, routeSet, options, at, timezone) {
|
|
668
|
+
const appointment = loaded.resources.find(({ type, id }) => type === "appointment" && id === options.approvalAppointmentId);
|
|
669
|
+
const authorityIssue = reportingRouteEventAuthorityIssue(loaded.resources, routeSet, {
|
|
670
|
+
appointmentId: options.approvalAppointmentId,
|
|
671
|
+
actorId: options.approvedById || appointment?.holderId,
|
|
672
|
+
at,
|
|
673
|
+
timezone,
|
|
674
|
+
workspaceId: loaded.workspace.id
|
|
675
|
+
});
|
|
676
|
+
if (authorityIssue) throw new Error(authorityIssue.message);
|
|
677
|
+
return appointment;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function reportingRouteLanesIndependent(record, records, at) {
|
|
681
|
+
if (!record.alternateLane) return false;
|
|
682
|
+
for (const lane of [record.primaryLane, record.alternateLane]) {
|
|
683
|
+
if (!lane?.dependencyBasis) return false;
|
|
684
|
+
if (lane.dependencyBasis === "cataloged" && !lane.dependencySystemIds?.length) return false;
|
|
685
|
+
if (lane.dependencyBasis === "none" && !String(lane.dependencyRationale || "").trim()) return false;
|
|
686
|
+
}
|
|
687
|
+
const primary = new Set(record.primaryLane?.dependencySystemIds || []);
|
|
688
|
+
const overlap = (record.alternateLane.dependencySystemIds || []).filter((id) => primary.has(id));
|
|
689
|
+
if (!overlap.length) return true;
|
|
690
|
+
const date = localDateTimeValue(at, record.approval?.timezone || "UTC").slice(0, 10);
|
|
691
|
+
return records.some((candidate) => (
|
|
692
|
+
candidate.type === "exception"
|
|
693
|
+
&& candidate.status === "approved"
|
|
694
|
+
&& candidate.reportingRouteSetId === record.id
|
|
695
|
+
&& candidate.reportingRouteLanePair === "primary-alternate"
|
|
696
|
+
&& overlap.every((id) => candidate.dependencySystemIds?.includes(id))
|
|
697
|
+
&& candidate.approval?.approvedOn <= date
|
|
698
|
+
&& candidate.approval?.expiresOn >= date
|
|
699
|
+
));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function routeEntry(loaded, id) {
|
|
703
|
+
const entry = loaded.entries.find(({ record }) => record.type === "reporting-route-set" && record.id === id);
|
|
704
|
+
if (!entry) throw new Error(`Reporting Channel Set "${id || ""}" was not found.`);
|
|
705
|
+
return entry;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function assertTimestampZone(value, timezone, label) {
|
|
709
|
+
const source = typeof value === "string" ? value : value.toISOString();
|
|
710
|
+
const local = source.replace(/(?:Z|[+-]\d\d:\d\d)$/, "").replace(/\.\d+$/, "");
|
|
711
|
+
if (localDateTimeValue(value, timezone) !== local) {
|
|
712
|
+
throw new Error(`${label} UTC offset does not match ${timezone}.`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function pastOrPresentTimestamp(value, label) {
|
|
717
|
+
const result = instant(value, label);
|
|
718
|
+
if (result > new Date()) throw new Error(`${label} cannot be in the future.`);
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function instant(value, label) {
|
|
723
|
+
const result = value instanceof Date ? value : new Date(value);
|
|
724
|
+
if (Number.isNaN(result.getTime())) throw new Error(`${label} must be an RFC 3339 timestamp.`);
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function fullCommit(value, label) {
|
|
729
|
+
if (!/^[a-f0-9]{40}$/i.test(String(value || ""))) throw new Error(`${label} must be a full 40-character Git commit ID.`);
|
|
730
|
+
return value;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function timezoneName(value) {
|
|
734
|
+
try { new Intl.DateTimeFormat("en", { timeZone: value }).format(); } catch { throw new Error("An IANA timezone is required."); }
|
|
735
|
+
return value;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function requiredText(value, label) {
|
|
739
|
+
if (!String(value || "").trim()) throw new Error(`${label} is required.`);
|
|
740
|
+
return String(value).trim();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function issue(code, resourceId, message) {
|
|
744
|
+
return { code, resourceId, message };
|
|
745
|
+
}
|