filegrc 0.16.20 → 0.16.22
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/retention.js +43 -9
- package/src/workflow.js +31 -5
package/package.json
CHANGED
package/src/retention.js
CHANGED
|
@@ -167,16 +167,17 @@ 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
|
-
|
|
172
|
-
|
|
170
|
+
const decisionHeading = heading || retentionPolicyHeading(loaded, entry, reviewer);
|
|
171
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput, false, decisionHeading, reviewer)];
|
|
172
|
+
let matchedSection = !decisionHeading;
|
|
173
|
+
for (const markdown of reviewMarkdownEntries(loaded, entry, false, reviewer)) {
|
|
173
174
|
try {
|
|
174
175
|
const source = entry.reviewCommit
|
|
175
176
|
? entry.reviewFiles?.get(`data/${markdown.path}`)
|
|
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,9 +215,10 @@ 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
|
-
|
|
219
|
-
|
|
218
|
+
const decisionHeading = legacySource ? heading : heading || retentionPolicyHeading(loaded, entry, reviewer);
|
|
219
|
+
const parts = [reviewSource(loaded, entry, scopeHashInput, legacySource, decisionHeading, reviewer)];
|
|
220
|
+
let matchedSection = !decisionHeading;
|
|
221
|
+
for (const markdown of reviewMarkdownEntries(loaded, entry, legacySource, reviewer)) {
|
|
220
222
|
try {
|
|
221
223
|
const source = (historicalCommit || entry.reviewCommit)
|
|
222
224
|
? loaded.historicalFiles?.get(`data/${markdown.path}`)
|
|
@@ -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) {
|
|
@@ -368,11 +370,16 @@ function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = f
|
|
|
368
370
|
)), scopeHashInput);
|
|
369
371
|
}
|
|
370
372
|
|
|
371
|
-
function reviewMarkdownEntries(loaded, entry, legacySource) {
|
|
373
|
+
function reviewMarkdownEntries(loaded, entry, legacySource, reviewer = null) {
|
|
372
374
|
// Control Markdown records the implementation procedure. Retention and
|
|
373
375
|
// mapping decisions depend on the structured Control design instead.
|
|
374
376
|
if (!legacySource && modelSupports(loaded.model, "retention-schedule-approval")
|
|
375
377
|
&& entry.record.type === "control") return [];
|
|
378
|
+
// A Component's Markdown explains how to retrieve evidence. Retention
|
|
379
|
+
// decisions depend on its information uses and scope, not those steps.
|
|
380
|
+
// Keep the Markdown in legacy hashes so committed approvals can be found.
|
|
381
|
+
if (!legacySource && reviewer?.type === "retention-schedule-item"
|
|
382
|
+
&& entry.record.type === "component") return [];
|
|
376
383
|
return markdownEntries(loaded.model, entry.record);
|
|
377
384
|
}
|
|
378
385
|
|
|
@@ -460,6 +467,32 @@ function sourceHeading(record, sourceId) {
|
|
|
460
467
|
? headings[sourceId] : null;
|
|
461
468
|
}
|
|
462
469
|
|
|
470
|
+
function retentionPolicyHeading(loaded, entry, reviewer) {
|
|
471
|
+
if (reviewer?.type !== "retention-schedule-item" || entry.record.type !== "policy") return null;
|
|
472
|
+
// Older rows often bind a whole Policy. A single retention section gives
|
|
473
|
+
// that decision a narrower basis while an explicit heading still wins.
|
|
474
|
+
const headings = new Set();
|
|
475
|
+
for (const markdown of markdownEntries(loaded.model, entry.record)) {
|
|
476
|
+
let source;
|
|
477
|
+
try {
|
|
478
|
+
source = loaded.historicalFiles?.get(`data/${markdown.path}`)
|
|
479
|
+
?? (entry.reviewCommit
|
|
480
|
+
? entry.reviewFiles?.get(`data/${markdown.path}`)
|
|
481
|
+
?? getFileAtRevision(loaded.root, entry.reviewCommit, `data/${markdown.path}`)
|
|
482
|
+
: readFileSync(resolveDataPath(loaded.root, markdown.path), "utf8"));
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (error.code === "ENOENT") continue;
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
for (const line of (source || "").split("\n")) {
|
|
488
|
+
const heading = /^#{2,6}\s+(.+?)\s*#*\s*$/.exec(line)?.[1];
|
|
489
|
+
if (heading && /\b(retention|disposal|deletion|destruction|archiving)\b/i.test(heading)
|
|
490
|
+
&& reviewedMarkdownSection(source, heading) !== null) headings.add(heading);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return headings.size === 1 ? [...headings][0] : null;
|
|
494
|
+
}
|
|
495
|
+
|
|
463
496
|
const reviewDecisionFields = {
|
|
464
497
|
control: ["statement", "requirementIds", "code", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"],
|
|
465
498
|
commitment: ["commitmentKind", "statement", "systemIds", "sourceResourceIds", "sourceSectionHeadings", "requirementIds", "controlIds", "customerFacing", "effectiveOn", "reportingRouteRequirements"],
|
|
@@ -489,6 +522,7 @@ function substantiveReviewSource(record, currentModel, reviewer = null) {
|
|
|
489
522
|
}
|
|
490
523
|
if (record.type === "component" && reviewer?.type === "retention-schedule-item") {
|
|
491
524
|
decision.systemUses = retentionSystemUses(decision.systemUses);
|
|
525
|
+
delete decision.evidenceSourceKinds;
|
|
492
526
|
}
|
|
493
527
|
if (["control", "commitment", "complementary-control"].includes(record.type)) {
|
|
494
528
|
decision.nonApplicableDecision = ["not-applicable", "externally-managed", "zero-population"]
|
package/src/workflow.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
1
|
+
import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
4
|
import { modelSupports } from "../model/index.js";
|
|
5
5
|
import { buildActionContext } from "./action-context.js";
|
|
6
6
|
import { applicabilityReviewIsCurrent, applicabilityScopeRevision } from "./applicability-scope.js";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
deleteResource,
|
|
15
15
|
updateResource
|
|
16
16
|
} from "./files.js";
|
|
17
|
-
import { getGitSummary, getWorkspaceHistories } from "./git.js";
|
|
17
|
+
import { getGitSummary, getWorkspaceHistories, runGitCommandSync } from "./git.js";
|
|
18
18
|
import {
|
|
19
19
|
completeObligationAction,
|
|
20
20
|
completeObligationEvent,
|
|
@@ -260,13 +260,39 @@ function contextualAction(item, loaded) {
|
|
|
260
260
|
|
|
261
261
|
export async function previewWorkflowMutation(input, mutation) {
|
|
262
262
|
const loaded = await loadWorkspace(input);
|
|
263
|
-
const
|
|
263
|
+
const previewCheckout = await mkdtemp(join(tmpdir(), "filegrc-workflow-preview-"));
|
|
264
264
|
try {
|
|
265
|
+
const repository = getGitSummary(loaded.root);
|
|
266
|
+
if (repository.available) {
|
|
267
|
+
// Legacy review bindings need committed source versions. A local clone
|
|
268
|
+
// gives preview the same history as update without touching the source.
|
|
269
|
+
runGitCommandSync(loaded.root, ["clone", "--shared", "--no-checkout", "--quiet", repository.root, previewCheckout], {
|
|
270
|
+
timeoutMs: 30_000
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const previewRoot = repository.available
|
|
274
|
+
? join(previewCheckout, relative(repository.root, loaded.root))
|
|
275
|
+
: previewCheckout;
|
|
276
|
+
await mkdir(previewRoot, { recursive: true });
|
|
277
|
+
if (repository.commit) {
|
|
278
|
+
// Restore the index and tracked workspace files. A no-checkout clone
|
|
279
|
+
// otherwise reports every source file as deleted and copied data as new.
|
|
280
|
+
runGitCommandSync(loaded.root, ["-C", previewCheckout, "reset", "--mixed", "HEAD"]);
|
|
281
|
+
if (runGitCommandSync(loaded.root, ["-C", previewRoot, "ls-files", "--", "."])) {
|
|
282
|
+
runGitCommandSync(loaded.root, ["-C", previewRoot, "restore", "--worktree", "--", "."]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await rm(join(previewRoot, "data"), { recursive: true, force: true });
|
|
265
286
|
await cp(join(loaded.root, "data"), join(previewRoot, "data"), {
|
|
266
287
|
recursive: true,
|
|
267
288
|
errorOnExist: true,
|
|
268
289
|
force: false
|
|
269
290
|
});
|
|
291
|
+
if (repository.available && !repository.clean && getGitSummary(previewRoot).clean) {
|
|
292
|
+
const marker = join(previewRoot, "preview-source-dirty.marker");
|
|
293
|
+
await writeFile(marker, "The source workspace has uncommitted files.\n");
|
|
294
|
+
runGitCommandSync(loaded.root, ["-C", previewRoot, "add", "-N", "-f", "--", "preview-source-dirty.marker"]);
|
|
295
|
+
}
|
|
270
296
|
const before = await assessWorkflow(loaded.root, mutation?.assessment || {});
|
|
271
297
|
const record = mutation?.record;
|
|
272
298
|
const existing = record?.id
|
|
@@ -307,7 +333,7 @@ export async function previewWorkflowMutation(input, mutation) {
|
|
|
307
333
|
workflow: after
|
|
308
334
|
};
|
|
309
335
|
} finally {
|
|
310
|
-
await rm(
|
|
336
|
+
await rm(previewCheckout, { recursive: true, force: true });
|
|
311
337
|
}
|
|
312
338
|
}
|
|
313
339
|
|