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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.19",
3
+ "version": "0.16.20",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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)) return false;
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
- return historicallyEquivalentReviewSource(loaded, id, stored, current);
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 = [];