filegrc 0.16.20 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/retention.js +35 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.20",
3
+ "version": "0.16.21",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
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 parts = [reviewSource(loaded, entry, scopeHashInput, false, heading, reviewer)];
171
- let matchedSection = !heading;
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, heading);
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 parts = [reviewSource(loaded, entry, scopeHashInput, legacySource, heading, reviewer)];
218
- let matchedSection = !heading;
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, heading);
229
+ const section = legacySource ? source : reviewedMarkdownSection(source, decisionHeading);
228
230
  if (section !== null) { parts.push(section); matchedSection = true; }
229
231
  }
230
232
  } catch (error) {
@@ -460,6 +462,32 @@ function sourceHeading(record, sourceId) {
460
462
  ? headings[sourceId] : null;
461
463
  }
462
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
+
463
491
  const reviewDecisionFields = {
464
492
  control: ["statement", "requirementIds", "code", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"],
465
493
  commitment: ["commitmentKind", "statement", "systemIds", "sourceResourceIds", "sourceSectionHeadings", "requirementIds", "controlIds", "customerFacing", "effectiveOn", "reportingRouteRequirements"],
@@ -489,6 +517,7 @@ function substantiveReviewSource(record, currentModel, reviewer = null) {
489
517
  }
490
518
  if (record.type === "component" && reviewer?.type === "retention-schedule-item") {
491
519
  decision.systemUses = retentionSystemUses(decision.systemUses);
520
+ delete decision.evidenceSourceKinds;
492
521
  }
493
522
  if (["control", "commitment", "complementary-control"].includes(record.type)) {
494
523
  decision.nonApplicableDecision = ["not-applicable", "externally-managed", "zero-population"]