filegrc 0.8.0 → 0.9.1
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 +7 -7
- package/model/index.js +6 -5
- package/model/v6.json +10358 -0
- package/package.json +1 -1
- package/src/cli.js +75 -6
- package/src/document-activation.js +61 -25
- package/src/files.js +54 -25
- package/src/index.js +8 -1
- package/src/model-migration.js +148 -3
- package/src/obligations.js +5 -5
- package/src/policy-library/information-security-policy-v2.md +2 -2
- package/src/policy-library.js +42 -10
- package/src/program-lifecycle.js +41 -3
- package/src/program-path.js +19 -18
- package/src/program-readiness.js +131 -16
- package/src/server.js +8 -1
- package/src/setup.js +1 -1
- package/src/validate.js +33 -5
- package/src/web.js +128 -26
- package/src/workflow.js +21 -4
package/src/program-readiness.js
CHANGED
|
@@ -64,7 +64,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
64
64
|
loaded.model
|
|
65
65
|
);
|
|
66
66
|
controlStage.items.push(...policyActivations.map(policyActivationItem));
|
|
67
|
-
controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled
|
|
67
|
+
controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled Obligations, and complete authoritative source ${modelSupports(loaded.model, "component-sources") ? "Components" : "Systems"}. Activate unchanged approved program Documents and Training after their requirements are implemented, then activate approved Policies at the implementation cutover.`;
|
|
68
68
|
const evidenceGateStages = [
|
|
69
69
|
scopeStage(
|
|
70
70
|
program,
|
|
@@ -120,6 +120,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
120
120
|
suggestedCandidatePeriodStart: canStartCandidatePeriod ? asOf : null,
|
|
121
121
|
policyActivations,
|
|
122
122
|
documentActivations: governedContent.documentActivations,
|
|
123
|
+
trainingActivations: governedContent.trainingActivations,
|
|
123
124
|
policyLibraryProposals: [
|
|
124
125
|
...policyLibrary.proposals,
|
|
125
126
|
...legacyPolicyLibraryProposals(records)
|
|
@@ -538,7 +539,10 @@ async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
|
538
539
|
const documents = modelSupports(model, "governed-document-activation")
|
|
539
540
|
? requiredGovernedDocuments(scope, records, byId, model)
|
|
540
541
|
: [];
|
|
541
|
-
const
|
|
542
|
+
const trainings = modelSupports(model, "governed-training-activation")
|
|
543
|
+
? records.filter(({ type, status }) => type === "training" && !["superseded", "retired"].includes(status))
|
|
544
|
+
: [];
|
|
545
|
+
const governedRecords = [...policies, ...documents, ...trainings];
|
|
542
546
|
const appointedReviewer = governedRecords
|
|
543
547
|
.filter((record) => partiesIndependent(record.ownerIds, record.approverIds, byId))
|
|
544
548
|
.flatMap((record) => [...currentPartyPeople(record.approverIds || [], byId)])
|
|
@@ -628,6 +632,8 @@ async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
|
628
632
|
const source = await readMarkdown(document);
|
|
629
633
|
const placeholderCount = openPlaceholderCount(source);
|
|
630
634
|
const isSecurityIncidentRecoveryPlan = document.id === "document-security-incident-recovery-plan";
|
|
635
|
+
const systemContinuityObjectivesRequired = isSecurityIncidentRecoveryPlan
|
|
636
|
+
&& scope.requirements.some(isAvailabilityRequirement);
|
|
631
637
|
const systemsWithCompleteContinuityObjectives = scope.systems.filter(({ continuityObjectives }) => (
|
|
632
638
|
Number.isInteger(continuityObjectives?.recoveryTimeHours)
|
|
633
639
|
&& Number.isInteger(continuityObjectives?.recoveryPointHours)
|
|
@@ -642,7 +648,10 @@ async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
|
642
648
|
linkedControls: (document.controlIds || []).some((id) => scope.controls.some((control) => control.id === id)),
|
|
643
649
|
contentComplete: substantiveMarkdown(source) && placeholderCount === 0,
|
|
644
650
|
...(isSecurityIncidentRecoveryPlan
|
|
645
|
-
? {
|
|
651
|
+
? {
|
|
652
|
+
systemContinuityObjectives: !systemContinuityObjectivesRequired
|
|
653
|
+
|| systemsWithCompleteContinuityObjectives.length === scope.systems.length && scope.systems.length > 0
|
|
654
|
+
}
|
|
646
655
|
: {})
|
|
647
656
|
};
|
|
648
657
|
const missing = Object.entries(checks)
|
|
@@ -661,10 +670,13 @@ async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
|
661
670
|
placeholderCount,
|
|
662
671
|
...(isSecurityIncidentRecoveryPlan
|
|
663
672
|
? {
|
|
673
|
+
systemContinuityObjectivesRequired,
|
|
664
674
|
continuityObjectiveSystemIds: systemsWithCompleteContinuityObjectives.map(({ id }) => id),
|
|
665
|
-
missingContinuityObjectiveSystemIds:
|
|
666
|
-
|
|
667
|
-
|
|
675
|
+
missingContinuityObjectiveSystemIds: systemContinuityObjectivesRequired
|
|
676
|
+
? scope.systems
|
|
677
|
+
.filter(({ id }) => !systemsWithCompleteContinuityObjectives.some((system) => system.id === id))
|
|
678
|
+
.map(({ id }) => id)
|
|
679
|
+
: []
|
|
668
680
|
}
|
|
669
681
|
: {}),
|
|
670
682
|
commands: [
|
|
@@ -675,10 +687,46 @@ async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
|
675
687
|
}
|
|
676
688
|
));
|
|
677
689
|
}
|
|
690
|
+
if (modelSupports(model, "governed-training-activation")) {
|
|
691
|
+
for (const training of trainings) {
|
|
692
|
+
const source = await readMarkdown(training);
|
|
693
|
+
const placeholderCount = openPlaceholderCount(source);
|
|
694
|
+
const checks = {
|
|
695
|
+
independentlyApproved: ["approved", "active"].includes(training.status)
|
|
696
|
+
&& Boolean(training.approvedOn)
|
|
697
|
+
&& Boolean(training.approvedContentRevisions)
|
|
698
|
+
&& partiesIndependent(training.ownerIds, training.approverIds, byId),
|
|
699
|
+
owner: currentPartyPeople(training.ownerIds, byId).size > 0,
|
|
700
|
+
linkedControls: (training.controlIds || []).some((id) => scope.controls.some((control) => control.id === id)),
|
|
701
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
702
|
+
};
|
|
703
|
+
const missing = Object.entries(checks)
|
|
704
|
+
.filter(([, value]) => !value)
|
|
705
|
+
.map(([name]) => governedApprovalCheckLabel(name));
|
|
706
|
+
items.push(item(
|
|
707
|
+
`training-approval-${training.id}`,
|
|
708
|
+
missing.length ? "action" : "complete",
|
|
709
|
+
training.title,
|
|
710
|
+
missing.length
|
|
711
|
+
? `Remaining Step 2 approval work: ${missing.join(", ")}${placeholderCount ? ` (${placeholderCount} open placeholders)` : ""}. Review and approve the exact Training content before implementation.`
|
|
712
|
+
: `Independently approved on ${training.approvedOn} and bound to the exact Training revision. Activation remains in Step 3.`,
|
|
713
|
+
training,
|
|
714
|
+
{
|
|
715
|
+
checks,
|
|
716
|
+
placeholderCount,
|
|
717
|
+
commands: [
|
|
718
|
+
`npx filegrc get ${shellArgument(training.id)} --mutation`,
|
|
719
|
+
`npx filegrc update training ${shellArgument(training.id)} MUTATION.json --json`,
|
|
720
|
+
"npx filegrc program-readiness --json"
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
678
726
|
return stage(
|
|
679
727
|
"policies",
|
|
680
|
-
"Approve Policies
|
|
681
|
-
"Approve Policy
|
|
728
|
+
"Approve Policies",
|
|
729
|
+
"Approve the exact Policy, program Document, and Training content that defines what the organization intends to require. Approval does not prove implementation or activate the content.",
|
|
682
730
|
items
|
|
683
731
|
);
|
|
684
732
|
}
|
|
@@ -727,10 +775,18 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf, mo
|
|
|
727
775
|
const documents = requiredGovernedDocuments(scope, records, byId, model);
|
|
728
776
|
const governedRecords = [
|
|
729
777
|
...documents,
|
|
730
|
-
...records.filter((record) =>
|
|
778
|
+
...records.filter((record) => (
|
|
779
|
+
record.type === "training"
|
|
780
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
781
|
+
&& (
|
|
782
|
+
requiredGovernedIds.has(record.id)
|
|
783
|
+
|| (record.controlIds || []).some((id) => scope.controls.some((control) => control.id === id))
|
|
784
|
+
)
|
|
785
|
+
))
|
|
731
786
|
];
|
|
732
787
|
const items = [];
|
|
733
788
|
const documentActivations = [];
|
|
789
|
+
const trainingActivations = [];
|
|
734
790
|
for (const record of governedRecords) {
|
|
735
791
|
const source = await readMarkdown(record);
|
|
736
792
|
const placeholderCount = openPlaceholderCount(source);
|
|
@@ -766,7 +822,30 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf, mo
|
|
|
766
822
|
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
767
823
|
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
768
824
|
}
|
|
769
|
-
: {
|
|
825
|
+
: modelSupports(model, "governed-training-activation") ? {
|
|
826
|
+
approvalBound: ["approved", "active"].includes(record.status)
|
|
827
|
+
&& Boolean(record.approvedOn)
|
|
828
|
+
&& Boolean(record.approvedContentRevisions)
|
|
829
|
+
&& partiesIndependent(record.ownerIds, record.approverIds, byId),
|
|
830
|
+
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
831
|
+
active: record.status === "active",
|
|
832
|
+
requirementsImplemented: linkedControlIds.length > 0 && missingImplementationControlIds.length === 0,
|
|
833
|
+
assignmentScheduled: enabledObligations.some((obligation) => (
|
|
834
|
+
obligation.templateResourceId === record.id
|
|
835
|
+
|| (obligation.scopeResourceIds || []).includes(record.id)
|
|
836
|
+
)),
|
|
837
|
+
activationRecorded: ["recorded", "legacy-v5"].includes(record.activationBasis),
|
|
838
|
+
activated: record.activationBasis === "legacy-v5" || Boolean(record.activatedOn),
|
|
839
|
+
activator: record.activationBasis === "legacy-v5" || Boolean((record.activatedByIds || []).length)
|
|
840
|
+
&& record.activatedByIds.every((id) => personWasActiveOn(byId.get(id), record.activatedOn)),
|
|
841
|
+
activatedContent: record.activationBasis === "legacy-v5" || Boolean(record.activatedContentRevisions),
|
|
842
|
+
activationMatchesApproval: record.activationBasis === "legacy-v5" || contentRevisionBindingsMatch(
|
|
843
|
+
record.approvedContentRevisions,
|
|
844
|
+
record.activatedContentRevisions
|
|
845
|
+
),
|
|
846
|
+
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
847
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
848
|
+
} : {
|
|
770
849
|
active: record.status === "active",
|
|
771
850
|
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
772
851
|
approved: Boolean(record.approvedOn && (record.approvedByIds || []).length),
|
|
@@ -810,6 +889,37 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf, mo
|
|
|
810
889
|
gapCount: record.status === "active" ? missing.length : preActivationGapCount
|
|
811
890
|
});
|
|
812
891
|
}
|
|
892
|
+
if (record.type === "training" && modelSupports(model, "governed-training-activation")) {
|
|
893
|
+
const activationComplete = Object.values(checks).every(Boolean);
|
|
894
|
+
const preActivationCheckNames = ["approvalBound", "owner", "requirementsImplemented", "assignmentScheduled", "contentComplete"];
|
|
895
|
+
const preActivationGapCount = preActivationCheckNames.filter((name) => !checks[name]).length;
|
|
896
|
+
const readyToActivate = record.status === "approved"
|
|
897
|
+
&& preActivationCheckNames.every((name) => checks[name]);
|
|
898
|
+
const state = record.status === "active" && activationComplete
|
|
899
|
+
? "active-and-operating"
|
|
900
|
+
: record.status === "active"
|
|
901
|
+
? "active-with-gaps"
|
|
902
|
+
: readyToActivate
|
|
903
|
+
? "ready-to-activate"
|
|
904
|
+
: record.status === "approved"
|
|
905
|
+
? "approved-implementation-pending"
|
|
906
|
+
: "approval-pending";
|
|
907
|
+
trainingActivations.push({
|
|
908
|
+
trainingId: record.id,
|
|
909
|
+
title: record.title,
|
|
910
|
+
state,
|
|
911
|
+
label: documentActivationLabel(state),
|
|
912
|
+
approvedOn: record.approvedOn || null,
|
|
913
|
+
activatedOn: record.activatedOn || null,
|
|
914
|
+
effectiveOn: record.effectiveOn || null,
|
|
915
|
+
linkedControlIds,
|
|
916
|
+
missingImplementationControlIds,
|
|
917
|
+
assignmentScheduled: checks.assignmentScheduled,
|
|
918
|
+
activationRevisionBound: Boolean(record.activatedContentRevisions),
|
|
919
|
+
activatedByIds: record.activatedByIds || [],
|
|
920
|
+
gapCount: record.status === "active" ? missing.length : preActivationGapCount
|
|
921
|
+
});
|
|
922
|
+
}
|
|
813
923
|
items.push(item(
|
|
814
924
|
`${record.type}-${record.id}`,
|
|
815
925
|
missing.length ? "action" : "complete",
|
|
@@ -825,7 +935,7 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf, mo
|
|
|
825
935
|
{
|
|
826
936
|
checks,
|
|
827
937
|
placeholderCount,
|
|
828
|
-
...(record.type
|
|
938
|
+
...(["document", "training"].includes(record.type) ? { linkedControlIds, missingImplementationControlIds } : {}),
|
|
829
939
|
commands: [
|
|
830
940
|
`npx filegrc get ${shellArgument(record.id)} --mutation`,
|
|
831
941
|
`npx filegrc update ${record.type} ${shellArgument(record.id)} MUTATION.json --json`,
|
|
@@ -834,7 +944,7 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf, mo
|
|
|
834
944
|
}
|
|
835
945
|
));
|
|
836
946
|
}
|
|
837
|
-
return { items, documentActivations };
|
|
947
|
+
return { items, documentActivations, trainingActivations };
|
|
838
948
|
}
|
|
839
949
|
|
|
840
950
|
function documentActivationLabel(state) {
|
|
@@ -986,12 +1096,12 @@ function policyActivationItem(assessment) {
|
|
|
986
1096
|
[assessment.plannedOrPartialControlIds.length, "planned or partial Controls"],
|
|
987
1097
|
[assessment.missingComponentControlIds.length, "Controls missing active Components"],
|
|
988
1098
|
[assessment.missingEvidenceSourceControlIds.length, "Controls missing ready evidence sources"],
|
|
989
|
-
[assessment.missingScheduleControlIds.length, "Controls missing enabled
|
|
1099
|
+
[assessment.missingScheduleControlIds.length, "Controls missing enabled Obligations"],
|
|
990
1100
|
[assessment.missingGovernedDocumentIds?.length || 0, "required governed Documents not active"],
|
|
991
1101
|
[assessment.unresolvedExceptionIds.length, "unresolved Exceptions"]
|
|
992
1102
|
].filter(([count]) => count).map(([count, label]) => `${count} ${label}`);
|
|
993
1103
|
const message = assessment.state === "active-and-operating"
|
|
994
|
-
? `Active and effective ${assessment.effectiveOn}; all ${assessment.linkedControlIds.length} linked Controls are implemented with Components, evidence sources, and enabled
|
|
1104
|
+
? `Active and effective ${assessment.effectiveOn}; all ${assessment.linkedControlIds.length} linked Controls are implemented with Components, evidence sources, and enabled Obligations.`
|
|
995
1105
|
: assessment.state === "ready-to-activate"
|
|
996
1106
|
? "Implementation checks are complete. Include this approved Policy in the Step 3 cutover when you are ready for it to take effect."
|
|
997
1107
|
: `${assessment.label}: ${counts.join(", ") || assessment.timingWarnings.join(" ")}. ${assessment.activationWarning || ""}`.trim();
|
|
@@ -1104,7 +1214,7 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1104
1214
|
}
|
|
1105
1215
|
));
|
|
1106
1216
|
}
|
|
1107
|
-
return stage("controls", "Implement Controls", "Each implemented Control needs an owner, actual procedure, scope, operation pattern, evidence source, mappings, an implementation date, and any required
|
|
1217
|
+
return stage("controls", "Implement Controls", "Each implemented Control needs an owner, actual procedure, scope, operation pattern, evidence source, mappings, an implementation date, and any required Obligations enabled. Scheduled work stays dormant until its governing Policy, program Documents, and Training are active and effective.", items);
|
|
1108
1218
|
}
|
|
1109
1219
|
|
|
1110
1220
|
async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
@@ -1438,6 +1548,7 @@ function governedContentCheckLabel(name) {
|
|
|
1438
1548
|
effective: "effective date",
|
|
1439
1549
|
effectiveContent: "effective content revision",
|
|
1440
1550
|
requirementsImplemented: "implemented linked requirements",
|
|
1551
|
+
assignmentScheduled: "enabled Training assignment schedule",
|
|
1441
1552
|
activationRecorded: "recorded activation basis",
|
|
1442
1553
|
activated: "separate activation date",
|
|
1443
1554
|
activator: "named activation Person",
|
|
@@ -1463,7 +1574,7 @@ function controlCheckLabel(name) {
|
|
|
1463
1574
|
implementationReview: "independent implementation review",
|
|
1464
1575
|
policyMapping: "policy mapping",
|
|
1465
1576
|
criteriaMapping: "criteria mapping",
|
|
1466
|
-
workQueue: "running
|
|
1577
|
+
workQueue: "running Obligation schedules"
|
|
1467
1578
|
})[name] || name;
|
|
1468
1579
|
}
|
|
1469
1580
|
|
|
@@ -1479,6 +1590,10 @@ function isDescriptionRequirement(requirement) {
|
|
|
1479
1590
|
|| /^DC\d+/i.test(requirement?.reference || "");
|
|
1480
1591
|
}
|
|
1481
1592
|
|
|
1593
|
+
function isAvailabilityRequirement(requirement) {
|
|
1594
|
+
return /^A1\./i.test(requirement?.reference || "");
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1482
1597
|
function isSecurityRequirement(requirement) {
|
|
1483
1598
|
const tags = requirement?.tags || [];
|
|
1484
1599
|
return tags.includes("security") || tags.includes("common-criteria") || /^CC\d+(?:\.|$)/i.test(requirement?.reference || "");
|
package/src/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { extname, join, resolve } from "node:path";
|
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
6
6
|
import { getResourceDefinition } from "../model/index.js";
|
|
7
7
|
import { prepareAuditWorkspace } from "./audit-preparation.js";
|
|
8
|
-
import { activateDocuments } from "./document-activation.js";
|
|
8
|
+
import { activateDocuments, activateGovernedContent } from "./document-activation.js";
|
|
9
9
|
import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
|
|
10
10
|
import { applyApplicabilityReviewWithContext, planApplicabilityReview } from "./batch-review.js";
|
|
11
11
|
import { applyCollectionReview, planCollectionReview } from "./collection-review.js";
|
|
@@ -338,6 +338,13 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
338
338
|
}, () => activateDocuments(input, { ...payload, confirmed: true }));
|
|
339
339
|
return json(response, 200, result);
|
|
340
340
|
}
|
|
341
|
+
if (request.method === "POST" && url.pathname === "/api/governed-content-activations") {
|
|
342
|
+
const payload = await readJson(request);
|
|
343
|
+
const result = await browserMutation(input, options, {
|
|
344
|
+
message: (activation) => `Activate ${activation.resourceIds.length} governed-content ${activation.resourceIds.length === 1 ? "record" : "records"}`
|
|
345
|
+
}, () => activateGovernedContent(input, { ...payload, confirmed: true }));
|
|
346
|
+
return json(response, 200, result);
|
|
347
|
+
}
|
|
341
348
|
if (request.method === "POST" && url.pathname === "/api/resources") {
|
|
342
349
|
const payload = normalizeResourceMutation(await readJson(request));
|
|
343
350
|
const { record } = payload;
|
package/src/setup.js
CHANGED
|
@@ -81,7 +81,7 @@ export function summarizeSetupResult(result) {
|
|
|
81
81
|
},
|
|
82
82
|
system: setupSystemSummary(result.system),
|
|
83
83
|
target: setupTargetSummary(result.program || result.workspace, {
|
|
84
|
-
modelVersion: result.workspace?.dataModelVersion || (result.program ? "
|
|
84
|
+
modelVersion: result.workspace?.dataModelVersion || (result.program ? "6" : "3")
|
|
85
85
|
}),
|
|
86
86
|
renderer: result.renderer ? setupRendererSummary(result.renderer) : null,
|
|
87
87
|
commitment: result.commitment || null,
|
package/src/validate.js
CHANGED
|
@@ -273,6 +273,20 @@ function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagno
|
|
|
273
273
|
));
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
+
for (const training of resources.filter(({ type, activationBasis }) => (
|
|
277
|
+
type === "training" && activationBasis === "recorded"
|
|
278
|
+
))) {
|
|
279
|
+
const invalidActorIds = (training.activatedByIds || []).filter((id) => (
|
|
280
|
+
!personWasActiveOn(byId.get(id), training.activatedOn)
|
|
281
|
+
));
|
|
282
|
+
if (invalidActorIds.length) {
|
|
283
|
+
diagnostics.push(error(
|
|
284
|
+
"invalid-training-activation-actor",
|
|
285
|
+
pathById.get(training.id) || `data/${training.id}`,
|
|
286
|
+
`Training activation actors must have been active on ${training.activatedOn}: ${invalidActorIds.join(", ")}.`
|
|
287
|
+
));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
276
290
|
}
|
|
277
291
|
|
|
278
292
|
function validateCollectionReview(record, loaded, byId, path, diagnostics) {
|
|
@@ -786,7 +800,7 @@ function validateCompletionDates(record, path, diagnostics) {
|
|
|
786
800
|
]);
|
|
787
801
|
validateOrderedDates(record, path, diagnostics, ["startedAt", "endedAt"]);
|
|
788
802
|
validateOrderedDates(record, path, diagnostics, ["fieldworkStart", "fieldworkEnd", "reportDate"]);
|
|
789
|
-
if (record.type
|
|
803
|
+
if (["document", "training"].includes(record.type) && record.activatedOn) {
|
|
790
804
|
validateOrderedDates(record, path, diagnostics, ["approvedOn", "activatedOn", "effectiveOn"]);
|
|
791
805
|
}
|
|
792
806
|
if (record.acceptance) {
|
|
@@ -916,17 +930,21 @@ async function validateContentBinding(record, model, root, path, diagnostics, bi
|
|
|
916
930
|
}
|
|
917
931
|
}
|
|
918
932
|
|
|
919
|
-
function approvalBound(record) {
|
|
933
|
+
function approvalBound(record, model) {
|
|
920
934
|
if (record.type === "policy") return ["approved", "active", "superseded", "retired"].includes(record.status);
|
|
921
935
|
if (record.type === "document") return ["approved", "active", "superseded", "retired"].includes(record.status);
|
|
922
|
-
if (record.type === "training")
|
|
936
|
+
if (record.type === "training") {
|
|
937
|
+
return (modelSupports(model, "governed-training-activation")
|
|
938
|
+
? ["approved", "active", "superseded", "retired"]
|
|
939
|
+
: ["active", "retired"]).includes(record.status);
|
|
940
|
+
}
|
|
923
941
|
return false;
|
|
924
942
|
}
|
|
925
943
|
|
|
926
944
|
function contentBindingFields(record, model) {
|
|
927
945
|
const fields = [];
|
|
928
946
|
if (["policy", "document"].includes(record.type)) {
|
|
929
|
-
fields.push({ field: "approvedContentRevisions", bound: approvalBound, label: "Approved" });
|
|
947
|
+
fields.push({ field: "approvedContentRevisions", bound: (candidate) => approvalBound(candidate, model), label: "Approved" });
|
|
930
948
|
}
|
|
931
949
|
if (record.type === "document" && model.resources.document?.fields?.activatedContentRevisions) {
|
|
932
950
|
fields.push({
|
|
@@ -936,7 +954,17 @@ function contentBindingFields(record, model) {
|
|
|
936
954
|
});
|
|
937
955
|
}
|
|
938
956
|
if (record.type === "training" && model.resources.training?.fields?.effectiveContentRevisions) {
|
|
939
|
-
fields.push({ field: "effectiveContentRevisions", bound: approvalBound, label: "Approved" });
|
|
957
|
+
fields.push({ field: "effectiveContentRevisions", bound: (candidate) => approvalBound(candidate, model), label: "Approved" });
|
|
958
|
+
}
|
|
959
|
+
if (record.type === "training" && model.resources.training?.fields?.approvedContentRevisions) {
|
|
960
|
+
fields.push({ field: "approvedContentRevisions", bound: (candidate) => approvalBound(candidate, model), label: "Approved" });
|
|
961
|
+
}
|
|
962
|
+
if (record.type === "training" && model.resources.training?.fields?.activatedContentRevisions) {
|
|
963
|
+
fields.push({
|
|
964
|
+
field: "activatedContentRevisions",
|
|
965
|
+
bound: (candidate) => ["active", "superseded", "retired"].includes(candidate?.status),
|
|
966
|
+
label: "Activated"
|
|
967
|
+
});
|
|
940
968
|
}
|
|
941
969
|
return fields;
|
|
942
970
|
}
|