filegrc 0.7.0 → 0.8.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 +14 -6
- package/model/index.js +21 -4
- package/model/v4.json +97 -6
- package/model/v5.json +10233 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +635 -81
- package/src/audit-transition.js +8 -2
- package/src/batch-review.js +21 -11
- package/src/cli.js +117 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +145 -0
- package/src/evidence-packet.js +393 -106
- package/src/external-reviewer.js +5 -4
- package/src/files.js +194 -35
- package/src/git.js +106 -10
- package/src/index.js +10 -1
- package/src/model-migration.js +218 -7
- package/src/obligations.js +14 -11
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +827 -0
- package/src/program-lifecycle.js +93 -3
- package/src/program-path.js +14 -9
- package/src/program-readiness.js +306 -75
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +29 -7
- package/src/setup.js +25 -5
- package/src/soc2.js +228 -0
- package/src/state.js +8 -0
- package/src/validate.js +168 -14
- package/src/web.js +257 -35
- package/src/workflow.js +107 -44
- package/src/workspace.js +5 -0
package/src/model-migration.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createResourceId } from "./id.js";
|
|
2
|
-
import { applyResourceBatch, contentRevision } from "./files.js";
|
|
2
|
+
import { applyModelMigrationBatch, applyResourceBatch, contentRevision } from "./files.js";
|
|
3
3
|
import { loadWorkspace } from "./workspace.js";
|
|
4
4
|
import { ACTIVE_MODEL_VERSION, loadModel } from "../model/index.js";
|
|
5
5
|
import { legacyCoverage } from "./coverage.js";
|
|
@@ -882,7 +882,7 @@ async function migrateV1ToV2(input = process.cwd(), options = {}) {
|
|
|
882
882
|
+ "and resolve every missing value, conflict, and manual action."
|
|
883
883
|
);
|
|
884
884
|
}
|
|
885
|
-
const result = await
|
|
885
|
+
const result = await applyModelMigrationBatch(input, plan.changes);
|
|
886
886
|
return { ...plan, applied: true, result };
|
|
887
887
|
}
|
|
888
888
|
|
|
@@ -891,7 +891,7 @@ export async function planModelMigration(input = process.cwd(), options = {}) {
|
|
|
891
891
|
const sourceVersion = String(loaded.workspace?.dataModelVersion || "");
|
|
892
892
|
const requestedTarget = options.targetModelVersion
|
|
893
893
|
? String(options.targetModelVersion)
|
|
894
|
-
: sourceVersion === "1" ? V1_TARGET_MODEL_VERSION : sourceVersion === "2" ? "3" : ACTIVE_MODEL_VERSION;
|
|
894
|
+
: sourceVersion === "1" ? V1_TARGET_MODEL_VERSION : sourceVersion === "2" ? "3" : sourceVersion === "3" ? "4" : ACTIVE_MODEL_VERSION;
|
|
895
895
|
if (sourceVersion === requestedTarget) return emptyPlan(sourceVersion, requestedTarget);
|
|
896
896
|
if (sourceVersion === "1" && requestedTarget === "2") {
|
|
897
897
|
return planV1ToV2Migration(input, options);
|
|
@@ -899,13 +899,16 @@ export async function planModelMigration(input = process.cwd(), options = {}) {
|
|
|
899
899
|
if (sourceVersion === "2" && requestedTarget === "3") {
|
|
900
900
|
return planV2ToV3Migration(loaded);
|
|
901
901
|
}
|
|
902
|
-
if (sourceVersion === "3" && requestedTarget ===
|
|
902
|
+
if (sourceVersion === "3" && requestedTarget === "4") {
|
|
903
903
|
return planV3ToV4Migration(loaded, options);
|
|
904
904
|
}
|
|
905
|
+
if (sourceVersion === "4" && requestedTarget === ACTIVE_MODEL_VERSION) {
|
|
906
|
+
return planV4ToV5Migration(loaded, options);
|
|
907
|
+
}
|
|
905
908
|
if (sourceVersion === "1" && requestedTarget === ACTIVE_MODEL_VERSION) {
|
|
906
909
|
throw new Error(
|
|
907
910
|
"Model v1 workspaces must migrate to model v2 first. "
|
|
908
|
-
+ "Preview and apply `npx filegrc migrate --to-model 2`, then migrate
|
|
911
|
+
+ "Preview and apply `npx filegrc migrate --to-model 2`, then migrate one version at a time through model v5."
|
|
909
912
|
);
|
|
910
913
|
}
|
|
911
914
|
throw new Error(`Model migration does not support v${sourceVersion} to v${requestedTarget}.`);
|
|
@@ -925,7 +928,7 @@ export async function migrateModel(input = process.cwd(), options = {}) {
|
|
|
925
928
|
if (plan.sourceModelVersion === "1" && plan.targetModelVersion === "2") {
|
|
926
929
|
return migrateV1ToV2(input, options);
|
|
927
930
|
}
|
|
928
|
-
const result = await
|
|
931
|
+
const result = await applyModelMigrationBatch(input, plan.changes);
|
|
929
932
|
return {
|
|
930
933
|
...plan,
|
|
931
934
|
applied: true,
|
|
@@ -1498,6 +1501,214 @@ async function planV3ToV4Migration(loaded, options = {}) {
|
|
|
1498
1501
|
};
|
|
1499
1502
|
}
|
|
1500
1503
|
|
|
1504
|
+
async function planV4ToV5Migration(loaded, options = {}) {
|
|
1505
|
+
if (!loaded.workspace?.id) throw new Error("Model migration requires a valid Workspace record.");
|
|
1506
|
+
const targetModel = loadModel("5");
|
|
1507
|
+
const revisions = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
1508
|
+
const automatic = [];
|
|
1509
|
+
const reviewRequired = [];
|
|
1510
|
+
const unsupported = [];
|
|
1511
|
+
const missing = [];
|
|
1512
|
+
const manualActions = [];
|
|
1513
|
+
const updates = [];
|
|
1514
|
+
const documentScopeDecisions = options.documentScopes && typeof options.documentScopes === "object"
|
|
1515
|
+
? options.documentScopes
|
|
1516
|
+
: {};
|
|
1517
|
+
const auditDocumentFields = [
|
|
1518
|
+
"engagementTermsDocumentId",
|
|
1519
|
+
...(targetModel.auditReadiness?.managementDocuments || []).map(({ field }) => field)
|
|
1520
|
+
];
|
|
1521
|
+
const auditsByDocumentId = new Map();
|
|
1522
|
+
const allAuditsByDocumentId = new Map();
|
|
1523
|
+
const addAuditReference = (map, documentId, audit) => {
|
|
1524
|
+
if (!documentId) return;
|
|
1525
|
+
if (!map.has(documentId)) map.set(documentId, new Map());
|
|
1526
|
+
map.get(documentId).set(audit.id, audit);
|
|
1527
|
+
};
|
|
1528
|
+
for (const audit of loaded.resources.filter(({ type }) => type === "audit")) {
|
|
1529
|
+
for (const field of auditDocumentFields) {
|
|
1530
|
+
const documentId = audit[field];
|
|
1531
|
+
if (!documentId) continue;
|
|
1532
|
+
addAuditReference(auditsByDocumentId, documentId, audit);
|
|
1533
|
+
addAuditReference(allAuditsByDocumentId, documentId, audit);
|
|
1534
|
+
}
|
|
1535
|
+
for (const documentId of audit.supplementalDocumentIds || []) {
|
|
1536
|
+
addAuditReference(allAuditsByDocumentId, documentId, audit);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const programDocumentIds = new Set([
|
|
1540
|
+
...loaded.resources.filter(({ type }) => type === "policy").flatMap(({ relatedDocumentIds }) => relatedDocumentIds || []),
|
|
1541
|
+
...loaded.resources.filter(({ type }) => type === "obligation").flatMap((record) => [
|
|
1542
|
+
...(record.scopeResourceIds || []),
|
|
1543
|
+
...(record.templateResourceId ? [record.templateResourceId] : [])
|
|
1544
|
+
])
|
|
1545
|
+
]);
|
|
1546
|
+
const auditDocumentKinds = new Set([
|
|
1547
|
+
...(targetModel.auditReadiness?.managementDocuments || []).map(({ kind }) => kind),
|
|
1548
|
+
"soc2-engagement-terms"
|
|
1549
|
+
]);
|
|
1550
|
+
const resetDocumentIds = [];
|
|
1551
|
+
const legacyDocumentIds = [];
|
|
1552
|
+
const documentIds = new Set(loaded.resources.filter(({ type }) => type === "document").map(({ id }) => id));
|
|
1553
|
+
|
|
1554
|
+
for (const documentId of Object.keys(documentScopeDecisions)) {
|
|
1555
|
+
if (!documentIds.has(documentId)) {
|
|
1556
|
+
unsupported.push(classifiedChange(
|
|
1557
|
+
"unsupported",
|
|
1558
|
+
documentId,
|
|
1559
|
+
"workflowScope",
|
|
1560
|
+
`Document scope decision "${documentId}" does not match a Document in this workspace.`
|
|
1561
|
+
));
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
for (const original of loaded.resources) {
|
|
1566
|
+
if (original.type === "workspace") {
|
|
1567
|
+
updates.push({ ...original, dataModelVersion: "5" });
|
|
1568
|
+
automatic.push(classifiedChange(
|
|
1569
|
+
"automatic",
|
|
1570
|
+
original.id,
|
|
1571
|
+
"dataModelVersion",
|
|
1572
|
+
"Select model v5 and enable the separate governed Document approval and activation lifecycle."
|
|
1573
|
+
));
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
if (original.type !== "document") continue;
|
|
1577
|
+
const governedAuditReferences = [...(auditsByDocumentId.get(original.id)?.values() || [])];
|
|
1578
|
+
const auditReferences = [...(allAuditsByDocumentId.get(original.id)?.values() || [])];
|
|
1579
|
+
const explicitScope = normalizeDocumentScopeDecision(documentScopeDecisions[original.id]);
|
|
1580
|
+
if (Object.hasOwn(documentScopeDecisions, original.id) && !explicitScope) {
|
|
1581
|
+
unsupported.push(classifiedChange(
|
|
1582
|
+
"unsupported",
|
|
1583
|
+
original.id,
|
|
1584
|
+
"workflowScope",
|
|
1585
|
+
`Document scope decision for "${original.id}" must be "program" or "engagement".`
|
|
1586
|
+
));
|
|
1587
|
+
}
|
|
1588
|
+
const engagementSignal = governedAuditReferences.length > 0 || auditDocumentKinds.has(original.documentKind);
|
|
1589
|
+
const programSignal = programDocumentIds.has(original.id);
|
|
1590
|
+
if (!explicitScope && engagementSignal && programSignal) {
|
|
1591
|
+
unsupported.push(classifiedChange(
|
|
1592
|
+
"unsupported",
|
|
1593
|
+
original.id,
|
|
1594
|
+
"workflowScope",
|
|
1595
|
+
`Document "${original.title}" is linked to both program governance and an Audit. Choose program or engagement in documentScopes.${original.id}.`
|
|
1596
|
+
));
|
|
1597
|
+
}
|
|
1598
|
+
const workflowScope = explicitScope || (engagementSignal && !programSignal ? "engagement" : "program");
|
|
1599
|
+
const record = { ...original, workflowScope };
|
|
1600
|
+
if (workflowScope === "engagement") delete record.programRole;
|
|
1601
|
+
automatic.push(classifiedChange(
|
|
1602
|
+
"automatic",
|
|
1603
|
+
original.id,
|
|
1604
|
+
"workflowScope",
|
|
1605
|
+
explicitScope
|
|
1606
|
+
? `Use the reviewed ${workflowScope} workflow scope.`
|
|
1607
|
+
: `Classify this Document as ${workflowScope} from its model kind and authoritative relationships.`
|
|
1608
|
+
));
|
|
1609
|
+
const historicalEngagement = workflowScope === "engagement" && auditReferences.some(({ status }) => (
|
|
1610
|
+
["issued", "delivered", "complete"].includes(status)
|
|
1611
|
+
));
|
|
1612
|
+
if (["superseded", "retired"].includes(record.status)) {
|
|
1613
|
+
if (historicalEngagement) {
|
|
1614
|
+
record.activationBasis = "legacy-v4";
|
|
1615
|
+
legacyDocumentIds.push(record.id);
|
|
1616
|
+
} else delete record.activationBasis;
|
|
1617
|
+
delete record.activatedOn;
|
|
1618
|
+
delete record.activatedByIds;
|
|
1619
|
+
delete record.activatedContentRevisions;
|
|
1620
|
+
} else if (record.status === "active" && historicalEngagement) {
|
|
1621
|
+
record.activationBasis = "legacy-v4";
|
|
1622
|
+
delete record.activatedOn;
|
|
1623
|
+
delete record.activatedByIds;
|
|
1624
|
+
delete record.activatedContentRevisions;
|
|
1625
|
+
legacyDocumentIds.push(record.id);
|
|
1626
|
+
reviewRequired.push(classifiedChange(
|
|
1627
|
+
"review-required",
|
|
1628
|
+
record.id,
|
|
1629
|
+
"activationBasis",
|
|
1630
|
+
"Preserve this historical engagement Document as active with a visible legacy-v4 basis. Model v4 recorded one combined approval and activation state, so the migration does not invent an activation date, actor, or second revision."
|
|
1631
|
+
));
|
|
1632
|
+
} else if (record.status === "active") {
|
|
1633
|
+
record.status = "approved";
|
|
1634
|
+
if (record.effectiveOn) record.proposedEffectiveOn = record.effectiveOn;
|
|
1635
|
+
delete record.effectiveOn;
|
|
1636
|
+
delete record.activationBasis;
|
|
1637
|
+
delete record.activatedOn;
|
|
1638
|
+
delete record.activatedByIds;
|
|
1639
|
+
delete record.activatedContentRevisions;
|
|
1640
|
+
resetDocumentIds.push(record.id);
|
|
1641
|
+
reviewRequired.push(classifiedChange(
|
|
1642
|
+
"review-required",
|
|
1643
|
+
record.id,
|
|
1644
|
+
"status",
|
|
1645
|
+
workflowScope === "engagement"
|
|
1646
|
+
? "Model v4 recorded one combined approval and activation state. Preserve the approval and approved revision, then record a separate Step 5 activation after confirming the current engagement facts."
|
|
1647
|
+
: "Model v4 recorded one combined approval and activation state. Preserve the approval and approved revision, confirm the linked requirements, then record a separate Step 3 activation."
|
|
1648
|
+
));
|
|
1649
|
+
}
|
|
1650
|
+
updates.push(record);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const updatedById = new Map(updates.map((record) => [record.id, record]));
|
|
1654
|
+
const migratedRecords = loaded.resources.map((record) => updatedById.get(record.id) || record);
|
|
1655
|
+
await collectTargetValidationActions(loaded, migratedRecords, targetModel, missing, manualActions);
|
|
1656
|
+
for (const item of [...missing, ...manualActions]) {
|
|
1657
|
+
unsupported.push(classifiedChange(
|
|
1658
|
+
"unsupported",
|
|
1659
|
+
item.resourceId,
|
|
1660
|
+
item.field,
|
|
1661
|
+
item.message || `Resolve ${item.field} before applying the model v5 migration.`
|
|
1662
|
+
));
|
|
1663
|
+
}
|
|
1664
|
+
const ready = unsupported.length === 0;
|
|
1665
|
+
return {
|
|
1666
|
+
schemaVersion: 2,
|
|
1667
|
+
sourceModelVersion: "4",
|
|
1668
|
+
targetModelVersion: "5",
|
|
1669
|
+
ready,
|
|
1670
|
+
missing,
|
|
1671
|
+
conflicts: [],
|
|
1672
|
+
manualActions,
|
|
1673
|
+
classifications: { automatic, reviewRequired, unsupported },
|
|
1674
|
+
notes: reviewRequired,
|
|
1675
|
+
migrationReport: {
|
|
1676
|
+
resetDocumentIds,
|
|
1677
|
+
legacyDocumentIds
|
|
1678
|
+
},
|
|
1679
|
+
summary: {
|
|
1680
|
+
create: 0,
|
|
1681
|
+
update: updates.length,
|
|
1682
|
+
automatic: automatic.length,
|
|
1683
|
+
reviewRequired: reviewRequired.length,
|
|
1684
|
+
unsupported: unsupported.length
|
|
1685
|
+
},
|
|
1686
|
+
fileDiff: {
|
|
1687
|
+
create: [],
|
|
1688
|
+
update: updates.map((record) => ({
|
|
1689
|
+
type: record.type,
|
|
1690
|
+
id: record.id,
|
|
1691
|
+
before: loaded.resources.find(({ id }) => id === record.id),
|
|
1692
|
+
after: record
|
|
1693
|
+
}))
|
|
1694
|
+
},
|
|
1695
|
+
changes: {
|
|
1696
|
+
create: [],
|
|
1697
|
+
update: updates,
|
|
1698
|
+
expectedRevisions: Object.fromEntries(updates.map(({ id }) => [id, revisions.get(id)])),
|
|
1699
|
+
validateWholeWorkspace: true,
|
|
1700
|
+
targetModelVersion: "5"
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function normalizeDocumentScopeDecision(value) {
|
|
1706
|
+
const candidate = typeof value === "object" && value
|
|
1707
|
+
? value.workflowScope || value.scope
|
|
1708
|
+
: value;
|
|
1709
|
+
return ["program", "engagement"].includes(candidate) ? candidate : null;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1501
1712
|
async function collectTargetValidationActions(loaded, records, model, missing, manualActions) {
|
|
1502
1713
|
const { validateWorkspace } = await import("./validate.js");
|
|
1503
1714
|
const existingById = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
@@ -2003,7 +2214,7 @@ function migrateInverseArrays(resources, byId, editable, conflicts, mapping) {
|
|
|
2003
2214
|
}
|
|
2004
2215
|
|
|
2005
2216
|
function emptyPlan(version, targetVersion = V1_TARGET_MODEL_VERSION) {
|
|
2006
|
-
const classifiedPlan =
|
|
2217
|
+
const classifiedPlan = Number(targetVersion) >= 3;
|
|
2007
2218
|
const includesPathMoves = String(targetVersion) === "4";
|
|
2008
2219
|
return {
|
|
2009
2220
|
schemaVersion: classifiedPlan ? 2 : 1,
|
package/src/obligations.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { scaffoldResourceMutation } from "./agent.js";
|
|
3
3
|
import { createResourceId } from "./id.js";
|
|
4
4
|
import { createResourceAndLink, createResources, updateResource } from "./files.js";
|
|
5
|
-
import { loadModel } from "../model/index.js";
|
|
5
|
+
import { loadModel, modelSupports } from "../model/index.js";
|
|
6
6
|
import { coverageEnd } from "./coverage.js";
|
|
7
7
|
import {
|
|
8
8
|
addCalendarDays,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./recurrence.js";
|
|
15
15
|
import { currentCalendarDate, isRfc3339Timestamp } from "./time.js";
|
|
16
16
|
import { loadWorkspace } from "./workspace.js";
|
|
17
|
-
import { obligationProgramStatus } from "./program-lifecycle.js";
|
|
17
|
+
import { obligationGovernedDocuments, obligationProgramStatus } from "./program-lifecycle.js";
|
|
18
18
|
import { resolveProgram } from "./program.js";
|
|
19
19
|
|
|
20
20
|
const COMPLETION_DATE_FIELDS = [
|
|
@@ -75,7 +75,7 @@ export function planObligations(resources, options = {}) {
|
|
|
75
75
|
for (const obligation of obligations) {
|
|
76
76
|
const activity = obligationActivity(model, obligation.activityType);
|
|
77
77
|
const expectedCompletionTypes = activity.completionResourceTypes;
|
|
78
|
-
const programStatus = obligationProgramStatus(obligation, byId, asOf);
|
|
78
|
+
const programStatus = obligationProgramStatus(obligation, byId, asOf, model);
|
|
79
79
|
if (obligation.recurrence?.mode === "event" && obligation.recurrence.eventType) {
|
|
80
80
|
const eventType = obligation.recurrence.eventType;
|
|
81
81
|
const group = triggerGroups.get(eventType) ?? {
|
|
@@ -111,7 +111,7 @@ export function planObligations(resources, options = {}) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const configuredAnchor = obligation.recurrence?.anchorDate || obligation.startsOn;
|
|
114
|
-
const activationDate = obligationActivationDate(obligation, byId);
|
|
114
|
+
const activationDate = obligationActivationDate(obligation, byId, model);
|
|
115
115
|
const recurrence = {
|
|
116
116
|
...(obligation.recurrence || {}),
|
|
117
117
|
anchorDate: configuredAnchor && activationDate
|
|
@@ -258,8 +258,8 @@ export async function createObligationEvent(input, options) {
|
|
|
258
258
|
)
|
|
259
259
|
));
|
|
260
260
|
if (!eventType || templates.length === 0) throw new Error(`No active obligations use event type "${eventType}".`);
|
|
261
|
-
if (templates.some((record) => obligationProgramStatus(record, byId, occurredOn) === "proposed")) {
|
|
262
|
-
throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing
|
|
261
|
+
if (templates.some((record) => obligationProgramStatus(record, byId, occurredOn, loaded.model) === "proposed")) {
|
|
262
|
+
throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing Policy and required governed Document active and effective, then implement at least one linked Control before starting this workflow.`);
|
|
263
263
|
}
|
|
264
264
|
if (templates.some((record) => normalizedEventWindow(record.window).precision === "timestamp") && !occurredAt) {
|
|
265
265
|
throw new Error(`Event type "${eventType}" has hour-based deadlines and requires an RFC 3339 occurredAt timestamp.`);
|
|
@@ -720,7 +720,7 @@ function completionTeam(resources, ownerIds) {
|
|
|
720
720
|
}
|
|
721
721
|
|
|
722
722
|
function defaultClassificationId(loaded) {
|
|
723
|
-
if (
|
|
723
|
+
if (modelSupports(loaded.model, "program-scope")) {
|
|
724
724
|
return loaded.resources.find(({ type, id, status }) => type === "classification" && id === "internal" && status === "active")?.id
|
|
725
725
|
|| loaded.resources.find(({ type, status }) => type === "classification" && status === "active")?.id
|
|
726
726
|
|| "";
|
|
@@ -792,7 +792,7 @@ function planEventRun(event, actionItems, byId, asOf, now, model) {
|
|
|
792
792
|
const completionProfile = obligation?.type === "obligation"
|
|
793
793
|
? obligationActivity(model, obligation.activityType).completionProfile || null
|
|
794
794
|
: null;
|
|
795
|
-
const completionIds =
|
|
795
|
+
const completionIds = modelSupports(model, "guided-workflow")
|
|
796
796
|
? record.completionResourceIds || []
|
|
797
797
|
: [...(record.completionResourceIds || []), ...(record.evidenceIds || [])];
|
|
798
798
|
const linkedCompletionIds = [...new Set(completionIds)];
|
|
@@ -991,13 +991,16 @@ function occurrenceStatus(window, asOf, complete) {
|
|
|
991
991
|
return "upcoming";
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
-
function obligationActivationDate(obligation, byId) {
|
|
995
|
-
const
|
|
994
|
+
function obligationActivationDate(obligation, byId, model) {
|
|
995
|
+
const policyDates = (obligation.policyIds || [])
|
|
996
996
|
.map((id) => byId.get(id))
|
|
997
997
|
.filter((policy) => policy?.type === "policy")
|
|
998
998
|
.map((policy) => policy.effectiveOn)
|
|
999
999
|
.filter(Boolean);
|
|
1000
|
-
|
|
1000
|
+
const documentDates = obligationGovernedDocuments(obligation, byId, model)
|
|
1001
|
+
.map((document) => document.effectiveOn)
|
|
1002
|
+
.filter(Boolean);
|
|
1003
|
+
return [...policyDates, ...documentDates].sort().at(-1) || null;
|
|
1001
1004
|
}
|
|
1002
1005
|
|
|
1003
1006
|
function relativeTiming(window, asOf) {
|