filegrc 0.16.0 → 0.16.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/package.json +1 -1
- package/src/applicability-scope.js +1 -1
- package/src/collection-review-integrity.js +6 -2
- package/src/collection-revision.js +41 -18
- package/src/program-amendment.js +6 -5
- package/src/requirement-mapping.js +2 -3
- package/src/retention.js +17 -7
- package/src/revisions.js +15 -6
- package/src/validate.js +5 -5
- package/src/workflow-history-integrity.js +1 -1
package/package.json
CHANGED
|
@@ -207,7 +207,7 @@ function canonicalFieldValue(model, value, field, name, revisionMap = false) {
|
|
|
207
207
|
: value;
|
|
208
208
|
}
|
|
209
209
|
return typeof value === "string" && (revisionMap || CALCULATED_REVISION_FIELDS.has(name))
|
|
210
|
-
? canonicalCalculatedRevision(value)
|
|
210
|
+
? canonicalCalculatedRevision(value, name)
|
|
211
211
|
: value;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadModel } from "../model/index.js";
|
|
2
|
-
import { collectionRevision } from "./collection-revision.js";
|
|
2
|
+
import { collectionRevision, collectionRevisionMatches } from "./collection-revision.js";
|
|
3
3
|
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
4
4
|
import { getDataFilesAtRevision, getFileAtRevision, getRecordIdentityHistory, hasGitRevision } from "./git.js";
|
|
5
5
|
import { currentCalendarDate, isRfc3339Timestamp } from "./time.js";
|
|
@@ -70,7 +70,11 @@ export function historicalCollectionReviewSnapshot(root, record, model, timezone
|
|
|
70
70
|
authoritativeSourceId: record.authoritativeComponentId
|
|
71
71
|
});
|
|
72
72
|
if (
|
|
73
|
-
!
|
|
73
|
+
!collectionRevisionMatches(snapshot, resourceType, record.collectionRevision, {
|
|
74
|
+
program,
|
|
75
|
+
authoritativeSourceId: record.authoritativeComponentId,
|
|
76
|
+
currentRevision
|
|
77
|
+
})
|
|
74
78
|
|| JSON.stringify([...(record.populationResourceIds || [])].sort()) !== JSON.stringify(collectionIds)
|
|
75
79
|
) return null;
|
|
76
80
|
const selectedIds = selector
|
|
@@ -12,25 +12,37 @@ import { markdownEntries } from "./resource-markdown.js";
|
|
|
12
12
|
import { CALCULATED_REVISION_FIELDS, CALCULATED_REVISION_MAP_FIELDS, calculateRevision, canonicalCalculatedRevision, revisionsMatch } from "./revisions.js";
|
|
13
13
|
|
|
14
14
|
export function collectionRevision(loaded, resourceType, options = {}) {
|
|
15
|
-
return calculateCollectionRevision(loaded, resourceType, options, false);
|
|
15
|
+
return calculateCollectionRevision(loaded, resourceType, options, false, options.scopeHashInput || "legacy", options.scopeFactsInput || "legacy");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function legacyCollectionRevision(loaded, resourceType, options = {}) {
|
|
19
|
-
return calculateCollectionRevision(loaded, resourceType, options, true);
|
|
19
|
+
return calculateCollectionRevision(loaded, resourceType, options, true, options.scopeHashInput || "legacy", options.scopeFactsInput || "legacy");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function collectionRevisionMatches(loaded, resourceType, storedRevision, options = {}) {
|
|
23
23
|
if (!storedRevision) return false;
|
|
24
24
|
const currentRevision = options.currentRevision
|
|
25
25
|
|| collectionRevision(loaded, resourceType, options);
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Accept both the pre-0.16 scope: input and the digest-only input used by
|
|
27
|
+
// 0.16.0, as well as the older collection basis from 0.9.1.
|
|
28
|
+
if (revisionsMatch("collection", storedRevision, currentRevision)) return true;
|
|
29
|
+
const allowOlderBasis = resourceType !== "retention-schedule-item"
|
|
30
|
+
|| !modelSupports(loaded.model, "retention-schedule-approval");
|
|
31
|
+
for (const olderBasis of [false, true]) {
|
|
32
|
+
if (olderBasis && !allowOlderBasis) continue;
|
|
33
|
+
for (const scopeHashInput of ["legacy", "digest"]) {
|
|
34
|
+
for (const scopeFactsInput of ["legacy", "source"]) {
|
|
35
|
+
if (!olderBasis && scopeHashInput === "legacy" && scopeFactsInput === "legacy") continue;
|
|
36
|
+
if (revisionsMatch("collection", storedRevision, calculateCollectionRevision(
|
|
37
|
+
loaded, resourceType, options, olderBasis, scopeHashInput, scopeFactsInput
|
|
38
|
+
))) return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
function calculateCollectionRevision(loaded, resourceType, options, legacy) {
|
|
45
|
+
function calculateCollectionRevision(loaded, resourceType, options, legacy, scopeHashInput = "legacy", scopeFactsInput = "legacy") {
|
|
34
46
|
const workspaceWideRetentionReview = resourceType === "retention-schedule-item"
|
|
35
47
|
&& modelSupports(loaded.model, "retention-schedule-approval")
|
|
36
48
|
&& !legacy;
|
|
@@ -55,7 +67,7 @@ function calculateCollectionRevision(loaded, resourceType, options, legacy) {
|
|
|
55
67
|
.map(({ record, value, includeContent }) => ({
|
|
56
68
|
id: record.id,
|
|
57
69
|
revision: createHash("sha256")
|
|
58
|
-
.update(JSON.stringify(canonicalRecordValue(loaded.model, record.type, value)))
|
|
70
|
+
.update(JSON.stringify(canonicalRecordValue(loaded.model, record.type, value, scopeHashInput)))
|
|
59
71
|
.digest("hex"),
|
|
60
72
|
contentRevisions: (legacy || includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
|
|
61
73
|
try {
|
|
@@ -68,35 +80,46 @@ function calculateCollectionRevision(loaded, resourceType, options, legacy) {
|
|
|
68
80
|
})
|
|
69
81
|
}))
|
|
70
82
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
71
|
-
const
|
|
83
|
+
const scopeFacts = collectionScopeRevisionFacts(loaded, resourceType, program);
|
|
84
|
+
const workspaceScope = scopeFactsInput === "source" ? scopeFacts : canonicalScopeFacts(scopeFacts);
|
|
72
85
|
const source = JSON.stringify({ resourceType, records, workspaceScope });
|
|
73
86
|
return legacy
|
|
74
87
|
? createHash("sha256").update(source).digest("hex")
|
|
75
88
|
: calculateRevision("collection", source);
|
|
76
89
|
}
|
|
77
90
|
|
|
78
|
-
function
|
|
91
|
+
function canonicalScopeFacts(value, field = null) {
|
|
92
|
+
if (Array.isArray(value)) return value.map((item) => canonicalScopeFacts(item));
|
|
93
|
+
if (value && typeof value === "object") {
|
|
94
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, canonicalScopeFacts(item, key)]));
|
|
95
|
+
}
|
|
96
|
+
return field === "scopeRevision" && typeof value === "string"
|
|
97
|
+
? canonicalCalculatedRevision(value, field)
|
|
98
|
+
: value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function canonicalRecordValue(model, resourceType, value, scopeHashInput) {
|
|
79
102
|
const fields = {
|
|
80
103
|
...model.commonFields,
|
|
81
104
|
...model.resources[resourceType]?.fields
|
|
82
105
|
};
|
|
83
|
-
return canonicalObject(model, value, fields);
|
|
106
|
+
return canonicalObject(model, value, fields, false, scopeHashInput);
|
|
84
107
|
}
|
|
85
108
|
|
|
86
|
-
function canonicalObject(model, value, fields = {}, revisionMap = false) {
|
|
109
|
+
function canonicalObject(model, value, fields = {}, revisionMap = false, scopeHashInput = "legacy") {
|
|
87
110
|
return Object.fromEntries(Object.keys(value).sort().map((name) => [
|
|
88
111
|
name,
|
|
89
|
-
canonicalFieldValue(model, value[name], fields[name], name, revisionMap)
|
|
112
|
+
canonicalFieldValue(model, value[name], fields[name], name, revisionMap, scopeHashInput)
|
|
90
113
|
]));
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
function canonicalFieldValue(model, value, field, name, revisionMap = false) {
|
|
116
|
+
function canonicalFieldValue(model, value, field, name, revisionMap = false, scopeHashInput = "legacy") {
|
|
94
117
|
if (Array.isArray(value)) {
|
|
95
118
|
const objectType = field?.itemObjectType;
|
|
96
119
|
const itemFields = objectType ? model.objectTypes?.[objectType]?.properties : undefined;
|
|
97
120
|
const items = value.map((item) => (
|
|
98
121
|
item && typeof item === "object" && !Array.isArray(item)
|
|
99
|
-
? canonicalObject(model, item, itemFields)
|
|
122
|
+
? canonicalObject(model, item, itemFields, false, scopeHashInput)
|
|
100
123
|
: item
|
|
101
124
|
));
|
|
102
125
|
return field?.type === "array"
|
|
@@ -105,10 +128,10 @@ function canonicalFieldValue(model, value, field, name, revisionMap = false) {
|
|
|
105
128
|
}
|
|
106
129
|
if (value && typeof value === "object") {
|
|
107
130
|
const objectType = field?.objectType;
|
|
108
|
-
return canonicalObject(model, value, objectType ? model.objectTypes?.[objectType]?.properties : undefined, CALCULATED_REVISION_MAP_FIELDS.has(name));
|
|
131
|
+
return canonicalObject(model, value, objectType ? model.objectTypes?.[objectType]?.properties : undefined, CALCULATED_REVISION_MAP_FIELDS.has(name), scopeHashInput);
|
|
109
132
|
}
|
|
110
133
|
return typeof value === "string" && (revisionMap || CALCULATED_REVISION_FIELDS.has(name))
|
|
111
|
-
? canonicalCalculatedRevision(value)
|
|
134
|
+
? canonicalCalculatedRevision(value, name, scopeHashInput)
|
|
112
135
|
: value;
|
|
113
136
|
}
|
|
114
137
|
|
package/src/program-amendment.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findResourceReferences } from "./agent.js";
|
|
2
|
-
import { resourceReviewRevision, resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
2
|
+
import { resourceReviewRevision, resourceReviewRevisionMatches, resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
3
3
|
import { loadWorkspace } from "./workspace.js";
|
|
4
4
|
|
|
5
5
|
const SOURCE_TYPES = new Set(["policy", "document", "framework", "requirement", "commitment"]);
|
|
@@ -114,7 +114,8 @@ export async function planProgramAmendment(input, options = {}) {
|
|
|
114
114
|
&& reviewBindingsDiffer(
|
|
115
115
|
[...new Set([...(record.sourceResourceIds || []), ...(record.targetResourceIds || [])])],
|
|
116
116
|
record.reviewedSourceRevisions,
|
|
117
|
-
currentReviewRevisions
|
|
117
|
+
currentReviewRevisions,
|
|
118
|
+
loaded
|
|
118
119
|
)
|
|
119
120
|
));
|
|
120
121
|
if (staleMappings.length) {
|
|
@@ -127,7 +128,7 @@ export async function planProgramAmendment(input, options = {}) {
|
|
|
127
128
|
const retention = related.filter((record) => (
|
|
128
129
|
record.type === "retention-schedule-item"
|
|
129
130
|
&& record.status === "active"
|
|
130
|
-
&& reviewBindingsDiffer(retentionReviewResourceIds(record, loaded), record.reviewedSourceRevisions, currentReviewRevisions)
|
|
131
|
+
&& reviewBindingsDiffer(retentionReviewResourceIds(record, loaded), record.reviewedSourceRevisions, currentReviewRevisions, loaded)
|
|
131
132
|
));
|
|
132
133
|
if (retention.length) {
|
|
133
134
|
missing.push({
|
|
@@ -160,10 +161,10 @@ export async function planProgramAmendment(input, options = {}) {
|
|
|
160
161
|
};
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function reviewBindingsDiffer(expectedIds, reviewed = {}, current) {
|
|
164
|
+
function reviewBindingsDiffer(expectedIds, reviewed = {}, current, loaded) {
|
|
164
165
|
const expected = new Set(expectedIds);
|
|
165
166
|
if (Object.keys(reviewed).length !== expected.size) return true;
|
|
166
|
-
return [...expected].some((id) => !current
|
|
167
|
+
return [...expected].some((id) => !resourceReviewRevisionMatches(loaded, current, id, reviewed[id]));
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
export async function assessProgramAmendmentReadiness(loaded) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { resourceReviewRevisions } from "./retention.js";
|
|
2
|
-
import { revisionsMatch } from "./revisions.js";
|
|
1
|
+
import { resourceReviewRevisionMatches, resourceReviewRevisions } from "./retention.js";
|
|
3
2
|
|
|
4
3
|
export async function assessRequirementMappingReadiness(loaded) {
|
|
5
4
|
if (!loaded.model.resources["requirement-mapping"]) return [];
|
|
@@ -18,7 +17,7 @@ export async function assessRequirementMappingReadiness(loaded) {
|
|
|
18
17
|
...(mapping.targetResourceIds || [])
|
|
19
18
|
])];
|
|
20
19
|
const staleIds = mappedIds.filter((id) => (
|
|
21
|
-
!
|
|
20
|
+
!resourceReviewRevisionMatches(loaded, revisions, id, mapping.reviewedSourceRevisions?.[id])
|
|
22
21
|
)).concat(Object.keys(mapping.reviewedSourceRevisions || {}).filter((id) => !mappedIds.includes(id)));
|
|
23
22
|
const structurallyComplete = mappingStructureIsComplete(mapping);
|
|
24
23
|
const complete = mapping.status === "active" && structurallyComplete && staleIds.length === 0;
|
package/src/retention.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function resourceReviewRevision(loaded, resourceId) {
|
|
|
154
154
|
return (await resourceReviewRevisions(loaded, [resourceId])).get(resourceId) || null;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
export async function resourceReviewRevisions(loaded, ids) {
|
|
157
|
+
export async function resourceReviewRevisions(loaded, ids, scopeHashInput = "legacy") {
|
|
158
158
|
const wanted = new Set(ids);
|
|
159
159
|
const entries = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
160
160
|
const revisions = new Map();
|
|
@@ -164,7 +164,7 @@ export async function resourceReviewRevisions(loaded, ids) {
|
|
|
164
164
|
const entry = entries.get(id);
|
|
165
165
|
if (!entry || reviewing.has(id)) return null;
|
|
166
166
|
reviewing.add(id);
|
|
167
|
-
const parts = [reviewSource(loaded, entry)];
|
|
167
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput)];
|
|
168
168
|
for (const markdown of markdownEntries(loaded.model, entry.record)) {
|
|
169
169
|
try {
|
|
170
170
|
parts.push(await readFile(resolveDataPath(loaded.root, markdown.path), "utf8"));
|
|
@@ -187,7 +187,7 @@ export async function resourceReviewRevisions(loaded, ids) {
|
|
|
187
187
|
return new Map([...revisions].filter(([id]) => wanted.has(id)));
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
export function resourceReviewRevisionsSync(loaded, ids) {
|
|
190
|
+
export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legacy") {
|
|
191
191
|
const wanted = new Set(ids);
|
|
192
192
|
const entries = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
193
193
|
const revisions = new Map();
|
|
@@ -197,7 +197,7 @@ export function resourceReviewRevisionsSync(loaded, ids) {
|
|
|
197
197
|
const entry = entries.get(id);
|
|
198
198
|
if (!entry || reviewing.has(id)) return null;
|
|
199
199
|
reviewing.add(id);
|
|
200
|
-
const parts = [reviewSource(loaded, entry)];
|
|
200
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput)];
|
|
201
201
|
for (const markdown of markdownEntries(loaded.model, entry.record)) {
|
|
202
202
|
try {
|
|
203
203
|
parts.push(readFileSync(resolveDataPath(loaded.root, markdown.path), "utf8"));
|
|
@@ -218,6 +218,16 @@ export function resourceReviewRevisionsSync(loaded, ids) {
|
|
|
218
218
|
return new Map([...revisions].filter(([id]) => wanted.has(id)));
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
export function resourceReviewRevisionMatches(loaded, revisions, id, stored) {
|
|
222
|
+
const current = revisions.get(id);
|
|
223
|
+
if (!current || !revisionDigest("content", stored)) return false;
|
|
224
|
+
if (revisionsMatch("content", stored, current)) return true;
|
|
225
|
+
// Version 0.16.0 used a bare digest as the hash input for scopeRevision.
|
|
226
|
+
// Read that binding without changing the legacy scope: basis restored here.
|
|
227
|
+
const compatible = resourceReviewRevisionsSync(loaded, [id], "digest").get(id);
|
|
228
|
+
return revisionsMatch("content", stored, compatible);
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
export function retentionUses(loaded, program) {
|
|
222
232
|
const systemIds = new Set(program.systemIds || []);
|
|
223
233
|
const componentIds = new Set(programComponents(loaded, program).map(({ id }) => id));
|
|
@@ -239,7 +249,7 @@ export function retentionUses(loaded, program) {
|
|
|
239
249
|
return [...new Map(uses.map((use) => [`${use.resource.id}:${use.informationTypeId}`, use])).values()];
|
|
240
250
|
}
|
|
241
251
|
|
|
242
|
-
function reviewSource(loaded, entry) {
|
|
252
|
+
function reviewSource(loaded, entry, scopeHashInput = "legacy") {
|
|
243
253
|
if (
|
|
244
254
|
modelSupports(loaded.model, "retention-schedule-approval")
|
|
245
255
|
&& entry.record.type === "document"
|
|
@@ -261,7 +271,7 @@ function reviewSource(loaded, entry) {
|
|
|
261
271
|
]) delete approved[field];
|
|
262
272
|
return JSON.stringify(approved);
|
|
263
273
|
}
|
|
264
|
-
return canonicalCalculatedRevisionJson(entry.source);
|
|
274
|
+
return canonicalCalculatedRevisionJson(entry.source, scopeHashInput);
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
export function nearDuplicateInformationTypes(records) {
|
|
@@ -327,7 +337,7 @@ export function retentionRuleIsCurrent(rule, revisions, byId = new Map(), loaded
|
|
|
327
337
|
const dependencyIds = retentionReviewResourceIds(rule, loaded);
|
|
328
338
|
if (Object.keys(rule.reviewedSourceRevisions).length !== dependencyIds.length) return false;
|
|
329
339
|
return dependencyIds.every((id) => (
|
|
330
|
-
|
|
340
|
+
resourceReviewRevisionMatches(loaded, revisions, id, rule.reviewedSourceRevisions?.[id])
|
|
331
341
|
));
|
|
332
342
|
}
|
|
333
343
|
|
package/src/revisions.js
CHANGED
|
@@ -59,8 +59,17 @@ export function calculatedRevisionDiagnostic(kind, stored, current) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function canonicalCalculatedRevision(value) {
|
|
62
|
+
export function canonicalCalculatedRevision(value, field = null, scopeHashInput = "legacy") {
|
|
63
63
|
const text = String(value || "");
|
|
64
|
+
// Legacy applicability reviews stored their scope digest with a `scope:`
|
|
65
|
+
// label. Keep that exact hash input when a newer scheme labels the same
|
|
66
|
+
// decision, including when the review is embedded in another source record.
|
|
67
|
+
if (field === "scopeRevision" && scopeHashInput === "legacy") {
|
|
68
|
+
const scopeDigest = revisionDigest("applicability-scope", text);
|
|
69
|
+
if (scopeDigest && (text.startsWith("scope:") || text.startsWith(PREFIXES["applicability-scope"]))) {
|
|
70
|
+
return `scope:${scopeDigest}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
64
73
|
// Keep the original digest as the calculation input. Merely installing a
|
|
65
74
|
// scheme-aware engine must not stale a decision made over unchanged facts.
|
|
66
75
|
if (/^[a-f0-9]{64}$/.test(text)) return text;
|
|
@@ -71,22 +80,22 @@ export function canonicalCalculatedRevision(value) {
|
|
|
71
80
|
|
|
72
81
|
// Preserve JSON layout because older resource-review bindings hashed the exact
|
|
73
82
|
// source bytes. Only scheme labels in model-defined revision slots may change.
|
|
74
|
-
export function canonicalCalculatedRevisionJson(source) {
|
|
83
|
+
export function canonicalCalculatedRevisionJson(source, scopeHashInput = "legacy") {
|
|
75
84
|
const fieldNames = [...CALCULATED_REVISION_FIELDS].join("|");
|
|
76
85
|
const mapNames = [...CALCULATED_REVISION_MAP_FIELDS].join("|");
|
|
77
86
|
const stringValue = /("(?:\\.|[^"\\])*")(\s*:\s*)("(?:\\.|[^"\\])*")/g;
|
|
78
|
-
const canonicalValue = (quoted) => {
|
|
87
|
+
const canonicalValue = (quoted, field) => {
|
|
79
88
|
const value = JSON.parse(quoted);
|
|
80
|
-
const canonical = canonicalCalculatedRevision(value);
|
|
89
|
+
const canonical = canonicalCalculatedRevision(value, field, scopeHashInput);
|
|
81
90
|
return canonical === value ? quoted : JSON.stringify(canonical);
|
|
82
91
|
};
|
|
83
92
|
return source
|
|
84
93
|
.replace(new RegExp(`("(?:${fieldNames})")(\\s*:\\s*)("(?:\\\\.|[^"\\\\])*")`, "g"),
|
|
85
|
-
(_match, key, separator, value) => `${key}${separator}${canonicalValue(value)}`)
|
|
94
|
+
(_match, key, separator, value) => `${key}${separator}${canonicalValue(value, JSON.parse(key))}`)
|
|
86
95
|
.replace(new RegExp(`("(?:${mapNames})")(\\s*:\\s*\\{)([^{}]*)(\\})`, "g"),
|
|
87
96
|
(_match, key, opening, body, closing) => (
|
|
88
97
|
`${key}${opening}${body.replace(stringValue, (_pair, name, separator, value) => (
|
|
89
|
-
`${name}${separator}${canonicalValue(value)}`
|
|
98
|
+
`${name}${separator}${canonicalValue(value, JSON.parse(name))}`
|
|
90
99
|
))}${closing}`
|
|
91
100
|
));
|
|
92
101
|
}
|
package/src/validate.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
parseCalendarDate,
|
|
19
19
|
validCalendarRecurrence
|
|
20
20
|
} from "./recurrence.js";
|
|
21
|
-
import { resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
21
|
+
import { resourceReviewRevisionMatches, resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
22
22
|
import { retentionScheduleApprovalIssues } from "./retention-schedule-approval.js";
|
|
23
23
|
import { obligationIsEnabled } from "./program-lifecycle.js";
|
|
24
24
|
import { currentPartyPeople, partyPeople } from "./parties.js";
|
|
@@ -156,7 +156,7 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
156
156
|
validateObligationOccurrence(record, loaded.model, loaded.resources, loaded.entries, loaded.root, loaded.workspace.timezone, byId, asOf, displayPath, diagnostics);
|
|
157
157
|
}
|
|
158
158
|
if (record.type === "retention-schedule-item") validateRetentionScheduleItem(record, loaded, byId, currentReviewRevisions, displayPath, diagnostics);
|
|
159
|
-
if (record.type === "requirement-mapping") validateRequirementMapping(record, currentReviewRevisions, displayPath, diagnostics);
|
|
159
|
+
if (record.type === "requirement-mapping") validateRequirementMapping(record, loaded, currentReviewRevisions, displayPath, diagnostics);
|
|
160
160
|
if (record.type === "action-item") {
|
|
161
161
|
validateCompletedObligationAction(record, byId, loaded.model, displayPath, diagnostics);
|
|
162
162
|
}
|
|
@@ -1998,7 +1998,7 @@ function validateRetentionScheduleItem(record, loaded, byId, currentReviewRevisi
|
|
|
1998
1998
|
const sources = retentionReviewResourceIds(record, loaded);
|
|
1999
1999
|
const revisions = record.reviewedSourceRevisions || {};
|
|
2000
2000
|
const missing = sources.filter((id) => (
|
|
2001
|
-
!
|
|
2001
|
+
!resourceReviewRevisionMatches(loaded, currentReviewRevisions, id, revisions[id])
|
|
2002
2002
|
)).concat(Object.keys(revisions).filter((id) => !sources.includes(id)));
|
|
2003
2003
|
if (missing.length) {
|
|
2004
2004
|
diagnostics.push(error(
|
|
@@ -2009,7 +2009,7 @@ function validateRetentionScheduleItem(record, loaded, byId, currentReviewRevisi
|
|
|
2009
2009
|
}
|
|
2010
2010
|
}
|
|
2011
2011
|
|
|
2012
|
-
function validateRequirementMapping(record, currentReviewRevisions, path, diagnostics) {
|
|
2012
|
+
function validateRequirementMapping(record, loaded, currentReviewRevisions, path, diagnostics) {
|
|
2013
2013
|
const overlap = (record.sourceResourceIds || []).filter((id) => (record.targetResourceIds || []).includes(id));
|
|
2014
2014
|
if (overlap.length) {
|
|
2015
2015
|
diagnostics.push(error(
|
|
@@ -2022,7 +2022,7 @@ function validateRequirementMapping(record, currentReviewRevisions, path, diagno
|
|
|
2022
2022
|
const mappedIds = [...new Set([...(record.sourceResourceIds || []), ...(record.targetResourceIds || [])])];
|
|
2023
2023
|
const revisions = record.reviewedSourceRevisions || {};
|
|
2024
2024
|
const missing = mappedIds.filter((id) => (
|
|
2025
|
-
!
|
|
2025
|
+
!resourceReviewRevisionMatches(loaded, currentReviewRevisions, id, revisions[id])
|
|
2026
2026
|
)).concat(Object.keys(revisions).filter((id) => !mappedIds.includes(id)));
|
|
2027
2027
|
if (missing.length) {
|
|
2028
2028
|
diagnostics.push(error(
|
|
@@ -836,7 +836,7 @@ function canonicalJson(value, field = null, revisionMap = false) {
|
|
|
836
836
|
if (Array.isArray(value)) return value.map((item) => canonicalJson(item));
|
|
837
837
|
if (!value || typeof value !== "object") {
|
|
838
838
|
return typeof value === "string" && (revisionMap || CALCULATED_REVISION_FIELDS.has(field))
|
|
839
|
-
? canonicalCalculatedRevision(value)
|
|
839
|
+
? canonicalCalculatedRevision(value, field)
|
|
840
840
|
: value;
|
|
841
841
|
}
|
|
842
842
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [
|