filegrc 0.16.19 → 0.16.20
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/collection-scope.js +5 -1
- package/src/retention.js +32 -16
package/package.json
CHANGED
package/src/collection-scope.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { modelSupports } from "../model/index.js";
|
|
2
2
|
import { programComponents, selectedRequirementIds } from "./program.js";
|
|
3
|
-
import { resourceReviewRevisionsSync, retentionReviewResourceIds, retentionUses } from "./retention.js";
|
|
3
|
+
import { resourceReviewRevisionsSync, retentionReviewResourceIds, retentionSystemUses, retentionUses } from "./retention.js";
|
|
4
4
|
import { documentIsAuditSpecific } from "./program-lifecycle.js";
|
|
5
5
|
|
|
6
6
|
export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
@@ -625,6 +625,10 @@ function dependencyRevisionValue(resourceType, record, legacy, historicalReviewM
|
|
|
625
625
|
const value = Object.fromEntries(fields
|
|
626
626
|
.filter((field) => record[field] !== undefined)
|
|
627
627
|
.map((field) => [field, record[field]]));
|
|
628
|
+
if (resourceType === "retention-schedule-item" && currentModel && !historicalReviewMetadata
|
|
629
|
+
&& !legacyRetentionSourceBasis && record.type === "component") {
|
|
630
|
+
value.systemUses = retentionSystemUses(value.systemUses);
|
|
631
|
+
}
|
|
628
632
|
if (controlDecision) value.unavailable = ["not-applicable", "retired", "superseded"].includes(record.status);
|
|
629
633
|
return value;
|
|
630
634
|
}
|
package/src/retention.js
CHANGED
|
@@ -167,7 +167,7 @@ export async function resourceReviewRevisions(loaded, ids, scopeHashInput = "leg
|
|
|
167
167
|
const entry = effectiveReviewEntry(loaded, entries.get(id));
|
|
168
168
|
if (!entry || reviewing.has(key)) return null;
|
|
169
169
|
reviewing.add(key);
|
|
170
|
-
const parts = [reviewSource(loaded, entry, scopeHashInput, false, heading)];
|
|
170
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput, false, heading, reviewer)];
|
|
171
171
|
let matchedSection = !heading;
|
|
172
172
|
for (const markdown of reviewMarkdownEntries(loaded, entry, false)) {
|
|
173
173
|
try {
|
|
@@ -214,7 +214,7 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
214
214
|
const entry = historicalCommit ? entries.get(id) : effectiveReviewEntry(loaded, entries.get(id));
|
|
215
215
|
if (!entry || reviewing.has(key)) return null;
|
|
216
216
|
reviewing.add(key);
|
|
217
|
-
const parts = [reviewSource(loaded, entry, scopeHashInput, legacySource, heading)];
|
|
217
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput, legacySource, heading, reviewer)];
|
|
218
218
|
let matchedSection = !heading;
|
|
219
219
|
for (const markdown of reviewMarkdownEntries(loaded, entry, legacySource)) {
|
|
220
220
|
try {
|
|
@@ -250,25 +250,26 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
export function resourceReviewRevisionMatches(loaded, revisions, id, stored, reviewer = null) {
|
|
253
|
-
const current = sourceHeading(reviewer, id)
|
|
253
|
+
const current = (sourceHeading(reviewer, id) || reviewer?.type === "retention-schedule-item")
|
|
254
254
|
? resourceReviewRevisionsSync(loaded, [id], "legacy", false, null, reviewer).get(id)
|
|
255
255
|
: revisions.get(id);
|
|
256
256
|
if (!current || !revisionDigest("content", stored)) return false;
|
|
257
257
|
if (revisionsMatch("content", stored, current)) return true;
|
|
258
|
-
if (sourceHeading(reviewer, id))
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
if (!sourceHeading(reviewer, id)) {
|
|
259
|
+
const legacy = resourceReviewRevisionsSync(loaded, [id], "legacy", true).get(id);
|
|
260
|
+
if (revisionsMatch("content", stored, legacy)) return true;
|
|
261
|
+
// Version 0.16.0 used a bare digest as the hash input for scopeRevision.
|
|
262
|
+
// Read that binding without changing the legacy scope: basis restored here.
|
|
263
|
+
const compatible = resourceReviewRevisionsSync(loaded, [id], "digest", true).get(id);
|
|
264
|
+
if (revisionsMatch("content", stored, compatible)) return true;
|
|
265
|
+
}
|
|
266
|
+
return historicallyEquivalentReviewSource(loaded, id, stored, current, reviewer);
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
// Recompute the old byte-sensitive binding at each committed state. The
|
|
269
270
|
// semantic source graph must still equal today's graph, including linked
|
|
270
271
|
// records and Markdown, before the old approval is treated as current.
|
|
271
|
-
function historicallyEquivalentReviewSource(loaded, id, stored, current) {
|
|
272
|
+
function historicallyEquivalentReviewSource(loaded, id, stored, current, reviewer = null) {
|
|
272
273
|
if (!loaded.root) return false;
|
|
273
274
|
const context = reviewHistoryContext(loaded.root, loaded);
|
|
274
275
|
const { index } = context;
|
|
@@ -289,8 +290,13 @@ function historicallyEquivalentReviewSource(loaded, id, stored, current) {
|
|
|
289
290
|
if (relevantPaths && ![...changed.keys()].some((path) => relevantPaths.has(path) || path.endsWith(".md"))) continue;
|
|
290
291
|
const snapshot = reviewHistoricalWorkspace(context, commit);
|
|
291
292
|
if (!snapshot?.entries.some(({ record }) => record.id === id)) continue;
|
|
292
|
-
const semantic = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit).get(id);
|
|
293
|
+
const semantic = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit, reviewer).get(id);
|
|
293
294
|
if (!revisionsMatch("content", semantic, current)) continue;
|
|
295
|
+
// A row may narrow a source to its retention facts or a named Markdown
|
|
296
|
+
// section. Accept its old binding only when those facts still match the
|
|
297
|
+
// committed source that supplied the binding.
|
|
298
|
+
const original = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit).get(id);
|
|
299
|
+
if (revisionsMatch("content", stored, original)) return true;
|
|
294
300
|
for (const scopeHashInput of ["legacy", "digest"]) {
|
|
295
301
|
const legacy = resourceReviewRevisionsSync(snapshot, [id], scopeHashInput, true, commit).get(id);
|
|
296
302
|
if (revisionsMatch("content", stored, legacy)) return true;
|
|
@@ -320,7 +326,7 @@ export function retentionUses(loaded, program) {
|
|
|
320
326
|
return [...new Map(uses.map((use) => [`${use.resource.id}:${use.informationTypeId}`, use])).values()];
|
|
321
327
|
}
|
|
322
328
|
|
|
323
|
-
function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = false, heading = null) {
|
|
329
|
+
function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = false, heading = null, reviewer = null) {
|
|
324
330
|
if (heading && !legacySource) {
|
|
325
331
|
const governingFields = entry.record.type === "policy"
|
|
326
332
|
? ["policyKind", "effectiveOn", "supersedesId", "parentPolicyId", "relatedPolicyIds", "relatedDocumentIds", "requirementIds", "audience", "acknowledgementRequired", "programRole", "reportingRouteRequirements"]
|
|
@@ -358,7 +364,7 @@ function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = f
|
|
|
358
364
|
return legacySource
|
|
359
365
|
? canonicalCalculatedRevisionJson(entry.source, scopeHashInput)
|
|
360
366
|
: canonicalCalculatedRevisionJson(JSON.stringify(substantiveReviewSource(
|
|
361
|
-
JSON.parse(entry.source), modelSupports(loaded.model, "retention-schedule-approval")
|
|
367
|
+
JSON.parse(entry.source), modelSupports(loaded.model, "retention-schedule-approval"), reviewer
|
|
362
368
|
)), scopeHashInput);
|
|
363
369
|
}
|
|
364
370
|
|
|
@@ -469,7 +475,7 @@ const reviewDecisionFields = {
|
|
|
469
475
|
document: ["documentKind", "workflowScope", "template", "version", "effectiveOn", "supersedesId", "systemIds", "controlIds", "relatedDocumentIds", "audience", "acknowledgementRequired", "trainingIds", "classificationId", "programRole", "componentIds", "reportingRouteRequirements"]
|
|
470
476
|
};
|
|
471
477
|
|
|
472
|
-
function substantiveReviewSource(record, currentModel) {
|
|
478
|
+
function substantiveReviewSource(record, currentModel, reviewer = null) {
|
|
473
479
|
if (currentModel && reviewDecisionFields[record.type]) {
|
|
474
480
|
const fields = ["id", "type", "title", "extensions", "externalIds", ...reviewDecisionFields[record.type]];
|
|
475
481
|
const decision = Object.fromEntries(fields.filter((field) => record[field] !== undefined)
|
|
@@ -481,6 +487,9 @@ function substantiveReviewSource(record, currentModel) {
|
|
|
481
487
|
} else {
|
|
482
488
|
decision.status = record.status || null;
|
|
483
489
|
}
|
|
490
|
+
if (record.type === "component" && reviewer?.type === "retention-schedule-item") {
|
|
491
|
+
decision.systemUses = retentionSystemUses(decision.systemUses);
|
|
492
|
+
}
|
|
484
493
|
if (["control", "commitment", "complementary-control"].includes(record.type)) {
|
|
485
494
|
decision.nonApplicableDecision = ["not-applicable", "externally-managed", "zero-population"]
|
|
486
495
|
.includes(record.applicabilityReview?.decision) ? record.applicabilityReview.decision : null;
|
|
@@ -511,6 +520,13 @@ function substantiveReviewSource(record, currentModel) {
|
|
|
511
520
|
return source;
|
|
512
521
|
}
|
|
513
522
|
|
|
523
|
+
export function retentionSystemUses(uses) {
|
|
524
|
+
return uses?.map((use) => ({
|
|
525
|
+
...use,
|
|
526
|
+
roles: use.roles?.filter((role) => role !== "evidence-source")
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
|
|
514
530
|
export function nearDuplicateInformationTypes(records) {
|
|
515
531
|
const types = records.filter((record) => record.type === "information-type" && !["retired", "superseded"].includes(record.status));
|
|
516
532
|
const pairs = [];
|