filegrc 0.9.2 → 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/v7.json +10359 -0
- package/model/v8.json +10947 -0
- package/package.json +1 -1
- package/src/applicability-scope.js +211 -0
- package/src/audit-preparation.js +156 -20
- package/src/batch-review.js +40 -24
- package/src/cli.js +85 -7
- package/src/collection-review.js +16 -3
- package/src/collection-revision.js +23 -3
- package/src/collection-scope.js +94 -7
- package/src/document-activation.js +13 -1
- package/src/git.js +4 -4
- package/src/index.js +3 -0
- package/src/model-migration.js +381 -35
- package/src/obligations.js +142 -39
- package/src/policy-activation.js +5 -0
- 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-lifecycle.js +1 -1
- package/src/program-path.js +13 -8
- package/src/program-readiness.js +43 -13
- package/src/reconciliation.js +15 -3
- package/src/requirement-mapping.js +61 -0
- package/src/retention.js +261 -0
- package/src/server.js +76 -2
- package/src/setup.js +1 -1
- package/src/source-coverage.js +21 -7
- package/src/state.js +171 -2
- package/src/validate.js +106 -11
- package/src/web.js +441 -70
- package/src/workflow.js +33 -13
package/src/validate.js
CHANGED
|
@@ -3,10 +3,14 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { getResourceDefinition, modelSupports } from "../model/index.js";
|
|
5
5
|
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
collectionRevision,
|
|
8
|
+
collectionRevisionMatches
|
|
9
|
+
} from "./collection-revision.js";
|
|
7
10
|
import { isSafeGitName } from "./git-name.js";
|
|
8
11
|
import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
|
|
9
12
|
import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
|
|
13
|
+
import { resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
10
14
|
import { obligationIsEnabled } from "./program-lifecycle.js";
|
|
11
15
|
import { partyPeople } from "./parties.js";
|
|
12
16
|
import { isMarkdownChoice, markdownEntries } from "./resource-markdown.js";
|
|
@@ -42,6 +46,15 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
42
46
|
]));
|
|
43
47
|
const asOf = currentCalendarDate(loaded.workspace?.timezone || "UTC");
|
|
44
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);
|
|
45
58
|
for (const obligation of loaded.resources.filter((record) => record.type === "obligation" && record.status !== "retired")) {
|
|
46
59
|
for (const controlId of obligation.controlIds || []) {
|
|
47
60
|
if (!obligationsByControl.has(controlId)) obligationsByControl.set(controlId, []);
|
|
@@ -85,6 +98,8 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
85
98
|
validateCollectionReview(record, loaded, byId, displayPath, diagnostics);
|
|
86
99
|
}
|
|
87
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);
|
|
88
103
|
if (record.type === "action-item") {
|
|
89
104
|
validateCompletedObligationAction(record, byId, loaded.model, displayPath, diagnostics);
|
|
90
105
|
}
|
|
@@ -153,16 +168,20 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
diagnostics.sort((a, b) => `${a.severity}:${a.path}:${a.code}`.localeCompare(`${b.severity}:${b.path}:${b.code}`));
|
|
156
|
-
|
|
171
|
+
const result = {
|
|
157
172
|
ok: !diagnostics.some(({ severity }) => severity === "error"),
|
|
158
173
|
diagnostics,
|
|
159
174
|
counts: {
|
|
160
175
|
resources: loaded.resources.length,
|
|
161
176
|
errors: diagnostics.filter(({ severity }) => severity === "error").length,
|
|
162
177
|
warnings: diagnostics.filter(({ severity }) => severity === "warning").length
|
|
163
|
-
}
|
|
164
|
-
loaded
|
|
178
|
+
}
|
|
165
179
|
};
|
|
180
|
+
Object.defineProperty(result, "loaded", {
|
|
181
|
+
value: loaded,
|
|
182
|
+
enumerable: false
|
|
183
|
+
});
|
|
184
|
+
return result;
|
|
166
185
|
}
|
|
167
186
|
|
|
168
187
|
function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagnostics) {
|
|
@@ -246,7 +265,7 @@ function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagno
|
|
|
246
265
|
`Program-scoped Document "${document.title}" cannot fill an Audit engagement or management-Document field.`
|
|
247
266
|
));
|
|
248
267
|
}
|
|
249
|
-
if (
|
|
268
|
+
if (!["legacy-v4", "historical"].includes(document.activationBasis)) continue;
|
|
250
269
|
const historicalAuditIds = new Set(auditRefs
|
|
251
270
|
.filter(({ audit }) => ["issued", "delivered", "complete"].includes(audit.status))
|
|
252
271
|
.map(({ audit }) => audit.id));
|
|
@@ -254,7 +273,7 @@ function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagno
|
|
|
254
273
|
diagnostics.push(error(
|
|
255
274
|
"invalid-legacy-document-activation",
|
|
256
275
|
path,
|
|
257
|
-
`
|
|
276
|
+
`The historical activation basis is reserved for an engagement Document tied to exactly one issued, delivered, or completed Audit.`
|
|
258
277
|
));
|
|
259
278
|
}
|
|
260
279
|
}
|
|
@@ -313,7 +332,18 @@ function validateCollectionReview(record, loaded, byId, path, diagnostics) {
|
|
|
313
332
|
? record.authoritativeComponentId || record.authoritativeSystemId
|
|
314
333
|
: null
|
|
315
334
|
});
|
|
316
|
-
const current =
|
|
335
|
+
const current = collectionRevisionMatches(
|
|
336
|
+
loaded,
|
|
337
|
+
record.resourceType,
|
|
338
|
+
record.collectionRevision,
|
|
339
|
+
{
|
|
340
|
+
program,
|
|
341
|
+
authoritativeSourceId: record.decision === "externally-managed"
|
|
342
|
+
? record.authoritativeComponentId || record.authoritativeSystemId
|
|
343
|
+
: null,
|
|
344
|
+
currentRevision
|
|
345
|
+
}
|
|
346
|
+
);
|
|
317
347
|
if (current && !recordCount && record.decision === "complete") {
|
|
318
348
|
diagnostics.push(error(
|
|
319
349
|
"invalid-collection-review-decision",
|
|
@@ -561,7 +591,7 @@ function validateCompletedObligationEvent(record, byId, model, path, diagnostics
|
|
|
561
591
|
}
|
|
562
592
|
const obligation = byId.get(action.obligationId);
|
|
563
593
|
const expectedTypes = obligation?.type === "obligation"
|
|
564
|
-
? model
|
|
594
|
+
? obligationCompletionTypes(model, obligation)
|
|
565
595
|
: [];
|
|
566
596
|
if (!expectedTypes.length) continue;
|
|
567
597
|
const completionIds = modelSupports(model, "guided-workflow")
|
|
@@ -588,7 +618,7 @@ function validateCompletedObligationAction(record, byId, model, path, diagnostic
|
|
|
588
618
|
) return;
|
|
589
619
|
const obligation = byId.get(record.obligationId);
|
|
590
620
|
if (obligation?.type !== "obligation") return;
|
|
591
|
-
const expectedTypes = model
|
|
621
|
+
const expectedTypes = obligationCompletionTypes(model, obligation);
|
|
592
622
|
if (!expectedTypes.length) return;
|
|
593
623
|
const linked = [...new Set(record.completionResourceIds || [])]
|
|
594
624
|
.map((id) => byId.get(id))
|
|
@@ -635,7 +665,19 @@ function validateIndependentApproval(record, byId, path, diagnostics) {
|
|
|
635
665
|
function validateObligation(record, model, byId, path, diagnostics) {
|
|
636
666
|
const recurrence = record.recurrence;
|
|
637
667
|
if (!recurrence || Array.isArray(recurrence) || typeof recurrence !== "object") return;
|
|
638
|
-
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
|
+
}
|
|
639
681
|
if (
|
|
640
682
|
activity
|
|
641
683
|
&& Array.isArray(activity.recurrenceModes)
|
|
@@ -674,7 +716,7 @@ function validateObligation(record, model, byId, path, diagnostics) {
|
|
|
674
716
|
));
|
|
675
717
|
}
|
|
676
718
|
} else if (recurrence.mode === "event") {
|
|
677
|
-
if (!model.policyEvents?.[recurrence.eventType]) {
|
|
719
|
+
if (!model.policyEvents?.[recurrence.eventType] && !modelSupports(model, "custom-obligations")) {
|
|
678
720
|
diagnostics.push(error(
|
|
679
721
|
"invalid-obligation-recurrence",
|
|
680
722
|
path,
|
|
@@ -737,6 +779,59 @@ function validateObligation(record, model, byId, path, diagnostics) {
|
|
|
737
779
|
}
|
|
738
780
|
}
|
|
739
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
|
+
|
|
740
835
|
function validatePolicyEvent(record, model, byId, path, diagnostics) {
|
|
741
836
|
const event = model.policyEvents?.[record.eventType];
|
|
742
837
|
if (!event) return;
|