filegrc 0.4.0 → 0.5.0
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/README.md +11 -3
- package/model/index.js +9 -5
- package/model/v3.json +9391 -0
- package/package.json +2 -2
- package/src/agent.js +53 -0
- package/src/appointments.js +19 -0
- package/src/audit-preparation.js +47 -6
- package/src/audit-transition.js +96 -0
- package/src/batch-review.js +109 -0
- package/src/cli.js +418 -61
- package/src/collection-review.js +185 -0
- package/src/evidence-packet.js +34 -2
- package/src/external-reviewer.js +165 -0
- package/src/files.js +46 -10
- package/src/index.js +36 -2
- package/src/model-docs.js +15 -0
- package/src/model-migration.js +516 -21
- package/src/obligations.js +396 -13
- package/src/program-lifecycle.js +1 -0
- package/src/program-path.js +49 -12
- package/src/program-readiness.js +331 -27
- package/src/reconciliation.js +277 -0
- package/src/server.js +242 -14
- package/src/setup.js +36 -4
- package/src/source-coverage.js +61 -0
- package/src/state.js +37 -1
- package/src/validate.js +100 -3
- package/src/web.js +961 -202
- package/src/workflow.js +1595 -0
package/src/cli.js
CHANGED
|
@@ -4,6 +4,17 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { loadModel } from "../model/index.js";
|
|
5
5
|
import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
|
|
6
6
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
7
|
+
import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
|
|
8
|
+
import {
|
|
9
|
+
applyApplicabilityReview,
|
|
10
|
+
planApplicabilityReview,
|
|
11
|
+
scaffoldApplicabilityReview
|
|
12
|
+
} from "./batch-review.js";
|
|
13
|
+
import {
|
|
14
|
+
applyCollectionReview,
|
|
15
|
+
planCollectionReview,
|
|
16
|
+
scaffoldCollectionReview
|
|
17
|
+
} from "./collection-review.js";
|
|
7
18
|
import { buildWorkspace } from "./build.js";
|
|
8
19
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
9
20
|
import {
|
|
@@ -21,11 +32,18 @@ import {
|
|
|
21
32
|
completeObligationEvent,
|
|
22
33
|
completeObligationOccurrence,
|
|
23
34
|
createObligationEvent,
|
|
24
|
-
planObligations
|
|
35
|
+
planObligations,
|
|
36
|
+
scaffoldObligationCompletion
|
|
25
37
|
} from "./obligations.js";
|
|
38
|
+
import {
|
|
39
|
+
planExternalReviewerGovernance,
|
|
40
|
+
scaffoldExternalReviewerGovernance,
|
|
41
|
+
setupExternalReviewerGovernance
|
|
42
|
+
} from "./external-reviewer.js";
|
|
26
43
|
import { relativeToWorkspace, resolveDataPath } from "./paths.js";
|
|
27
44
|
import { buildAgentProgramPath } from "./program-path.js";
|
|
28
45
|
import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
46
|
+
import { applyReconciliation, planReconciliation } from "./reconciliation.js";
|
|
29
47
|
import { markdownEntries } from "./resource-markdown.js";
|
|
30
48
|
import { effectiveResourceStatus } from "./resource-status.js";
|
|
31
49
|
import { searchResources } from "./search.js";
|
|
@@ -36,9 +54,16 @@ import { createAppState } from "./state.js";
|
|
|
36
54
|
import { currentCalendarDate } from "./time.js";
|
|
37
55
|
import { validateWorkspace } from "./validate.js";
|
|
38
56
|
import { loadWorkspace } from "./workspace.js";
|
|
57
|
+
import {
|
|
58
|
+
assessWorkflow,
|
|
59
|
+
buildWorkflowDelta,
|
|
60
|
+
previewWorkflowMutation,
|
|
61
|
+
workflowForResource
|
|
62
|
+
} from "./workflow.js";
|
|
39
63
|
|
|
40
64
|
const BOOLEAN_FLAGS = new Set([
|
|
41
65
|
"allow-non-authoritative-writes",
|
|
66
|
+
"apply",
|
|
42
67
|
"check-docs",
|
|
43
68
|
"complete",
|
|
44
69
|
"current",
|
|
@@ -49,7 +74,10 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
49
74
|
"next",
|
|
50
75
|
"preview",
|
|
51
76
|
"require-ready",
|
|
77
|
+
"require-healthy",
|
|
78
|
+
"scaffold",
|
|
52
79
|
"summary",
|
|
80
|
+
"workflow",
|
|
53
81
|
"write-docs",
|
|
54
82
|
"yes"
|
|
55
83
|
]);
|
|
@@ -98,7 +126,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
98
126
|
});
|
|
99
127
|
const result = flags.preview
|
|
100
128
|
? await planWorkspaceSetup(root, setupInput)
|
|
101
|
-
: await setupWorkspace(root, setupInput);
|
|
129
|
+
: await withWorkflowDelta(root, () => setupWorkspace(root, setupInput));
|
|
102
130
|
const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
|
|
103
131
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
104
132
|
else if (flags.preview) {
|
|
@@ -148,14 +176,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
148
176
|
}
|
|
149
177
|
if (command === "migrate") {
|
|
150
178
|
const targetModel = String(flags["to-model"] || "");
|
|
151
|
-
if (
|
|
179
|
+
if (!["2", "3"].includes(targetModel)) throw new Error("Pass --to-model 2 or --to-model 3.");
|
|
152
180
|
const options = {
|
|
153
181
|
jobTitle: flags["job-title"],
|
|
154
|
-
startsOn: flags["starts-on"]
|
|
182
|
+
startsOn: flags["starts-on"],
|
|
183
|
+
targetModelVersion: targetModel
|
|
155
184
|
};
|
|
156
185
|
const plan = await planModelMigration(root, options);
|
|
157
186
|
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
158
|
-
throw new Error(
|
|
187
|
+
throw new Error(`Review migrate --to-model ${targetModel} --preview --json, then pass --yes to apply the migration.`);
|
|
159
188
|
}
|
|
160
189
|
const result = flags.preview
|
|
161
190
|
? plan
|
|
@@ -215,6 +244,71 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
215
244
|
else printProgramPathOutput(output, flags);
|
|
216
245
|
return output;
|
|
217
246
|
}
|
|
247
|
+
if (command === "workflow") {
|
|
248
|
+
const result = await assessWorkflow(root, {
|
|
249
|
+
auditId: positionals[0] || flags.audit,
|
|
250
|
+
asOf: flags["as-of"],
|
|
251
|
+
through: flags.through,
|
|
252
|
+
includeComplete: Boolean(flags.complete)
|
|
253
|
+
});
|
|
254
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
255
|
+
else printWorkflow(result);
|
|
256
|
+
if (flags["require-ready"] && result.assessments.evidenceReadiness.status !== "complete") {
|
|
257
|
+
process.exitCode = 2;
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
if (command === "period-health") {
|
|
262
|
+
const coverage = flags.start || flags.end
|
|
263
|
+
? { kind: "range", startsOn: flags.start, endsOn: flags.end }
|
|
264
|
+
: undefined;
|
|
265
|
+
const result = await assessWorkflow(root, {
|
|
266
|
+
auditId: positionals[0] || flags.audit,
|
|
267
|
+
asOf: flags["as-of"],
|
|
268
|
+
through: flags.end || flags.through,
|
|
269
|
+
coverage
|
|
270
|
+
});
|
|
271
|
+
const output = {
|
|
272
|
+
contractVersion: result.contractVersion,
|
|
273
|
+
dataModelVersion: result.dataModelVersion,
|
|
274
|
+
evaluatedAt: result.evaluatedAt,
|
|
275
|
+
input: result.input,
|
|
276
|
+
assessment: result.assessments.periodHealth,
|
|
277
|
+
findings: result.findings.filter(({ assessment }) => assessment === "period-health"),
|
|
278
|
+
workItems: result.workItems.filter((item) => (
|
|
279
|
+
["overdue", "due", "scheduled", "blocked"].includes(item.state)
|
|
280
|
+
)),
|
|
281
|
+
recommended: result.recommended
|
|
282
|
+
};
|
|
283
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
284
|
+
else {
|
|
285
|
+
console.log(`${output.assessment.status.toUpperCase()}: ${output.assessment.message}`);
|
|
286
|
+
for (const finding of output.findings) {
|
|
287
|
+
console.log(`${finding.state.toUpperCase()}\t${finding.title}\t${finding.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (flags["require-healthy"] && output.assessment.status !== "complete") process.exitCode = 2;
|
|
291
|
+
return output;
|
|
292
|
+
}
|
|
293
|
+
if (command === "milestone-check") {
|
|
294
|
+
const loaded = await loadWorkspace(root);
|
|
295
|
+
const result = await assessWorkflow(loaded, { asOf: flags["as-of"] });
|
|
296
|
+
const target = loaded.workspace?.assuranceGoal === "none"
|
|
297
|
+
? "structuralValidity"
|
|
298
|
+
: loaded.workspace?.candidateCoverage
|
|
299
|
+
? "periodHealth"
|
|
300
|
+
: "evidenceReadiness";
|
|
301
|
+
const output = {
|
|
302
|
+
milestone: target,
|
|
303
|
+
assessment: result.assessments[target],
|
|
304
|
+
findingKeys: result.assessments[target].findingKeys,
|
|
305
|
+
evaluatedAt: result.evaluatedAt
|
|
306
|
+
};
|
|
307
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
308
|
+
else console.log(`${target}: ${output.assessment.status.toUpperCase()} · ${output.assessment.message}`);
|
|
309
|
+
if (output.assessment.status !== "complete") process.exitCode = 2;
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
218
312
|
if (command === "scaffold") {
|
|
219
313
|
const loaded = await loadWorkspace(root);
|
|
220
314
|
const type = positionals[0];
|
|
@@ -236,9 +330,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
236
330
|
? { ...record, effectiveStatus }
|
|
237
331
|
: record;
|
|
238
332
|
});
|
|
239
|
-
|
|
333
|
+
const output = flags.workflow
|
|
334
|
+
? {
|
|
335
|
+
records,
|
|
336
|
+
workflow: await assessWorkflow(loaded, { asOf })
|
|
337
|
+
}
|
|
338
|
+
: records;
|
|
339
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
240
340
|
else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.effectiveStatus ?? record.status ?? ""}\t${record.title}`);
|
|
241
|
-
return
|
|
341
|
+
return output;
|
|
242
342
|
}
|
|
243
343
|
if (command === "search") {
|
|
244
344
|
const loaded = await loadWorkspace(root);
|
|
@@ -260,7 +360,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
260
360
|
});
|
|
261
361
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
262
362
|
else {
|
|
263
|
-
console.log(`${result.counts.overdue} overdue, ${result.counts.due} due, ${result.counts.upcoming} upcoming, ${result.counts.proposed} starter proposals`);
|
|
363
|
+
console.log(`${result.counts.overdue} overdue, ${result.counts.blocked} blocked, ${result.counts.due} due, ${result.counts.upcoming} upcoming, ${result.counts.proposed} starter proposals`);
|
|
264
364
|
for (const item of result.items) {
|
|
265
365
|
const deadline = item.dueWindowEndAt || item.dueWindowEnd;
|
|
266
366
|
if (!deadline) throw new Error(`Planned work "${item.title}" is missing a deadline.`);
|
|
@@ -269,7 +369,12 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
269
369
|
item.dueWindowStartAt || item.dueWindowStart,
|
|
270
370
|
deadline,
|
|
271
371
|
item.title,
|
|
272
|
-
item.actionItemId || item.obligationId
|
|
372
|
+
item.actionItemId || item.obligationId,
|
|
373
|
+
item.actionItemId
|
|
374
|
+
? item.status === "blocked"
|
|
375
|
+
? `filegrc get ${item.actionItemId} --mutation`
|
|
376
|
+
: `filegrc complete-action ${item.actionItemId} --scaffold --completed-on YYYY-MM-DD`
|
|
377
|
+
: `filegrc complete ${item.obligationId} --scaffold --window-start ${item.dueWindowStart} --completed-on YYYY-MM-DD`
|
|
273
378
|
].join("\t"));
|
|
274
379
|
}
|
|
275
380
|
if (result.triggers.length) {
|
|
@@ -360,19 +465,124 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
360
465
|
if (command === "prepare-audit") {
|
|
361
466
|
const auditId = positionals[0] || flags.audit;
|
|
362
467
|
if (!auditId) throw new Error("An audit ID is required.");
|
|
363
|
-
const result = await prepareAuditWorkspace(root, { auditId });
|
|
468
|
+
const result = await withWorkflowDelta(root, () => prepareAuditWorkspace(root, { auditId }));
|
|
364
469
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
365
470
|
else console.log(`Prepared ${result.auditId}: linked ${result.linkedDocumentIds.length} management documents and created ${result.createdPopulationIds.length} population records.`);
|
|
366
471
|
return result;
|
|
367
472
|
}
|
|
473
|
+
if (command === "reconcile") {
|
|
474
|
+
const result = flags.apply
|
|
475
|
+
? await withWorkflowDelta(root, () => applyReconciliation(root, {
|
|
476
|
+
candidateId: flags.candidate,
|
|
477
|
+
transitionFingerprint: flags.candidate,
|
|
478
|
+
occurredOn: flags["occurred-on"],
|
|
479
|
+
occurredAt: flags["occurred-at"],
|
|
480
|
+
riskLevel: flags["risk-level"],
|
|
481
|
+
title: flags.title,
|
|
482
|
+
confirmed: flags.yes === true
|
|
483
|
+
}))
|
|
484
|
+
: await planReconciliation(root);
|
|
485
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
486
|
+
else if (flags.apply) {
|
|
487
|
+
console.log(`Reconciled ${result.candidate.eventType}: created ${result.event.id} and ${result.actions.length} linked tasks.`);
|
|
488
|
+
} else if (!result.candidates.length) {
|
|
489
|
+
console.log("No direct-file transitions need confirmation.");
|
|
490
|
+
} else {
|
|
491
|
+
for (const candidate of result.candidates) {
|
|
492
|
+
console.log(`${candidate.id}\t${candidate.eventType}\t${candidate.subject.title}\t${candidate.message}`);
|
|
493
|
+
console.log(` ${candidate.action.command}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
if (command === "external-reviewer-setup") {
|
|
499
|
+
if (flags.scaffold) {
|
|
500
|
+
const result = await scaffoldExternalReviewerGovernance(root);
|
|
501
|
+
console.log(JSON.stringify(result, null, 2));
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
const payload = await readSetupPayload(positionals[0]);
|
|
505
|
+
const options = {
|
|
506
|
+
...payload,
|
|
507
|
+
confirmed: flags.yes === true
|
|
508
|
+
};
|
|
509
|
+
const result = flags.preview
|
|
510
|
+
? await planExternalReviewerGovernance(root, options)
|
|
511
|
+
: await withWorkflowDelta(root, () => setupExternalReviewerGovernance(root, options));
|
|
512
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
513
|
+
else if (flags.preview) {
|
|
514
|
+
console.log(`External reviewer governance preview: ${result.changes.create.length} records to create and ${result.changes.update.length} to update.`);
|
|
515
|
+
} else {
|
|
516
|
+
console.log(`Assigned external reviewer ${result.reviewerId} through ${result.appointmentIds.length} active Appointments.`);
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
if (command === "next-audit-cycle") {
|
|
521
|
+
const payload = await readSetupPayload(positionals[1]);
|
|
522
|
+
const options = {
|
|
523
|
+
...payload,
|
|
524
|
+
priorAuditId: positionals[0] || flags.audit || payload.priorAuditId,
|
|
525
|
+
startsOn: flags.start || payload.startsOn,
|
|
526
|
+
endsOn: flags.end || payload.endsOn,
|
|
527
|
+
confirmed: flags.yes === true
|
|
528
|
+
};
|
|
529
|
+
const result = flags.preview
|
|
530
|
+
? await planNextAuditCycle(root, options)
|
|
531
|
+
: await withWorkflowDelta(root, () => createNextAuditCycle(root, options));
|
|
532
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
533
|
+
else if (flags.preview) {
|
|
534
|
+
console.log(`${result.operation} preview: ${result.audit.title}, ${result.audit.coverage.startsOn} through ${result.audit.coverage.endsOn}.`);
|
|
535
|
+
} else {
|
|
536
|
+
console.log(`Created ${result.audit.id}. Review carried-forward scope and period continuity before fieldwork.`);
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
if (command === "review-applicability") {
|
|
541
|
+
if (flags.scaffold) {
|
|
542
|
+
const result = await scaffoldApplicabilityReview(root, { type: flags.type });
|
|
543
|
+
console.log(JSON.stringify(result, null, 2));
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
const payload = await readSetupPayload(positionals[0]);
|
|
547
|
+
const options = { ...payload, confirmed: flags.yes === true };
|
|
548
|
+
const result = flags.preview
|
|
549
|
+
? await planApplicabilityReview(root, options)
|
|
550
|
+
: await withWorkflowDelta(root, () => applyApplicabilityReview(root, options));
|
|
551
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
552
|
+
else if (flags.preview) console.log(`Applicability preview: ${result.reviewedIds.length} decisions.`);
|
|
553
|
+
else console.log(`Recorded ${result.reviewedIds.length} reviewed applicability decisions.`);
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
if (command === "review-collection") {
|
|
557
|
+
const resourceType = positionals[0] || flags.type;
|
|
558
|
+
if (flags.scaffold) {
|
|
559
|
+
const result = await scaffoldCollectionReview(root, { resourceType });
|
|
560
|
+
console.log(JSON.stringify(result, null, 2));
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
const payload = await readSetupPayload(positionals[1]);
|
|
564
|
+
const options = {
|
|
565
|
+
...payload,
|
|
566
|
+
resourceType: resourceType || payload.resourceType,
|
|
567
|
+
confirmed: flags.yes === true
|
|
568
|
+
};
|
|
569
|
+
const result = flags.preview
|
|
570
|
+
? await planCollectionReview(root, options)
|
|
571
|
+
: await withWorkflowDelta(root, () => applyCollectionReview(root, options));
|
|
572
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
573
|
+
else if (flags.preview) console.log(`Collection review preview: ${result.assessment.configuration.title}.`);
|
|
574
|
+
else console.log(`Confirmed ${result.assessment.configuration.title}.`);
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
368
577
|
if (command === "trigger") {
|
|
369
|
-
const result = await createObligationEvent(root, {
|
|
578
|
+
const result = await withWorkflowDelta(root, () => createObligationEvent(root, {
|
|
370
579
|
eventType: positionals[0],
|
|
371
580
|
occurredOn: flags["occurred-on"],
|
|
372
581
|
occurredAt: flags["occurred-at"],
|
|
582
|
+
riskLevel: flags["risk-level"],
|
|
373
583
|
subjectResourceIds: String(flags.subject || "").split(",").map((value) => value.trim()).filter(Boolean),
|
|
374
584
|
title: flags.title
|
|
375
|
-
});
|
|
585
|
+
}));
|
|
376
586
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
377
587
|
else {
|
|
378
588
|
console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
|
|
@@ -432,8 +642,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
432
642
|
console.log(JSON.stringify(mutation, null, 2));
|
|
433
643
|
return mutation;
|
|
434
644
|
}
|
|
435
|
-
|
|
436
|
-
|
|
645
|
+
const output = flags.workflow
|
|
646
|
+
? {
|
|
647
|
+
record,
|
|
648
|
+
workflow: workflowForResource(await assessWorkflow(loaded), record.type, record.id)
|
|
649
|
+
}
|
|
650
|
+
: record;
|
|
651
|
+
console.log(JSON.stringify(output, null, 2));
|
|
652
|
+
return output;
|
|
437
653
|
}
|
|
438
654
|
if (command === "references") {
|
|
439
655
|
const loaded = await loadWorkspace(root);
|
|
@@ -447,36 +663,65 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
447
663
|
}
|
|
448
664
|
return result;
|
|
449
665
|
}
|
|
666
|
+
if (command === "preview-mutation") {
|
|
667
|
+
const payload = await readSetupPayload(positionals[0]);
|
|
668
|
+
const result = await previewWorkflowMutation(root, payload);
|
|
669
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
670
|
+
else {
|
|
671
|
+
console.log(`${result.operation.toUpperCase()} preview for ${result.target.type}/${result.target.id}`);
|
|
672
|
+
console.log(`${result.workflowDelta.findings.added.length} findings added, ${result.workflowDelta.findings.removed.length} resolved, ${result.workflowDelta.findings.changed.length} changed.`);
|
|
673
|
+
if (result.workflow.recommended) console.log(`Next: ${result.workflow.recommended.title}`);
|
|
674
|
+
console.log("No workspace files were changed.");
|
|
675
|
+
}
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
450
678
|
if (command === "create") {
|
|
451
679
|
const mutation = await readMutation(positionals[0]);
|
|
452
|
-
const result = await createResource(root, mutation.record, { content: mutation.content });
|
|
453
|
-
if (flags.json) console.log(JSON.stringify(
|
|
680
|
+
const result = await withWorkflowDelta(root, () => createResource(root, mutation.record, { content: mutation.content }));
|
|
681
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
454
682
|
else console.log(`Created ${result.record.type}/${result.record.id}`);
|
|
455
683
|
return result;
|
|
456
684
|
}
|
|
457
685
|
if (command === "complete") {
|
|
458
686
|
const [obligationId, file] = positionals;
|
|
687
|
+
if (flags.scaffold) {
|
|
688
|
+
const result = await scaffoldObligationCompletion(root, {
|
|
689
|
+
obligationId,
|
|
690
|
+
windowStart: flags["window-start"],
|
|
691
|
+
completedOn: flags["completed-on"]
|
|
692
|
+
});
|
|
693
|
+
console.log(JSON.stringify(result, null, 2));
|
|
694
|
+
return result;
|
|
695
|
+
}
|
|
459
696
|
const mutation = await readMutation(file);
|
|
460
|
-
const result = await completeObligationOccurrence(root, {
|
|
697
|
+
const result = await withWorkflowDelta(root, () => completeObligationOccurrence(root, {
|
|
461
698
|
obligationId,
|
|
462
699
|
record: mutation.record,
|
|
463
700
|
content: mutation.content,
|
|
464
|
-
expectedRevision:
|
|
465
|
-
});
|
|
701
|
+
expectedRevision: expectedRevision(flags, mutation, `obligation/${obligationId}`)
|
|
702
|
+
}));
|
|
466
703
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
467
704
|
else console.log(`Created ${result.created.type}/${result.created.id} and linked it to obligation/${obligationId}`);
|
|
468
705
|
return result;
|
|
469
706
|
}
|
|
470
707
|
if (command === "complete-action") {
|
|
471
708
|
const [actionItemId, file] = positionals;
|
|
709
|
+
if (flags.scaffold) {
|
|
710
|
+
const result = await scaffoldObligationCompletion(root, {
|
|
711
|
+
actionItemId,
|
|
712
|
+
completedOn: flags["completed-on"]
|
|
713
|
+
});
|
|
714
|
+
console.log(JSON.stringify(result, null, 2));
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
472
717
|
const mutation = await readMutation(file);
|
|
473
|
-
const result = await completeObligationAction(root, {
|
|
718
|
+
const result = await withWorkflowDelta(root, () => completeObligationAction(root, {
|
|
474
719
|
actionItemId,
|
|
475
720
|
completedOn: flags["completed-on"],
|
|
476
721
|
record: mutation.record,
|
|
477
722
|
content: mutation.content,
|
|
478
|
-
expectedRevision:
|
|
479
|
-
});
|
|
723
|
+
expectedRevision: expectedRevision(flags, mutation, `action-item/${actionItemId}`)
|
|
724
|
+
}));
|
|
480
725
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
481
726
|
else console.log(`Created ${result.created.type}/${result.created.id}, linked it to action-item/${actionItemId}, and marked the action done.`);
|
|
482
727
|
return result;
|
|
@@ -484,25 +729,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
484
729
|
if (command === "complete-event") {
|
|
485
730
|
const eventId = positionals[0];
|
|
486
731
|
if (!eventId) throw new Error("A Policy Event ID is required.");
|
|
487
|
-
const result = await completeObligationEvent(root, {
|
|
732
|
+
const result = await withWorkflowDelta(root, () => completeObligationEvent(root, {
|
|
488
733
|
eventId,
|
|
489
734
|
completedOn: flags["completed-on"],
|
|
490
735
|
expectedRevision: requireExpectedRevision(flags, `obligation-event/${eventId}`)
|
|
491
|
-
});
|
|
492
|
-
if (flags.json) console.log(JSON.stringify(
|
|
736
|
+
}));
|
|
737
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
493
738
|
else console.log(`Marked obligation-event/${eventId} complete.`);
|
|
494
739
|
return result;
|
|
495
740
|
}
|
|
496
741
|
if (command === "update") {
|
|
497
742
|
const [type, id, file] = positionals;
|
|
498
743
|
const mutation = await readMutation(file, { requireRevision: true });
|
|
499
|
-
const result = await updateResource(root, type, id, mutation.record, {
|
|
744
|
+
const result = await withWorkflowDelta(root, () => updateResource(root, type, id, mutation.record, {
|
|
500
745
|
content: mutation.content,
|
|
501
746
|
expectedRevision: mutation.revision,
|
|
502
747
|
expectedContentRevisions: mutation.contentRevisions,
|
|
503
748
|
requireExpectedContentRevisions: true
|
|
504
|
-
});
|
|
505
|
-
if (flags.json) console.log(JSON.stringify(
|
|
749
|
+
}));
|
|
750
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
506
751
|
else console.log(`Updated ${result.record.type}/${result.record.id}`);
|
|
507
752
|
return result;
|
|
508
753
|
}
|
|
@@ -522,14 +767,21 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
522
767
|
const state = await createAppState(root);
|
|
523
768
|
const stateEntry = state.resources.find((item) => item.record.type === type && item.record.id === id);
|
|
524
769
|
const existingContentRevision = stateEntry.content?.[slot.name]?.revision;
|
|
525
|
-
await updateResource(root, type, id, record, {
|
|
770
|
+
const mutationResult = await withWorkflowDelta(root, () => updateResource(root, type, id, record, {
|
|
526
771
|
content: { [slot.name]: source },
|
|
527
772
|
expectedRevision: stateEntry.revision,
|
|
528
773
|
expectedContentRevisions: existingContentRevision
|
|
529
774
|
? { [slot.path]: flags["expected-revision"] ?? existingContentRevision }
|
|
530
775
|
: undefined
|
|
531
|
-
});
|
|
532
|
-
const result = {
|
|
776
|
+
}));
|
|
777
|
+
const result = {
|
|
778
|
+
type,
|
|
779
|
+
id,
|
|
780
|
+
slot: slot.name,
|
|
781
|
+
path: `data/${slot.path}`,
|
|
782
|
+
written: true,
|
|
783
|
+
workflowDelta: mutationResult.workflowDelta
|
|
784
|
+
};
|
|
533
785
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
534
786
|
else console.log(`Updated ${result.path}`);
|
|
535
787
|
return result;
|
|
@@ -549,14 +801,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
549
801
|
if (command === "attach") {
|
|
550
802
|
const [evidenceId, sourcePath] = positionals;
|
|
551
803
|
if (!evidenceId || !sourcePath) throw new Error("An evidence ID and source file are required.");
|
|
552
|
-
const result = await addEvidenceAttachment(root, evidenceId, sourcePath, {
|
|
804
|
+
const result = await withWorkflowDelta(root, () => addEvidenceAttachment(root, evidenceId, sourcePath, {
|
|
553
805
|
name: flags.name,
|
|
554
806
|
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
555
|
-
});
|
|
807
|
+
}));
|
|
556
808
|
const output = {
|
|
557
809
|
evidenceId,
|
|
558
810
|
path: `data/${result.dataRelativePath}`,
|
|
559
|
-
filePaths: result.record.filePaths
|
|
811
|
+
filePaths: result.record.filePaths,
|
|
812
|
+
workflowDelta: result.workflowDelta
|
|
560
813
|
};
|
|
561
814
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
562
815
|
else console.log(`Attached ${output.path} to evidence/${evidenceId}`);
|
|
@@ -566,13 +819,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
566
819
|
const [evidenceId, attachment] = positionals;
|
|
567
820
|
if (!evidenceId || !attachment) throw new Error("An evidence ID and attachment name are required.");
|
|
568
821
|
if (!flags.yes) throw new Error("Pass --yes to confirm attachment removal.");
|
|
569
|
-
const result = await removeEvidenceAttachment(root, evidenceId, attachment, {
|
|
822
|
+
const result = await withWorkflowDelta(root, () => removeEvidenceAttachment(root, evidenceId, attachment, {
|
|
570
823
|
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
571
|
-
});
|
|
824
|
+
}));
|
|
572
825
|
const output = {
|
|
573
826
|
evidenceId,
|
|
574
827
|
removed: `data/${result.dataRelativePath}`,
|
|
575
|
-
filePaths: result.record.filePaths ?? []
|
|
828
|
+
filePaths: result.record.filePaths ?? [],
|
|
829
|
+
workflowDelta: result.workflowDelta
|
|
576
830
|
};
|
|
577
831
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
578
832
|
else console.log(`Detached and removed ${output.removed} from evidence/${evidenceId}`);
|
|
@@ -581,11 +835,13 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
581
835
|
if (command === "delete") {
|
|
582
836
|
const [type, id] = positionals;
|
|
583
837
|
if (!flags.yes) throw new Error("Pass --yes to confirm deletion. Preserve historical records unless this is a mistake or uncommitted draft.");
|
|
584
|
-
await deleteResource(root, type, id, {
|
|
838
|
+
const result = await withWorkflowDelta(root, () => deleteResource(root, type, id, {
|
|
585
839
|
expectedRevision: requireExpectedRevision(flags, `${type}/${id}`)
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
|
|
840
|
+
}));
|
|
841
|
+
const output = { deleted: true, type, id, workflowDelta: result.workflowDelta };
|
|
842
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
843
|
+
else console.log(`Deleted ${type}/${id}`);
|
|
844
|
+
return output;
|
|
589
845
|
}
|
|
590
846
|
throw new Error(`Unknown command "${command}". Run filegrc help.`);
|
|
591
847
|
}
|
|
@@ -611,6 +867,16 @@ function parseArgs(args) {
|
|
|
611
867
|
return { positionals, flags };
|
|
612
868
|
}
|
|
613
869
|
|
|
870
|
+
async function withWorkflowDelta(root, task) {
|
|
871
|
+
const before = await assessWorkflow(root);
|
|
872
|
+
const result = await task();
|
|
873
|
+
const after = await assessWorkflow(root);
|
|
874
|
+
return {
|
|
875
|
+
...result,
|
|
876
|
+
workflowDelta: buildWorkflowDelta(before, after)
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
614
880
|
async function readMutation(path, options = {}) {
|
|
615
881
|
if (!path) throw new Error("A JSON file path or - is required.");
|
|
616
882
|
const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
|
|
@@ -625,6 +891,14 @@ function requireExpectedRevision(flags, target) {
|
|
|
625
891
|
return revision;
|
|
626
892
|
}
|
|
627
893
|
|
|
894
|
+
function expectedRevision(flags, mutation, target) {
|
|
895
|
+
const revision = flags["expected-revision"] || mutation.revision;
|
|
896
|
+
if (typeof revision !== "string" || revision.length === 0) {
|
|
897
|
+
throw new Error(`A mutation revision or --expected-revision is required when changing ${target}. Reload the resource and try again.`);
|
|
898
|
+
}
|
|
899
|
+
return revision;
|
|
900
|
+
}
|
|
901
|
+
|
|
628
902
|
async function readSetupPayload(path) {
|
|
629
903
|
if (!path) return {};
|
|
630
904
|
const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
|
|
@@ -715,26 +989,38 @@ Usage:
|
|
|
715
989
|
filegrc build [root] [--output .filegrc/site]
|
|
716
990
|
filegrc validate [root] [--json]
|
|
717
991
|
filegrc model [--json|--write-docs|--check-docs]
|
|
718
|
-
filegrc migrate --to-model 2 [--preview] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
992
|
+
filegrc migrate --to-model <2|3> [--preview] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
719
993
|
filegrc describe <resource-type>
|
|
720
994
|
filegrc types [--json]
|
|
721
995
|
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
722
996
|
filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--summary|--next|--current] [--json]
|
|
997
|
+
filegrc workflow [audit-id] [--as-of YYYY-MM-DD] [--through YYYY-MM-DD] [--complete] [--require-ready] [--json]
|
|
998
|
+
filegrc period-health [audit-id] [--start YYYY-MM-DD --end YYYY-MM-DD] [--as-of YYYY-MM-DD] [--require-healthy] [--json]
|
|
999
|
+
filegrc milestone-check [--as-of YYYY-MM-DD] [--json]
|
|
723
1000
|
filegrc scaffold <resource-type> --title text [--id resource-id]
|
|
724
|
-
filegrc list [resource-type] [--json]
|
|
1001
|
+
filegrc list [resource-type] [--workflow] [--json]
|
|
725
1002
|
filegrc search <query> [--type resource-type] [--json]
|
|
726
1003
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
727
1004
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
728
1005
|
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
729
1006
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
730
1007
|
filegrc prepare-audit <audit-id> [--json]
|
|
731
|
-
filegrc
|
|
1008
|
+
filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes] [--risk-level normal|high] [--json]
|
|
1009
|
+
filegrc external-reviewer-setup --scaffold
|
|
1010
|
+
filegrc external-reviewer-setup <reviewer.json|-> [--preview|--yes] [--json]
|
|
1011
|
+
filegrc next-audit-cycle <prior-audit-id> [cycle.json|-] --start YYYY-MM-DD --end YYYY-MM-DD [--preview|--yes] [--json]
|
|
1012
|
+
filegrc review-applicability [--scaffold --type requirement|control|commitment|complementary-control] [decisions.json|-] [--preview|--yes] [--json]
|
|
1013
|
+
filegrc review-collection <resource-type> [--scaffold | review.json|-] [--preview|--yes] [--json]
|
|
1014
|
+
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--risk-level normal|high] [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
732
1015
|
filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
733
1016
|
filegrc get [resource-type] <id> [--mutation]
|
|
734
1017
|
filegrc references <id> [--json]
|
|
1018
|
+
filegrc preview-mutation <preview.json|-> [--json]
|
|
735
1019
|
filegrc create <mutation.json|-> [--json]
|
|
736
|
-
filegrc complete <obligation-id>
|
|
737
|
-
filegrc complete
|
|
1020
|
+
filegrc complete <obligation-id> --scaffold --window-start YYYY-MM-DD [--completed-on YYYY-MM-DD]
|
|
1021
|
+
filegrc complete <obligation-id> <completion-record.json|-> [--expected-revision hash] [--json]
|
|
1022
|
+
filegrc complete-action <action-item-id> --scaffold [--completed-on YYYY-MM-DD]
|
|
1023
|
+
filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD [--expected-revision hash] [--json]
|
|
738
1024
|
filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD --expected-revision hash [--json]
|
|
739
1025
|
filegrc update <resource-type> <id> <mutation.json|-> [--json]
|
|
740
1026
|
filegrc content <resource-type> <id> [slot] [--write markdown-file|-] [--expected-revision hash] [--json]
|
|
@@ -777,7 +1063,7 @@ Options:
|
|
|
777
1063
|
--boundary <description> boundary
|
|
778
1064
|
--owner <person-id> ownerId
|
|
779
1065
|
--criticality <level> low, medium, high, or critical
|
|
780
|
-
--classification <
|
|
1066
|
+
--classification <level> public, internal, confidential, or restricted
|
|
781
1067
|
--internet-exposed <bool> true or false
|
|
782
1068
|
--program-goal <goal> none, readiness, type-1, or type-2
|
|
783
1069
|
--draft Save the service boundary as planned
|
|
@@ -790,16 +1076,15 @@ Options:
|
|
|
790
1076
|
}
|
|
791
1077
|
if (command === "migrate") {
|
|
792
1078
|
console.log(`Usage:
|
|
793
|
-
filegrc migrate --to-model 2 [options]
|
|
1079
|
+
filegrc migrate --to-model <2|3> [options]
|
|
794
1080
|
|
|
795
|
-
Upgrade a
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
schemaVersion keys, and changes dataModelVersion last. It writes no Git commit.
|
|
1081
|
+
Upgrade a workspace through an explicit, reviewable model boundary. Model v1
|
|
1082
|
+
workspaces migrate to v2 first. Model v2 workspaces migrate to v3 with planned
|
|
1083
|
+
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1084
|
+
and dataModelVersion changed last. The command writes no Git commit.
|
|
800
1085
|
|
|
801
1086
|
Options:
|
|
802
|
-
--to-model <version> Required target model;
|
|
1087
|
+
--to-model <version> Required target model; 2 for legacy v1 migration, 3 for the active model
|
|
803
1088
|
--preview Show the complete atomic record plan without writing
|
|
804
1089
|
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
805
1090
|
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
@@ -809,7 +1094,7 @@ Options:
|
|
|
809
1094
|
--help Show this help
|
|
810
1095
|
|
|
811
1096
|
Start with:
|
|
812
|
-
npx filegrc migrate --to-model
|
|
1097
|
+
npx filegrc migrate --to-model 3 --preview --json`);
|
|
813
1098
|
return;
|
|
814
1099
|
}
|
|
815
1100
|
if (command === "program-readiness") {
|
|
@@ -829,6 +1114,25 @@ Options:
|
|
|
829
1114
|
--help Show this help`);
|
|
830
1115
|
return;
|
|
831
1116
|
}
|
|
1117
|
+
if (command === "workflow") {
|
|
1118
|
+
console.log(`Usage:
|
|
1119
|
+
filegrc workflow [audit-id] [options]
|
|
1120
|
+
|
|
1121
|
+
Return the shared assessment envelope used by browser, HTTP, CLI, static, and
|
|
1122
|
+
agent workflows. Results include named assessments, normalized findings,
|
|
1123
|
+
deterministic Work Items, and one recommended next action.
|
|
1124
|
+
|
|
1125
|
+
Options:
|
|
1126
|
+
--audit <id> Limit audit assessments to one engagement
|
|
1127
|
+
--as-of <date> Evaluate on YYYY-MM-DD
|
|
1128
|
+
--through <date> Include scheduled work through YYYY-MM-DD
|
|
1129
|
+
--complete Include completed Work Items
|
|
1130
|
+
--require-ready Exit with code 2 unless Evidence Readiness passes
|
|
1131
|
+
--json Print the versioned result envelope
|
|
1132
|
+
--root <path> Workspace path
|
|
1133
|
+
--help Show this help`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
832
1136
|
if (command === "program-path") {
|
|
833
1137
|
console.log(`Usage:
|
|
834
1138
|
filegrc program-path [audit-id] [options]
|
|
@@ -875,11 +1179,14 @@ function agentOverview(model) {
|
|
|
875
1179
|
build: "filegrc build [root]",
|
|
876
1180
|
validate: "filegrc validate [root] --json",
|
|
877
1181
|
model: "filegrc model --json",
|
|
878
|
-
migrate: "filegrc migrate --to-model
|
|
1182
|
+
migrate: "filegrc migrate --to-model 3 --preview --json",
|
|
879
1183
|
describe: "filegrc describe <resource-type>",
|
|
880
1184
|
types: "filegrc types --json",
|
|
881
1185
|
guide: "filegrc guide [resource-type] --json",
|
|
882
1186
|
programPath: "filegrc program-path [audit-id] --next --json",
|
|
1187
|
+
workflow: "filegrc workflow [audit-id] --json",
|
|
1188
|
+
periodHealth: "filegrc period-health [audit-id] --require-healthy --json",
|
|
1189
|
+
milestoneCheck: "filegrc milestone-check --json",
|
|
883
1190
|
scaffold: "filegrc scaffold <resource-type> --title <name>",
|
|
884
1191
|
list: "filegrc list [resource-type] --json",
|
|
885
1192
|
search: "filegrc search <query> --json",
|
|
@@ -888,10 +1195,16 @@ function agentOverview(model) {
|
|
|
888
1195
|
evidenceMap: "filegrc evidence-map --json",
|
|
889
1196
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
890
1197
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
|
1198
|
+
reconcile: "filegrc reconcile --preview --json",
|
|
1199
|
+
externalReviewerSetup: "filegrc external-reviewer-setup [--scaffold | <reviewer.json|-> --preview] --json",
|
|
1200
|
+
nextAuditCycle: "filegrc next-audit-cycle <prior-audit-id> --start <date> --end <date> --preview --json",
|
|
1201
|
+
reviewApplicability: "filegrc review-applicability <decisions.json|-> --preview --json",
|
|
1202
|
+
reviewCollection: "filegrc review-collection <resource-type> [--scaffold | <review.json|-> --preview] --json",
|
|
891
1203
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
892
1204
|
evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
|
|
893
1205
|
get: "filegrc get <resource-id> [--mutation]",
|
|
894
1206
|
references: "filegrc references <resource-id> --json",
|
|
1207
|
+
previewMutation: "filegrc preview-mutation <preview.json> --json",
|
|
895
1208
|
create: "filegrc create <mutation.json>",
|
|
896
1209
|
complete: "filegrc complete <obligation-id> <completion-mutation.json>",
|
|
897
1210
|
completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
|
|
@@ -934,6 +1247,17 @@ function printAgentGuide(result) {
|
|
|
934
1247
|
console.log(`Policy basis: ${result.policyBasis}`);
|
|
935
1248
|
console.log(`Timing: ${result.cadence}`);
|
|
936
1249
|
console.log(`JSON: ${result.location}`);
|
|
1250
|
+
if (result.reviewRequirements.collectionReview) {
|
|
1251
|
+
const review = result.reviewRequirements.collectionReview;
|
|
1252
|
+
console.log(`\nCollection review: ${review.title} (${review.status}, ${review.recordCount} ${review.recordCount === 1 ? "record" : "records"})`);
|
|
1253
|
+
console.log(review.description);
|
|
1254
|
+
for (const point of review.reviewPoints) console.log(`- ${point}`);
|
|
1255
|
+
console.log(`Action: ${review.command}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (result.reviewRequirements.recordReviewPoints.length) {
|
|
1258
|
+
console.log("\nWhen reviewing each record:");
|
|
1259
|
+
for (const point of result.reviewRequirements.recordReviewPoints) console.log(`- ${point}`);
|
|
1260
|
+
}
|
|
937
1261
|
console.log("\nRequired fields:");
|
|
938
1262
|
for (const field of result.requiredAtCreation) console.log(formatGuideField(field));
|
|
939
1263
|
if (result.conditionalRequirements.length) {
|
|
@@ -980,11 +1304,28 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
980
1304
|
const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
|
|
981
1305
|
const stages = buildAgentProgramPath(model).map((stage) => {
|
|
982
1306
|
if (stage.id === "audit") {
|
|
1307
|
+
const auditAction = auditReadiness?.firstAction || (
|
|
1308
|
+
auditReadiness?.status === "not-started"
|
|
1309
|
+
? {
|
|
1310
|
+
id: "create-audit",
|
|
1311
|
+
status: "action",
|
|
1312
|
+
title: "Create the planned CPA engagement",
|
|
1313
|
+
message: "Create a planned Audit from the current management scope, then replace the remaining scaffold values with the CPA firm and firm-agreed scope and dates.",
|
|
1314
|
+
resourceType: "audit",
|
|
1315
|
+
commands: [
|
|
1316
|
+
"npx filegrc guide audit --json",
|
|
1317
|
+
"npx filegrc scaffold audit --title \"YEAR SOC 2 TYPE\" > audit-mutation.json",
|
|
1318
|
+
"npx filegrc create audit-mutation.json --json",
|
|
1319
|
+
"npx filegrc prepare-audit AUDIT_ID --json"
|
|
1320
|
+
]
|
|
1321
|
+
}
|
|
1322
|
+
: null
|
|
1323
|
+
);
|
|
983
1324
|
return {
|
|
984
1325
|
...stage,
|
|
985
1326
|
status: auditReadiness?.status || "not-started",
|
|
986
1327
|
counts: auditReadiness?.counts || null,
|
|
987
|
-
nextActions:
|
|
1328
|
+
nextActions: auditAction ? [auditAction] : []
|
|
988
1329
|
};
|
|
989
1330
|
}
|
|
990
1331
|
const readinessId = stage.id === "run" ? "operation" : stage.id;
|
|
@@ -1002,6 +1343,7 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1002
1343
|
const currentStep = stages.find((stage) => !["complete", "operating", "management-ready"].includes(stage.status)) || stages.at(-1);
|
|
1003
1344
|
return {
|
|
1004
1345
|
schemaVersion: 1,
|
|
1346
|
+
dataModelVersion: String(model.modelVersion),
|
|
1005
1347
|
asOf: readiness.asOf,
|
|
1006
1348
|
currentStep: { id: currentStep.id, number: currentStep.number, title: currentStep.title },
|
|
1007
1349
|
evidenceReady: readiness.evidenceReady,
|
|
@@ -1018,14 +1360,13 @@ function printProgramPath(result) {
|
|
|
1018
1360
|
console.log(stage.summary);
|
|
1019
1361
|
for (const page of stage.pages) {
|
|
1020
1362
|
console.log(`${page.order ? `Step ${page.order}` : "Operating area"} · ${page.title} (${page.type || `utility:${page.utility}`})`);
|
|
1021
|
-
console.log(`
|
|
1022
|
-
console.log(`
|
|
1023
|
-
console.log(` Policy basis: ${page.policyBasis}`);
|
|
1363
|
+
console.log(` ${page.summary}`);
|
|
1364
|
+
if (page.guide) console.log(` Details: ${page.guide}`);
|
|
1024
1365
|
}
|
|
1025
1366
|
if (stage.operatingRecords?.length) {
|
|
1026
1367
|
console.log("Operating record guides:");
|
|
1027
1368
|
for (const record of stage.operatingRecords) {
|
|
1028
|
-
console.log(` ${record.type}\t${record.
|
|
1369
|
+
console.log(` ${record.type}\t${record.summary}\t${record.guide}`);
|
|
1029
1370
|
}
|
|
1030
1371
|
}
|
|
1031
1372
|
console.log("Commands:");
|
|
@@ -1049,6 +1390,7 @@ function selectProgramPathOutput(result, flags) {
|
|
|
1049
1390
|
function summarizeProgramPath(result) {
|
|
1050
1391
|
return {
|
|
1051
1392
|
schemaVersion: result.schemaVersion,
|
|
1393
|
+
dataModelVersion: result.dataModelVersion,
|
|
1052
1394
|
asOf: result.asOf,
|
|
1053
1395
|
currentStep: result.currentStep,
|
|
1054
1396
|
evidenceReady: result.evidenceReady,
|
|
@@ -1069,6 +1411,7 @@ function nextProgramPath(result) {
|
|
|
1069
1411
|
const nextAction = stage?.nextActions[0];
|
|
1070
1412
|
return {
|
|
1071
1413
|
schemaVersion: result.schemaVersion,
|
|
1414
|
+
dataModelVersion: result.dataModelVersion,
|
|
1072
1415
|
asOf: result.asOf,
|
|
1073
1416
|
currentStep: result.currentStep,
|
|
1074
1417
|
evidenceReady: result.evidenceReady,
|
|
@@ -1151,6 +1494,20 @@ function printProgramPathOutput(result, flags) {
|
|
|
1151
1494
|
printProgramPath(result);
|
|
1152
1495
|
}
|
|
1153
1496
|
|
|
1497
|
+
function printWorkflow(result) {
|
|
1498
|
+
console.log(`Workflow contract v${result.contractVersion}, model v${result.dataModelVersion}`);
|
|
1499
|
+
for (const [name, assessment] of Object.entries(result.assessments)) {
|
|
1500
|
+
console.log(`${String(assessment.status).toUpperCase()}\t${name}\t${assessment.message}`);
|
|
1501
|
+
}
|
|
1502
|
+
console.log(`\n${result.counts.findings.ready || 0} ready findings, ${result.counts.workItems.overdue || 0} overdue Work Items`);
|
|
1503
|
+
if (result.recommended) {
|
|
1504
|
+
console.log(`Next: ${result.recommended.title}`);
|
|
1505
|
+
if (result.recommended.message) console.log(` ${result.recommended.message}`);
|
|
1506
|
+
const command = result.recommended.nextAction?.command || result.recommended.actions?.[0]?.command;
|
|
1507
|
+
if (command) console.log(` ${command}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1154
1511
|
function summarizeProgramReadiness(result) {
|
|
1155
1512
|
const ownership = result.stages
|
|
1156
1513
|
.flatMap((stage) => stage.items)
|