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/README.md +16 -6
- package/model/index.js +37 -3
- package/model/v1.json +81 -47
- package/model/v2.json +8022 -0
- package/package.json +1 -1
- package/src/agent.js +36 -8
- package/src/audit-preparation.js +63 -60
- package/src/cli.js +168 -110
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +115 -75
- package/src/files.js +230 -28
- package/src/git.js +239 -41
- package/src/index.js +5 -5
- package/src/model-docs.js +88 -7
- package/src/model-migration.js +1463 -0
- package/src/mutation.js +42 -0
- package/src/obligations.js +108 -84
- package/src/parties.js +17 -2
- package/src/program-path.js +31 -58
- package/src/program-readiness.js +142 -106
- package/src/resource-status.js +17 -0
- package/src/server.js +110 -39
- package/src/setup.js +27 -28
- package/src/state.js +86 -25
- package/src/timing.js +41 -0
- package/src/validate.js +609 -43
- package/src/web.js +506 -129
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/src/mutation.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveWorkspaceRoot } from "./paths.js";
|
|
|
3
3
|
|
|
4
4
|
const mutationQueues = new Map();
|
|
5
5
|
const activeMutation = new AsyncLocalStorage();
|
|
6
|
+
const deferredValidation = new AsyncLocalStorage();
|
|
6
7
|
|
|
7
8
|
export function serializeWorkspaceMutation(input, task) {
|
|
8
9
|
const root = resolveWorkspaceRoot(input);
|
|
@@ -16,3 +17,44 @@ export function serializeWorkspaceMutation(input, task) {
|
|
|
16
17
|
mutationQueues.set(root, tracked);
|
|
17
18
|
return tracked;
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
export function withDeferredWorkspaceValidation(task) {
|
|
22
|
+
return deferredValidation.run(true, task);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function workspaceValidationDeferred() {
|
|
26
|
+
return deferredValidation.getStore() === true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeResourceMutation(value, options = {}) {
|
|
30
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
31
|
+
throw new Error("A { record, content, revision, contentRevisions } mutation object is required.");
|
|
32
|
+
}
|
|
33
|
+
if (!Object.hasOwn(value, "record")) {
|
|
34
|
+
throw new Error("A mutation envelope with a record property is required.");
|
|
35
|
+
}
|
|
36
|
+
if (!value.record || Array.isArray(value.record) || typeof value.record !== "object") {
|
|
37
|
+
throw new Error("Mutation record must be a JSON object.");
|
|
38
|
+
}
|
|
39
|
+
if (value.content !== undefined && (Array.isArray(value.content) || typeof value.content !== "object" || value.content === null)) {
|
|
40
|
+
throw new Error("Mutation content must be an object keyed by Markdown slot.");
|
|
41
|
+
}
|
|
42
|
+
if (value.revision !== undefined && typeof value.revision !== "string") {
|
|
43
|
+
throw new Error("Mutation revision must be a string.");
|
|
44
|
+
}
|
|
45
|
+
if (options.requireRevision && (typeof value.revision !== "string" || value.revision.length === 0)) {
|
|
46
|
+
throw new Error("Mutation revision is required when updating a resource.");
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
value.contentRevisions !== undefined
|
|
50
|
+
&& (Array.isArray(value.contentRevisions) || typeof value.contentRevisions !== "object" || value.contentRevisions === null)
|
|
51
|
+
) {
|
|
52
|
+
throw new Error("Mutation contentRevisions must be an object keyed by data-relative Markdown path.");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
record: value.record,
|
|
56
|
+
content: value.content,
|
|
57
|
+
revision: value.revision,
|
|
58
|
+
contentRevisions: value.contentRevisions
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/obligations.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createResourceId } from "./id.js";
|
|
2
2
|
import { createResourceAndLink, createResources, updateResource } from "./files.js";
|
|
3
|
+
import { loadModel } from "../model/index.js";
|
|
4
|
+
import { coverageEnd } from "./coverage.js";
|
|
3
5
|
import {
|
|
4
6
|
addCalendarDays,
|
|
5
7
|
calendarDayDifference,
|
|
@@ -16,22 +18,25 @@ const COMPLETION_DATE_FIELDS = [
|
|
|
16
18
|
"completedOn",
|
|
17
19
|
"performedOn",
|
|
18
20
|
"reviewedOn",
|
|
19
|
-
"assessmentDate",
|
|
20
|
-
"meetingDate",
|
|
21
|
-
"scheduledOn",
|
|
22
21
|
"occurredOn",
|
|
23
22
|
"collectedOn",
|
|
24
23
|
"verifiedOn",
|
|
25
24
|
"approvedOn",
|
|
26
25
|
"submittedOn",
|
|
27
26
|
"closedOn",
|
|
28
|
-
"reportDate"
|
|
29
|
-
|
|
27
|
+
"reportDate"
|
|
28
|
+
];
|
|
29
|
+
const COMPLETION_TIMESTAMP_FIELDS = [
|
|
30
|
+
"completedAt",
|
|
31
|
+
"endedAt",
|
|
32
|
+
"closedAt",
|
|
33
|
+
"provisionedOn",
|
|
34
|
+
"deprovisionedOn"
|
|
30
35
|
];
|
|
31
36
|
const MAX_PLANNED_ITEMS = 10_000;
|
|
32
|
-
const DEFAULT_EVENT_DEADLINE_DAYS = 30;
|
|
33
37
|
|
|
34
38
|
export function planObligations(resources, options = {}) {
|
|
39
|
+
const model = options.model || loadModel("2");
|
|
35
40
|
const asOf = requireDate(options.asOf ?? new Date().toISOString().slice(0, 10), "as-of date");
|
|
36
41
|
const defaultNow = options.asOf ? `${asOf}T23:59:59Z` : new Date().toISOString();
|
|
37
42
|
const now = requireTimestamp(options.now ?? defaultNow, "current timestamp");
|
|
@@ -56,12 +61,15 @@ export function planObligations(resources, options = {}) {
|
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
for (const obligation of obligations) {
|
|
64
|
+
const activity = obligationActivity(model, obligation.activityType);
|
|
65
|
+
const expectedCompletionTypes = activity.completionResourceTypes;
|
|
59
66
|
const programStatus = obligationProgramStatus(obligation, byId, asOf);
|
|
60
67
|
if (obligation.recurrence?.mode === "event" && obligation.recurrence.eventType) {
|
|
61
68
|
const eventType = obligation.recurrence.eventType;
|
|
62
69
|
const group = triggerGroups.get(eventType) ?? {
|
|
63
70
|
eventType,
|
|
64
|
-
|
|
71
|
+
title: model.policyEvents?.[eventType]?.title || humanize(eventType),
|
|
72
|
+
prompt: obligation.triggerPrompt || model.policyEvents?.[eventType]?.title || humanize(eventType),
|
|
65
73
|
policyIds: [],
|
|
66
74
|
obligationIds: [],
|
|
67
75
|
programStatus,
|
|
@@ -79,7 +87,8 @@ export function planObligations(resources, options = {}) {
|
|
|
79
87
|
controlIds: obligation.controlIds || [],
|
|
80
88
|
scopeResourceIds: obligation.scopeResourceIds || [],
|
|
81
89
|
templateResourceId: obligation.templateResourceId || null,
|
|
82
|
-
completionResourceTypes:
|
|
90
|
+
completionResourceTypes: expectedCompletionTypes,
|
|
91
|
+
completionType: activity.completionType,
|
|
83
92
|
programStatus,
|
|
84
93
|
window: normalizedEventWindow(obligation.window)
|
|
85
94
|
});
|
|
@@ -113,7 +122,7 @@ export function planObligations(resources, options = {}) {
|
|
|
113
122
|
.filter((record) => (
|
|
114
123
|
record
|
|
115
124
|
&& completionFallsInWindow(record, window)
|
|
116
|
-
&& completionTypeMatches(record,
|
|
125
|
+
&& completionTypeMatches(record, expectedCompletionTypes)
|
|
117
126
|
));
|
|
118
127
|
const timingStatus = occurrenceStatus(window, asOf, completions.length > 0);
|
|
119
128
|
const status = timingStatus === "complete" || programStatus === "accepted" ? timingStatus : "proposed";
|
|
@@ -131,6 +140,8 @@ export function planObligations(resources, options = {}) {
|
|
|
131
140
|
policyIds: obligation.policyIds || [],
|
|
132
141
|
controlIds: obligation.controlIds || [],
|
|
133
142
|
scopeResourceIds: obligation.scopeResourceIds || [],
|
|
143
|
+
completionResourceTypes: expectedCompletionTypes,
|
|
144
|
+
completionType: activity.completionType,
|
|
134
145
|
completionResourceIds: completions.map((record) => record.id),
|
|
135
146
|
status,
|
|
136
147
|
timingStatus,
|
|
@@ -156,7 +167,14 @@ export function planObligations(resources, options = {}) {
|
|
|
156
167
|
if (!actionsBySource.has(record.sourceResourceId)) actionsBySource.set(record.sourceResourceId, []);
|
|
157
168
|
actionsBySource.get(record.sourceResourceId).push(record);
|
|
158
169
|
}
|
|
159
|
-
const eventRuns = events.map((event) => planEventRun(
|
|
170
|
+
const eventRuns = events.map((event) => planEventRun(
|
|
171
|
+
event,
|
|
172
|
+
actionsBySource.get(event.id) || [],
|
|
173
|
+
byId,
|
|
174
|
+
asOf,
|
|
175
|
+
now,
|
|
176
|
+
model
|
|
177
|
+
));
|
|
160
178
|
const eventItems = eventRuns
|
|
161
179
|
.filter((run) => run.status !== "canceled")
|
|
162
180
|
.flatMap((run) => run.actions)
|
|
@@ -216,7 +234,7 @@ export async function createObligationEvent(input, options) {
|
|
|
216
234
|
if (templates.some((record) => obligationProgramStatus(record, byId, occurredOn) === "proposed")) {
|
|
217
235
|
throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing policy effective and implement at least one linked control before starting this workflow.`);
|
|
218
236
|
}
|
|
219
|
-
if (templates.some((record) =>
|
|
237
|
+
if (templates.some((record) => normalizedEventWindow(record.window).precision === "timestamp") && !occurredAt) {
|
|
220
238
|
throw new Error(`Event type "${eventType}" has hour-based deadlines and requires an RFC 3339 occurredAt timestamp.`);
|
|
221
239
|
}
|
|
222
240
|
const subjectResourceIds = [...new Set((options.subjectResourceIds || []).map(String).filter(Boolean))];
|
|
@@ -230,7 +248,6 @@ export async function createObligationEvent(input, options) {
|
|
|
230
248
|
existingIds.push(id);
|
|
231
249
|
const window = eventWindow(obligation, occurredOn, occurredAt, loaded.workspace.timezone);
|
|
232
250
|
return {
|
|
233
|
-
schemaVersion: 1,
|
|
234
251
|
id,
|
|
235
252
|
type: "action-item",
|
|
236
253
|
title: obligation.title,
|
|
@@ -238,16 +255,11 @@ export async function createObligationEvent(input, options) {
|
|
|
238
255
|
assigneeIds: obligation.ownerIds || [],
|
|
239
256
|
sourceResourceId: eventId,
|
|
240
257
|
obligationId: obligation.id,
|
|
241
|
-
description: eventActionDescription(obligation, eventType),
|
|
242
|
-
|
|
243
|
-
...(window.dueWindowEnd ? { dueWindowEnd: window.dueWindowEnd, dueOn: window.dueWindowEnd } : {}),
|
|
244
|
-
...(window.overdueOn ? { overdueOn: window.overdueOn } : {}),
|
|
245
|
-
...(window.dueWindowStartAt ? { dueWindowStartAt: window.dueWindowStartAt } : {}),
|
|
246
|
-
...(window.dueWindowEndAt ? { dueWindowEndAt: window.dueWindowEndAt, overdueAt: window.dueWindowEndAt } : {})
|
|
258
|
+
description: eventActionDescription(obligation, eventType, loaded.model),
|
|
259
|
+
completionWindow: storedCompletionWindow(window, loaded.workspace.timezone)
|
|
247
260
|
};
|
|
248
261
|
});
|
|
249
262
|
const event = {
|
|
250
|
-
schemaVersion: 1,
|
|
251
263
|
id: eventId,
|
|
252
264
|
type: "obligation-event",
|
|
253
265
|
title,
|
|
@@ -269,7 +281,7 @@ export async function completeObligationOccurrence(input, options) {
|
|
|
269
281
|
record.type === "obligation" && record.id === options?.obligationId
|
|
270
282
|
));
|
|
271
283
|
if (!obligation) throw new Error(`Obligation "${options?.obligationId ?? ""}" was not found.`);
|
|
272
|
-
assertExpectedCompletionType(obligation, options?.record);
|
|
284
|
+
assertExpectedCompletionType(obligation, options?.record, loaded.model);
|
|
273
285
|
return createResourceAndLink(loaded.root, options.record, {
|
|
274
286
|
type: "obligation",
|
|
275
287
|
id: obligation.id,
|
|
@@ -289,7 +301,7 @@ export async function completeObligationAction(input, options) {
|
|
|
289
301
|
record.type === "obligation" && record.id === action.obligationId
|
|
290
302
|
));
|
|
291
303
|
if (!obligation) throw new Error(`Obligation "${action.obligationId}" was not found.`);
|
|
292
|
-
assertExpectedCompletionType(obligation, options?.record);
|
|
304
|
+
assertExpectedCompletionType(obligation, options?.record, loaded.model);
|
|
293
305
|
const completedOn = requireDate(options?.completedOn, "completion date");
|
|
294
306
|
const event = loaded.resources.find((record) => record.type === "obligation-event" && record.id === action.sourceResourceId);
|
|
295
307
|
if (event?.occurredOn && completedOn < event.occurredOn) {
|
|
@@ -320,7 +332,8 @@ export async function completeObligationEvent(input, options) {
|
|
|
320
332
|
const plan = planObligations(loaded.resources, {
|
|
321
333
|
asOf: completedOn,
|
|
322
334
|
through: completedOn,
|
|
323
|
-
includeComplete: true
|
|
335
|
+
includeComplete: true,
|
|
336
|
+
model: loaded.model
|
|
324
337
|
});
|
|
325
338
|
const run = plan.eventRuns.find((item) => item.id === event.id);
|
|
326
339
|
if (!run || run.actions.length === 0) {
|
|
@@ -341,11 +354,11 @@ export async function completeObligationEvent(input, options) {
|
|
|
341
354
|
});
|
|
342
355
|
}
|
|
343
356
|
|
|
344
|
-
function assertExpectedCompletionType(obligation, record) {
|
|
357
|
+
function assertExpectedCompletionType(obligation, record, model) {
|
|
345
358
|
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
346
359
|
throw new Error("A completion resource record is required.");
|
|
347
360
|
}
|
|
348
|
-
const expected = obligation.completionResourceTypes
|
|
361
|
+
const expected = obligationActivity(model, obligation.activityType).completionResourceTypes;
|
|
349
362
|
if (expected.length && !expected.includes(record.type)) {
|
|
350
363
|
throw new Error(
|
|
351
364
|
`Obligation "${obligation.id}" expects a completion resource of type ${expected.join(" or ")}, not "${record.type ?? ""}".`
|
|
@@ -357,10 +370,12 @@ function calendarWindow(recurrence, configuredWindow, index) {
|
|
|
357
370
|
const occurrence = calendarOccurrence(recurrence, index);
|
|
358
371
|
const next = calendarOccurrence(recurrence, index + 1);
|
|
359
372
|
if (!occurrence || !next) return null;
|
|
360
|
-
const startOffset = Number.isInteger(configuredWindow
|
|
373
|
+
const startOffset = configuredWindow?.precision === "date" && Number.isInteger(configuredWindow.startsAfter)
|
|
374
|
+
? configuredWindow.startsAfter
|
|
375
|
+
: 0;
|
|
361
376
|
const dueWindowStart = addCalendarDays(occurrence, startOffset);
|
|
362
|
-
const dueWindowEnd = Number.isInteger(configuredWindow
|
|
363
|
-
? addCalendarDays(occurrence, configuredWindow.
|
|
377
|
+
const dueWindowEnd = configuredWindow?.precision === "date" && Number.isInteger(configuredWindow.dueAfter)
|
|
378
|
+
? addCalendarDays(occurrence, configuredWindow.dueAfter)
|
|
364
379
|
: addCalendarDays(next, -1);
|
|
365
380
|
const overdueOn = dueWindowEnd ? addCalendarDays(dueWindowEnd, 1) : null;
|
|
366
381
|
if (!dueWindowStart || !dueWindowEnd || !overdueOn) return null;
|
|
@@ -373,10 +388,10 @@ function calendarWindow(recurrence, configuredWindow, index) {
|
|
|
373
388
|
|
|
374
389
|
function eventWindow(obligation, occurredOn, occurredAt, timezone) {
|
|
375
390
|
const configuredWindow = normalizedEventWindow(obligation.window);
|
|
376
|
-
if (
|
|
377
|
-
const startOffset = Number.isInteger(configuredWindow.
|
|
391
|
+
if (configuredWindow.precision === "timestamp" && occurredAt) {
|
|
392
|
+
const startOffset = Number.isInteger(configuredWindow.startsAfter) ? configuredWindow.startsAfter : 0;
|
|
378
393
|
const dueWindowStartAt = addHours(occurredAt, startOffset);
|
|
379
|
-
const dueWindowEndAt = addHours(occurredAt, configuredWindow.
|
|
394
|
+
const dueWindowEndAt = addHours(occurredAt, configuredWindow.dueAfter);
|
|
380
395
|
return {
|
|
381
396
|
dueWindowStart: timestampCalendarDate(dueWindowStartAt, timezone),
|
|
382
397
|
dueWindowEnd: timestampCalendarDate(dueWindowEndAt, timezone),
|
|
@@ -386,9 +401,9 @@ function eventWindow(obligation, occurredOn, occurredAt, timezone) {
|
|
|
386
401
|
overdueAt: dueWindowEndAt
|
|
387
402
|
};
|
|
388
403
|
}
|
|
389
|
-
const startOffset = Number.isInteger(configuredWindow.
|
|
404
|
+
const startOffset = Number.isInteger(configuredWindow.startsAfter) ? configuredWindow.startsAfter : 0;
|
|
390
405
|
const dueWindowStart = addCalendarDays(occurredOn, startOffset);
|
|
391
|
-
const dueWindowEnd = addCalendarDays(occurredOn, configuredWindow.
|
|
406
|
+
const dueWindowEnd = addCalendarDays(occurredOn, configuredWindow.dueAfter);
|
|
392
407
|
const overdueOn = dueWindowEnd ? addCalendarDays(dueWindowEnd, 1) : null;
|
|
393
408
|
if (!dueWindowStart || !dueWindowEnd || !overdueOn) {
|
|
394
409
|
throw new Error("Event deadline dates must fall within the supported calendar range.");
|
|
@@ -400,11 +415,13 @@ function eventWindow(obligation, occurredOn, occurredAt, timezone) {
|
|
|
400
415
|
};
|
|
401
416
|
}
|
|
402
417
|
|
|
403
|
-
function planEventRun(event, actionItems, byId, asOf, now) {
|
|
418
|
+
function planEventRun(event, actionItems, byId, asOf, now, model = loadModel("2")) {
|
|
404
419
|
const actions = actionItems
|
|
405
420
|
.map((record) => {
|
|
406
421
|
const obligation = byId.get(record.obligationId);
|
|
407
|
-
const expectedCompletionTypes = obligation?.
|
|
422
|
+
const expectedCompletionTypes = obligation?.type === "obligation"
|
|
423
|
+
? obligationActivity(model, obligation.activityType).completionResourceTypes
|
|
424
|
+
: [];
|
|
408
425
|
const linkedCompletionIds = [...new Set([
|
|
409
426
|
...(record.completionResourceIds || []),
|
|
410
427
|
...(record.evidenceIds || [])
|
|
@@ -413,28 +430,7 @@ function planEventRun(event, actionItems, byId, asOf, now) {
|
|
|
413
430
|
const completionSatisfied = expectedCompletionTypes.length === 0
|
|
414
431
|
|| matchingCompletionIds.length > 0;
|
|
415
432
|
const complete = record.status === "done" && completionSatisfied;
|
|
416
|
-
const
|
|
417
|
-
const fallbackEndOffsetDays = Number.isInteger(configuredWindow.endOffsetDays)
|
|
418
|
-
? configuredWindow.endOffsetDays
|
|
419
|
-
: Math.ceil(configuredWindow.endOffsetHours / 24);
|
|
420
|
-
const dueWindowStart = record.dueWindowStart || event.occurredOn;
|
|
421
|
-
const dueWindowEnd = record.dueWindowEnd || record.dueOn || addCalendarDays(event.occurredOn, fallbackEndOffsetDays);
|
|
422
|
-
const dueWindowStartAt = record.dueWindowStartAt
|
|
423
|
-
|| (event.occurredAt && Number.isInteger(configuredWindow.startOffsetHours)
|
|
424
|
-
? addHours(event.occurredAt, configuredWindow.startOffsetHours)
|
|
425
|
-
: null);
|
|
426
|
-
const dueWindowEndAt = record.dueWindowEndAt
|
|
427
|
-
|| (event.occurredAt && Number.isInteger(configuredWindow.endOffsetHours)
|
|
428
|
-
? addHours(event.occurredAt, configuredWindow.endOffsetHours)
|
|
429
|
-
: null);
|
|
430
|
-
const window = {
|
|
431
|
-
dueWindowStart,
|
|
432
|
-
dueWindowEnd,
|
|
433
|
-
overdueOn: record.overdueOn || (dueWindowEnd ? addCalendarDays(dueWindowEnd, 1) : null),
|
|
434
|
-
dueWindowStartAt,
|
|
435
|
-
dueWindowEndAt,
|
|
436
|
-
overdueAt: record.overdueAt || dueWindowEndAt
|
|
437
|
-
};
|
|
433
|
+
const window = plannedCompletionWindow(record.completionWindow);
|
|
438
434
|
const status = complete
|
|
439
435
|
? "complete"
|
|
440
436
|
: window.overdueAt && new Date(now) > new Date(window.overdueAt)
|
|
@@ -498,18 +494,7 @@ function planEventRun(event, actionItems, byId, asOf, now) {
|
|
|
498
494
|
|
|
499
495
|
function planStandaloneAction(record, byId, asOf, now) {
|
|
500
496
|
const source = byId.get(record.sourceResourceId);
|
|
501
|
-
const
|
|
502
|
-
const dueWindowStart = record.dueWindowStart || dueWindowEnd;
|
|
503
|
-
const dueWindowEndAt = record.dueWindowEndAt || null;
|
|
504
|
-
const dueWindowStartAt = record.dueWindowStartAt || dueWindowEndAt;
|
|
505
|
-
const window = {
|
|
506
|
-
dueWindowStart,
|
|
507
|
-
dueWindowEnd,
|
|
508
|
-
overdueOn: record.overdueOn || (dueWindowEnd ? addCalendarDays(dueWindowEnd, 1) : null),
|
|
509
|
-
dueWindowStartAt,
|
|
510
|
-
dueWindowEndAt,
|
|
511
|
-
overdueAt: record.overdueAt || dueWindowEndAt
|
|
512
|
-
};
|
|
497
|
+
const window = plannedCompletionWindow(record.completionWindow);
|
|
513
498
|
const complete = ["done", "canceled"].includes(record.status);
|
|
514
499
|
const status = complete
|
|
515
500
|
? "complete"
|
|
@@ -543,27 +528,54 @@ function planStandaloneAction(record, byId, asOf, now) {
|
|
|
543
528
|
};
|
|
544
529
|
}
|
|
545
530
|
|
|
531
|
+
function storedCompletionWindow(window, timezone) {
|
|
532
|
+
if (window.dueWindowEndAt) {
|
|
533
|
+
return {
|
|
534
|
+
precision: "timestamp",
|
|
535
|
+
startsAt: window.dueWindowStartAt,
|
|
536
|
+
dueAt: window.dueWindowEndAt,
|
|
537
|
+
overdueAt: window.overdueAt || window.dueWindowEndAt,
|
|
538
|
+
timezone
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
precision: "date",
|
|
543
|
+
startsOn: window.dueWindowStart,
|
|
544
|
+
dueOn: window.dueWindowEnd,
|
|
545
|
+
overdueOn: window.overdueOn
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function plannedCompletionWindow(window) {
|
|
550
|
+
if (window?.precision === "timestamp") {
|
|
551
|
+
return {
|
|
552
|
+
dueWindowStart: timestampCalendarDate(window.startsAt, window.timezone),
|
|
553
|
+
dueWindowEnd: timestampCalendarDate(window.dueAt, window.timezone),
|
|
554
|
+
overdueOn: timestampCalendarDate(window.overdueAt, window.timezone),
|
|
555
|
+
dueWindowStartAt: window.startsAt,
|
|
556
|
+
dueWindowEndAt: window.dueAt,
|
|
557
|
+
overdueAt: window.overdueAt
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
dueWindowStart: window?.startsOn || null,
|
|
562
|
+
dueWindowEnd: window?.dueOn || null,
|
|
563
|
+
overdueOn: window?.overdueOn || null,
|
|
564
|
+
dueWindowStartAt: null,
|
|
565
|
+
dueWindowEndAt: null,
|
|
566
|
+
overdueAt: null
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
546
570
|
function completionFallsInWindow(record, window) {
|
|
547
571
|
const date = completionDate(record);
|
|
548
572
|
return Boolean(date && date >= window.dueWindowStart && date <= window.dueWindowEnd);
|
|
549
573
|
}
|
|
550
574
|
|
|
551
575
|
function normalizedEventWindow(configuredWindow) {
|
|
552
|
-
|
|
576
|
+
return configuredWindow && !Array.isArray(configuredWindow) && typeof configuredWindow === "object"
|
|
553
577
|
? configuredWindow
|
|
554
578
|
: {};
|
|
555
|
-
if (Number.isInteger(window.endOffsetDays) || Number.isInteger(window.endOffsetHours)) return window;
|
|
556
|
-
if (Number.isInteger(window.startOffsetHours)) {
|
|
557
|
-
return {
|
|
558
|
-
...window,
|
|
559
|
-
endOffsetHours: window.startOffsetHours + (DEFAULT_EVENT_DEADLINE_DAYS * 24)
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
return {
|
|
563
|
-
...window,
|
|
564
|
-
startOffsetDays: Number.isInteger(window.startOffsetDays) ? window.startOffsetDays : 0,
|
|
565
|
-
endOffsetDays: DEFAULT_EVENT_DEADLINE_DAYS
|
|
566
|
-
};
|
|
567
579
|
}
|
|
568
580
|
|
|
569
581
|
function completionTypeMatches(record, expectedTypes = []) {
|
|
@@ -571,6 +583,11 @@ function completionTypeMatches(record, expectedTypes = []) {
|
|
|
571
583
|
}
|
|
572
584
|
|
|
573
585
|
function completionDate(record) {
|
|
586
|
+
const coverageDate = coverageEnd(record.coverage);
|
|
587
|
+
if (parseCalendarDate(coverageDate)) return coverageDate;
|
|
588
|
+
for (const field of COMPLETION_TIMESTAMP_FIELDS) {
|
|
589
|
+
if (isRfc3339Timestamp(record[field])) return record[field].slice(0, 10);
|
|
590
|
+
}
|
|
574
591
|
for (const field of COMPLETION_DATE_FIELDS) {
|
|
575
592
|
if (parseCalendarDate(record[field])) return record[field];
|
|
576
593
|
}
|
|
@@ -625,15 +642,22 @@ function comparePlannedItems(a, b) {
|
|
|
625
642
|
|| a.title.localeCompare(b.title);
|
|
626
643
|
}
|
|
627
644
|
|
|
628
|
-
function eventActionDescription(obligation, eventType) {
|
|
645
|
+
function eventActionDescription(obligation, eventType, model) {
|
|
629
646
|
const policy = obligation.policyIds?.length ? ` Policy sources: ${obligation.policyIds.join(", ")}.` : "";
|
|
630
647
|
const scope = obligation.scopeResourceIds?.length ? ` Review scoped resources: ${obligation.scopeResourceIds.join(", ")}.` : "";
|
|
631
|
-
const
|
|
632
|
-
|
|
648
|
+
const expected = obligationActivity(model, obligation.activityType).completionResourceTypes;
|
|
649
|
+
const completion = expected.length
|
|
650
|
+
? ` Link completion records of type ${expected.join(", ")} and any evidence before marking this done.`
|
|
633
651
|
: " Link the completion record and evidence before marking this done.";
|
|
634
652
|
return `Triggered by ${eventType}.${policy}${scope}${completion}`;
|
|
635
653
|
}
|
|
636
654
|
|
|
655
|
+
function obligationActivity(model, activityType) {
|
|
656
|
+
const activity = model.obligationActivities?.[activityType];
|
|
657
|
+
if (!activity) throw new Error(`Unknown obligation activity type "${activityType ?? ""}".`);
|
|
658
|
+
return activity;
|
|
659
|
+
}
|
|
660
|
+
|
|
637
661
|
function requireDate(value, label) {
|
|
638
662
|
if (!parseCalendarDate(value)) throw new Error(`A valid ${label} is required.`);
|
|
639
663
|
return value;
|
package/src/parties.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const CURRENT_PERSON_STATUSES = new Set(["active"
|
|
1
|
+
const CURRENT_PERSON_STATUSES = new Set(["active"]);
|
|
2
2
|
const CURRENT_TEAM_STATUSES = new Set(["active"]);
|
|
3
|
+
const CURRENT_APPOINTMENT_STATUSES = new Set(["active"]);
|
|
3
4
|
|
|
4
5
|
export function partyPeople(ids = [], byId, options = {}, seen = new Set()) {
|
|
5
6
|
const people = new Set();
|
|
@@ -26,6 +27,19 @@ export function partyPeople(ids = [], byId, options = {}, seen = new Set()) {
|
|
|
26
27
|
people.add(personId);
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
if (
|
|
31
|
+
record?.type === "appointment"
|
|
32
|
+
&& (!options.appointmentStatuses || options.appointmentStatuses.has(record.status))
|
|
33
|
+
) {
|
|
34
|
+
for (const personId of partyPeople(
|
|
35
|
+
[record.holderId],
|
|
36
|
+
byId,
|
|
37
|
+
options,
|
|
38
|
+
seen
|
|
39
|
+
)) {
|
|
40
|
+
people.add(personId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
29
43
|
}
|
|
30
44
|
return people;
|
|
31
45
|
}
|
|
@@ -33,7 +47,8 @@ export function partyPeople(ids = [], byId, options = {}, seen = new Set()) {
|
|
|
33
47
|
export function currentPartyPeople(ids = [], byId) {
|
|
34
48
|
return partyPeople(ids, byId, {
|
|
35
49
|
personStatuses: CURRENT_PERSON_STATUSES,
|
|
36
|
-
teamStatuses: CURRENT_TEAM_STATUSES
|
|
50
|
+
teamStatuses: CURRENT_TEAM_STATUSES,
|
|
51
|
+
appointmentStatuses: CURRENT_APPOINTMENT_STATUSES
|
|
37
52
|
});
|
|
38
53
|
}
|
|
39
54
|
|