filegrc 0.10.0 → 0.11.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 +8 -5
- package/model/v8.json +10947 -0
- package/package.json +1 -1
- package/src/cli.js +66 -1
- package/src/collection-scope.js +12 -0
- package/src/git.js +4 -4
- package/src/index.js +3 -0
- package/src/model-migration.js +160 -4
- package/src/obligations.js +15 -9
- package/src/policy-library/data-retention-schedule-v2.md +25 -0
- package/src/policy-library.js +52 -24
- package/src/program-amendment.js +222 -0
- package/src/program-path.js +12 -4
- package/src/program-readiness.js +25 -12
- package/src/requirement-mapping.js +61 -0
- package/src/retention.js +261 -0
- package/src/server.js +74 -1
- package/src/source-coverage.js +21 -7
- package/src/state.js +169 -1
- package/src/validate.js +81 -4
- package/src/web.js +320 -27
- package/src/workflow.js +26 -7
package/src/validate.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { isSafeGitName } from "./git-name.js";
|
|
11
11
|
import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
|
|
12
12
|
import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
|
|
13
|
+
import { resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
13
14
|
import { obligationIsEnabled } from "./program-lifecycle.js";
|
|
14
15
|
import { partyPeople } from "./parties.js";
|
|
15
16
|
import { isMarkdownChoice, markdownEntries } from "./resource-markdown.js";
|
|
@@ -45,6 +46,15 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
45
46
|
]));
|
|
46
47
|
const asOf = currentCalendarDate(loaded.workspace?.timezone || "UTC");
|
|
47
48
|
const obligationsByControl = new Map();
|
|
49
|
+
const reviewRecords = loaded.resources.filter((record) => (
|
|
50
|
+
record.status === "active" && ["retention-schedule-item", "requirement-mapping"].includes(record.type)
|
|
51
|
+
));
|
|
52
|
+
const reviewDependencyIds = reviewRecords.flatMap((record) => (
|
|
53
|
+
record.type === "retention-schedule-item"
|
|
54
|
+
? retentionReviewResourceIds(record, loaded)
|
|
55
|
+
: [...(record.sourceResourceIds || []), ...(record.targetResourceIds || [])]
|
|
56
|
+
));
|
|
57
|
+
const currentReviewRevisions = await resourceReviewRevisions(loaded, reviewDependencyIds);
|
|
48
58
|
for (const obligation of loaded.resources.filter((record) => record.type === "obligation" && record.status !== "retired")) {
|
|
49
59
|
for (const controlId of obligation.controlIds || []) {
|
|
50
60
|
if (!obligationsByControl.has(controlId)) obligationsByControl.set(controlId, []);
|
|
@@ -88,6 +98,8 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
88
98
|
validateCollectionReview(record, loaded, byId, displayPath, diagnostics);
|
|
89
99
|
}
|
|
90
100
|
if (record.type === "obligation") validateObligation(record, loaded.model, byId, displayPath, diagnostics);
|
|
101
|
+
if (record.type === "retention-schedule-item") validateRetentionScheduleItem(record, loaded, byId, currentReviewRevisions, displayPath, diagnostics);
|
|
102
|
+
if (record.type === "requirement-mapping") validateRequirementMapping(record, currentReviewRevisions, displayPath, diagnostics);
|
|
91
103
|
if (record.type === "action-item") {
|
|
92
104
|
validateCompletedObligationAction(record, byId, loaded.model, displayPath, diagnostics);
|
|
93
105
|
}
|
|
@@ -579,7 +591,7 @@ function validateCompletedObligationEvent(record, byId, model, path, diagnostics
|
|
|
579
591
|
}
|
|
580
592
|
const obligation = byId.get(action.obligationId);
|
|
581
593
|
const expectedTypes = obligation?.type === "obligation"
|
|
582
|
-
? model
|
|
594
|
+
? obligationCompletionTypes(model, obligation)
|
|
583
595
|
: [];
|
|
584
596
|
if (!expectedTypes.length) continue;
|
|
585
597
|
const completionIds = modelSupports(model, "guided-workflow")
|
|
@@ -606,7 +618,7 @@ function validateCompletedObligationAction(record, byId, model, path, diagnostic
|
|
|
606
618
|
) return;
|
|
607
619
|
const obligation = byId.get(record.obligationId);
|
|
608
620
|
if (obligation?.type !== "obligation") return;
|
|
609
|
-
const expectedTypes = model
|
|
621
|
+
const expectedTypes = obligationCompletionTypes(model, obligation);
|
|
610
622
|
if (!expectedTypes.length) return;
|
|
611
623
|
const linked = [...new Set(record.completionResourceIds || [])]
|
|
612
624
|
.map((id) => byId.get(id))
|
|
@@ -653,7 +665,19 @@ function validateIndependentApproval(record, byId, path, diagnostics) {
|
|
|
653
665
|
function validateObligation(record, model, byId, path, diagnostics) {
|
|
654
666
|
const recurrence = record.recurrence;
|
|
655
667
|
if (!recurrence || Array.isArray(recurrence) || typeof recurrence !== "object") return;
|
|
656
|
-
const activity =
|
|
668
|
+
const activity = record.activityType === "custom"
|
|
669
|
+
? { ...model.obligationActivities?.custom, ...record.customActivity }
|
|
670
|
+
: model.obligationActivities?.[record.activityType];
|
|
671
|
+
if (record.activityType === "custom") {
|
|
672
|
+
const invalidTypes = (record.customActivity?.completionResourceTypes || []).filter((type) => !model.resources[type]);
|
|
673
|
+
if (invalidTypes.length) {
|
|
674
|
+
diagnostics.push(error(
|
|
675
|
+
"invalid-obligation-activity",
|
|
676
|
+
path,
|
|
677
|
+
`customActivity.completionResourceTypes contains unknown resource types: ${invalidTypes.join(", ")}.`
|
|
678
|
+
));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
657
681
|
if (
|
|
658
682
|
activity
|
|
659
683
|
&& Array.isArray(activity.recurrenceModes)
|
|
@@ -692,7 +716,7 @@ function validateObligation(record, model, byId, path, diagnostics) {
|
|
|
692
716
|
));
|
|
693
717
|
}
|
|
694
718
|
} else if (recurrence.mode === "event") {
|
|
695
|
-
if (!model.policyEvents?.[recurrence.eventType]) {
|
|
719
|
+
if (!model.policyEvents?.[recurrence.eventType] && !modelSupports(model, "custom-obligations")) {
|
|
696
720
|
diagnostics.push(error(
|
|
697
721
|
"invalid-obligation-recurrence",
|
|
698
722
|
path,
|
|
@@ -755,6 +779,59 @@ function validateObligation(record, model, byId, path, diagnostics) {
|
|
|
755
779
|
}
|
|
756
780
|
}
|
|
757
781
|
|
|
782
|
+
function validateRetentionScheduleItem(record, loaded, byId, currentReviewRevisions, path, diagnostics) {
|
|
783
|
+
if (record.status !== "active") return;
|
|
784
|
+
const schedule = byId.get(record.scheduleDocumentId);
|
|
785
|
+
if (schedule?.type !== "document" || schedule.documentKind !== "schedule" || schedule.workflowScope !== "program" || ["superseded", "retired"].includes(schedule.status)) {
|
|
786
|
+
diagnostics.push(error(
|
|
787
|
+
"invalid-retention-schedule-document",
|
|
788
|
+
path,
|
|
789
|
+
"scheduleDocumentId must reference a current program Document whose documentKind is schedule."
|
|
790
|
+
));
|
|
791
|
+
}
|
|
792
|
+
const sources = retentionReviewResourceIds(record, loaded);
|
|
793
|
+
const revisions = record.reviewedSourceRevisions || {};
|
|
794
|
+
const missing = sources.filter((id) => (
|
|
795
|
+
!currentReviewRevisions.get(id) || revisions[id] !== currentReviewRevisions.get(id)
|
|
796
|
+
)).concat(Object.keys(revisions).filter((id) => !sources.includes(id)));
|
|
797
|
+
if (missing.length) {
|
|
798
|
+
diagnostics.push(error(
|
|
799
|
+
"stale-retention-review",
|
|
800
|
+
path,
|
|
801
|
+
`reviewedSourceRevisions must bind the schedule Document, Information Types, operational scope, and authority or source records before activation: ${missing.join(", ")}.`
|
|
802
|
+
));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function validateRequirementMapping(record, currentReviewRevisions, path, diagnostics) {
|
|
807
|
+
const overlap = (record.sourceResourceIds || []).filter((id) => (record.targetResourceIds || []).includes(id));
|
|
808
|
+
if (overlap.length) {
|
|
809
|
+
diagnostics.push(error(
|
|
810
|
+
"invalid-requirement-mapping",
|
|
811
|
+
path,
|
|
812
|
+
`A Requirement Mapping cannot place the same record on both sides: ${[...new Set(overlap)].join(", ")}.`
|
|
813
|
+
));
|
|
814
|
+
}
|
|
815
|
+
if (record.status !== "active") return;
|
|
816
|
+
const mappedIds = [...new Set([...(record.sourceResourceIds || []), ...(record.targetResourceIds || [])])];
|
|
817
|
+
const revisions = record.reviewedSourceRevisions || {};
|
|
818
|
+
const missing = mappedIds.filter((id) => (
|
|
819
|
+
!currentReviewRevisions.get(id) || revisions[id] !== currentReviewRevisions.get(id)
|
|
820
|
+
)).concat(Object.keys(revisions).filter((id) => !mappedIds.includes(id)));
|
|
821
|
+
if (missing.length) {
|
|
822
|
+
diagnostics.push(error(
|
|
823
|
+
"stale-requirement-mapping",
|
|
824
|
+
path,
|
|
825
|
+
`reviewedSourceRevisions must bind every mapped resource before activation: ${missing.join(", ")}.`
|
|
826
|
+
));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function obligationCompletionTypes(model, obligation) {
|
|
831
|
+
if (obligation.activityType === "custom") return obligation.customActivity?.completionResourceTypes || [];
|
|
832
|
+
return model.obligationActivities?.[obligation.activityType]?.completionResourceTypes || [];
|
|
833
|
+
}
|
|
834
|
+
|
|
758
835
|
function validatePolicyEvent(record, model, byId, path, diagnostics) {
|
|
759
836
|
const event = model.policyEvents?.[record.eventType];
|
|
760
837
|
if (!event) return;
|