filegrc 0.16.19 → 0.16.21
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 +65 -20
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,8 +167,9 @@ 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
|
|
171
|
-
|
|
170
|
+
const decisionHeading = heading || retentionPolicyHeading(loaded, entry, reviewer);
|
|
171
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput, false, decisionHeading, reviewer)];
|
|
172
|
+
let matchedSection = !decisionHeading;
|
|
172
173
|
for (const markdown of reviewMarkdownEntries(loaded, entry, false)) {
|
|
173
174
|
try {
|
|
174
175
|
const source = entry.reviewCommit
|
|
@@ -176,7 +177,7 @@ export async function resourceReviewRevisions(loaded, ids, scopeHashInput = "leg
|
|
|
176
177
|
?? getFileAtRevision(loaded.root, entry.reviewCommit, `data/${markdown.path}`)
|
|
177
178
|
: await readFile(resolveDataPath(loaded.root, markdown.path), "utf8");
|
|
178
179
|
if (source !== null) {
|
|
179
|
-
const section = reviewedMarkdownSection(source,
|
|
180
|
+
const section = reviewedMarkdownSection(source, decisionHeading);
|
|
180
181
|
if (section !== null) { parts.push(section); matchedSection = true; }
|
|
181
182
|
}
|
|
182
183
|
} catch (error) {
|
|
@@ -214,8 +215,9 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
214
215
|
const entry = historicalCommit ? entries.get(id) : effectiveReviewEntry(loaded, entries.get(id));
|
|
215
216
|
if (!entry || reviewing.has(key)) return null;
|
|
216
217
|
reviewing.add(key);
|
|
217
|
-
const
|
|
218
|
-
|
|
218
|
+
const decisionHeading = legacySource ? heading : heading || retentionPolicyHeading(loaded, entry, reviewer);
|
|
219
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput, legacySource, decisionHeading, reviewer)];
|
|
220
|
+
let matchedSection = !decisionHeading;
|
|
219
221
|
for (const markdown of reviewMarkdownEntries(loaded, entry, legacySource)) {
|
|
220
222
|
try {
|
|
221
223
|
const source = (historicalCommit || entry.reviewCommit)
|
|
@@ -224,7 +226,7 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
224
226
|
?? getFileAtRevision(loaded.root, historicalCommit || entry.reviewCommit, `data/${markdown.path}`)
|
|
225
227
|
: readFileSync(resolveDataPath(loaded.root, markdown.path), "utf8");
|
|
226
228
|
if (source !== null) {
|
|
227
|
-
const section = legacySource ? source : reviewedMarkdownSection(source,
|
|
229
|
+
const section = legacySource ? source : reviewedMarkdownSection(source, decisionHeading);
|
|
228
230
|
if (section !== null) { parts.push(section); matchedSection = true; }
|
|
229
231
|
}
|
|
230
232
|
} catch (error) {
|
|
@@ -250,25 +252,26 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
export function resourceReviewRevisionMatches(loaded, revisions, id, stored, reviewer = null) {
|
|
253
|
-
const current = sourceHeading(reviewer, id)
|
|
255
|
+
const current = (sourceHeading(reviewer, id) || reviewer?.type === "retention-schedule-item")
|
|
254
256
|
? resourceReviewRevisionsSync(loaded, [id], "legacy", false, null, reviewer).get(id)
|
|
255
257
|
: revisions.get(id);
|
|
256
258
|
if (!current || !revisionDigest("content", stored)) return false;
|
|
257
259
|
if (revisionsMatch("content", stored, current)) return true;
|
|
258
|
-
if (sourceHeading(reviewer, id))
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
260
|
+
if (!sourceHeading(reviewer, id)) {
|
|
261
|
+
const legacy = resourceReviewRevisionsSync(loaded, [id], "legacy", true).get(id);
|
|
262
|
+
if (revisionsMatch("content", stored, legacy)) return true;
|
|
263
|
+
// Version 0.16.0 used a bare digest as the hash input for scopeRevision.
|
|
264
|
+
// Read that binding without changing the legacy scope: basis restored here.
|
|
265
|
+
const compatible = resourceReviewRevisionsSync(loaded, [id], "digest", true).get(id);
|
|
266
|
+
if (revisionsMatch("content", stored, compatible)) return true;
|
|
267
|
+
}
|
|
268
|
+
return historicallyEquivalentReviewSource(loaded, id, stored, current, reviewer);
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
// Recompute the old byte-sensitive binding at each committed state. The
|
|
269
272
|
// semantic source graph must still equal today's graph, including linked
|
|
270
273
|
// records and Markdown, before the old approval is treated as current.
|
|
271
|
-
function historicallyEquivalentReviewSource(loaded, id, stored, current) {
|
|
274
|
+
function historicallyEquivalentReviewSource(loaded, id, stored, current, reviewer = null) {
|
|
272
275
|
if (!loaded.root) return false;
|
|
273
276
|
const context = reviewHistoryContext(loaded.root, loaded);
|
|
274
277
|
const { index } = context;
|
|
@@ -289,8 +292,13 @@ function historicallyEquivalentReviewSource(loaded, id, stored, current) {
|
|
|
289
292
|
if (relevantPaths && ![...changed.keys()].some((path) => relevantPaths.has(path) || path.endsWith(".md"))) continue;
|
|
290
293
|
const snapshot = reviewHistoricalWorkspace(context, commit);
|
|
291
294
|
if (!snapshot?.entries.some(({ record }) => record.id === id)) continue;
|
|
292
|
-
const semantic = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit).get(id);
|
|
295
|
+
const semantic = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit, reviewer).get(id);
|
|
293
296
|
if (!revisionsMatch("content", semantic, current)) continue;
|
|
297
|
+
// A row may narrow a source to its retention facts or a named Markdown
|
|
298
|
+
// section. Accept its old binding only when those facts still match the
|
|
299
|
+
// committed source that supplied the binding.
|
|
300
|
+
const original = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit).get(id);
|
|
301
|
+
if (revisionsMatch("content", stored, original)) return true;
|
|
294
302
|
for (const scopeHashInput of ["legacy", "digest"]) {
|
|
295
303
|
const legacy = resourceReviewRevisionsSync(snapshot, [id], scopeHashInput, true, commit).get(id);
|
|
296
304
|
if (revisionsMatch("content", stored, legacy)) return true;
|
|
@@ -320,7 +328,7 @@ export function retentionUses(loaded, program) {
|
|
|
320
328
|
return [...new Map(uses.map((use) => [`${use.resource.id}:${use.informationTypeId}`, use])).values()];
|
|
321
329
|
}
|
|
322
330
|
|
|
323
|
-
function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = false, heading = null) {
|
|
331
|
+
function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = false, heading = null, reviewer = null) {
|
|
324
332
|
if (heading && !legacySource) {
|
|
325
333
|
const governingFields = entry.record.type === "policy"
|
|
326
334
|
? ["policyKind", "effectiveOn", "supersedesId", "parentPolicyId", "relatedPolicyIds", "relatedDocumentIds", "requirementIds", "audience", "acknowledgementRequired", "programRole", "reportingRouteRequirements"]
|
|
@@ -358,7 +366,7 @@ function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = f
|
|
|
358
366
|
return legacySource
|
|
359
367
|
? canonicalCalculatedRevisionJson(entry.source, scopeHashInput)
|
|
360
368
|
: canonicalCalculatedRevisionJson(JSON.stringify(substantiveReviewSource(
|
|
361
|
-
JSON.parse(entry.source), modelSupports(loaded.model, "retention-schedule-approval")
|
|
369
|
+
JSON.parse(entry.source), modelSupports(loaded.model, "retention-schedule-approval"), reviewer
|
|
362
370
|
)), scopeHashInput);
|
|
363
371
|
}
|
|
364
372
|
|
|
@@ -454,6 +462,32 @@ function sourceHeading(record, sourceId) {
|
|
|
454
462
|
? headings[sourceId] : null;
|
|
455
463
|
}
|
|
456
464
|
|
|
465
|
+
function retentionPolicyHeading(loaded, entry, reviewer) {
|
|
466
|
+
if (reviewer?.type !== "retention-schedule-item" || entry.record.type !== "policy") return null;
|
|
467
|
+
// Older rows often bind a whole Policy. A single retention section gives
|
|
468
|
+
// that decision a narrower basis while an explicit heading still wins.
|
|
469
|
+
const headings = new Set();
|
|
470
|
+
for (const markdown of markdownEntries(loaded.model, entry.record)) {
|
|
471
|
+
let source;
|
|
472
|
+
try {
|
|
473
|
+
source = loaded.historicalFiles?.get(`data/${markdown.path}`)
|
|
474
|
+
?? (entry.reviewCommit
|
|
475
|
+
? entry.reviewFiles?.get(`data/${markdown.path}`)
|
|
476
|
+
?? getFileAtRevision(loaded.root, entry.reviewCommit, `data/${markdown.path}`)
|
|
477
|
+
: readFileSync(resolveDataPath(loaded.root, markdown.path), "utf8"));
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error.code === "ENOENT") continue;
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
for (const line of (source || "").split("\n")) {
|
|
483
|
+
const heading = /^#{2,6}\s+(.+?)\s*#*\s*$/.exec(line)?.[1];
|
|
484
|
+
if (heading && /\b(retention|disposal|deletion|destruction|archiving)\b/i.test(heading)
|
|
485
|
+
&& reviewedMarkdownSection(source, heading) !== null) headings.add(heading);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return headings.size === 1 ? [...headings][0] : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
457
491
|
const reviewDecisionFields = {
|
|
458
492
|
control: ["statement", "requirementIds", "code", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"],
|
|
459
493
|
commitment: ["commitmentKind", "statement", "systemIds", "sourceResourceIds", "sourceSectionHeadings", "requirementIds", "controlIds", "customerFacing", "effectiveOn", "reportingRouteRequirements"],
|
|
@@ -469,7 +503,7 @@ const reviewDecisionFields = {
|
|
|
469
503
|
document: ["documentKind", "workflowScope", "template", "version", "effectiveOn", "supersedesId", "systemIds", "controlIds", "relatedDocumentIds", "audience", "acknowledgementRequired", "trainingIds", "classificationId", "programRole", "componentIds", "reportingRouteRequirements"]
|
|
470
504
|
};
|
|
471
505
|
|
|
472
|
-
function substantiveReviewSource(record, currentModel) {
|
|
506
|
+
function substantiveReviewSource(record, currentModel, reviewer = null) {
|
|
473
507
|
if (currentModel && reviewDecisionFields[record.type]) {
|
|
474
508
|
const fields = ["id", "type", "title", "extensions", "externalIds", ...reviewDecisionFields[record.type]];
|
|
475
509
|
const decision = Object.fromEntries(fields.filter((field) => record[field] !== undefined)
|
|
@@ -481,6 +515,10 @@ function substantiveReviewSource(record, currentModel) {
|
|
|
481
515
|
} else {
|
|
482
516
|
decision.status = record.status || null;
|
|
483
517
|
}
|
|
518
|
+
if (record.type === "component" && reviewer?.type === "retention-schedule-item") {
|
|
519
|
+
decision.systemUses = retentionSystemUses(decision.systemUses);
|
|
520
|
+
delete decision.evidenceSourceKinds;
|
|
521
|
+
}
|
|
484
522
|
if (["control", "commitment", "complementary-control"].includes(record.type)) {
|
|
485
523
|
decision.nonApplicableDecision = ["not-applicable", "externally-managed", "zero-population"]
|
|
486
524
|
.includes(record.applicabilityReview?.decision) ? record.applicabilityReview.decision : null;
|
|
@@ -511,6 +549,13 @@ function substantiveReviewSource(record, currentModel) {
|
|
|
511
549
|
return source;
|
|
512
550
|
}
|
|
513
551
|
|
|
552
|
+
export function retentionSystemUses(uses) {
|
|
553
|
+
return uses?.map((use) => ({
|
|
554
|
+
...use,
|
|
555
|
+
roles: use.roles?.filter((role) => role !== "evidence-source")
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
|
|
514
559
|
export function nearDuplicateInformationTypes(records) {
|
|
515
560
|
const types = records.filter((record) => record.type === "information-type" && !["retired", "superseded"].includes(record.status));
|
|
516
561
|
const pairs = [];
|