filegrc 0.3.4 → 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 +24 -6
- package/model/index.js +41 -3
- package/model/v1.json +81 -47
- package/model/v2.json +8022 -0
- package/model/v3.json +9391 -0
- package/package.json +2 -2
- package/src/agent.js +89 -8
- package/src/appointments.js +19 -0
- package/src/audit-preparation.js +109 -65
- package/src/audit-transition.js +96 -0
- package/src/batch-review.js +109 -0
- package/src/cli.js +563 -148
- package/src/collection-review.js +185 -0
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +149 -77
- package/src/external-reviewer.js +165 -0
- package/src/files.js +267 -29
- package/src/git.js +239 -41
- package/src/index.js +41 -7
- package/src/model-docs.js +103 -7
- package/src/model-migration.js +1958 -0
- package/src/mutation.js +42 -0
- package/src/obligations.js +502 -95
- package/src/parties.js +17 -2
- package/src/program-lifecycle.js +1 -0
- package/src/program-path.js +70 -60
- package/src/program-readiness.js +470 -130
- package/src/reconciliation.js +277 -0
- package/src/resource-status.js +17 -0
- package/src/server.js +347 -48
- package/src/setup.js +57 -26
- package/src/source-coverage.js +61 -0
- package/src/state.js +122 -25
- package/src/timing.js +41 -0
- package/src/validate.js +707 -44
- package/src/web.js +1440 -304
- package/src/workflow.js +1595 -0
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/src/cli.js
CHANGED
|
@@ -4,9 +4,19 @@ 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
|
-
import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
|
|
10
20
|
import {
|
|
11
21
|
addEvidenceAttachment,
|
|
12
22
|
createResource,
|
|
@@ -15,17 +25,27 @@ import {
|
|
|
15
25
|
updateResource
|
|
16
26
|
} from "./files.js";
|
|
17
27
|
import { generateModelDocumentation } from "./model-docs.js";
|
|
28
|
+
import { migrateModel, planModelMigration } from "./model-migration.js";
|
|
29
|
+
import { normalizeResourceMutation } from "./mutation.js";
|
|
18
30
|
import {
|
|
19
31
|
completeObligationAction,
|
|
20
32
|
completeObligationEvent,
|
|
21
33
|
completeObligationOccurrence,
|
|
22
34
|
createObligationEvent,
|
|
23
|
-
planObligations
|
|
35
|
+
planObligations,
|
|
36
|
+
scaffoldObligationCompletion
|
|
24
37
|
} from "./obligations.js";
|
|
38
|
+
import {
|
|
39
|
+
planExternalReviewerGovernance,
|
|
40
|
+
scaffoldExternalReviewerGovernance,
|
|
41
|
+
setupExternalReviewerGovernance
|
|
42
|
+
} from "./external-reviewer.js";
|
|
25
43
|
import { relativeToWorkspace, resolveDataPath } from "./paths.js";
|
|
26
|
-
import { buildAgentProgramPath
|
|
27
|
-
import { assessProgramReadiness } from "./program-readiness.js";
|
|
44
|
+
import { buildAgentProgramPath } from "./program-path.js";
|
|
45
|
+
import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
46
|
+
import { applyReconciliation, planReconciliation } from "./reconciliation.js";
|
|
28
47
|
import { markdownEntries } from "./resource-markdown.js";
|
|
48
|
+
import { effectiveResourceStatus } from "./resource-status.js";
|
|
29
49
|
import { searchResources } from "./search.js";
|
|
30
50
|
import { serveWorkspace } from "./server.js";
|
|
31
51
|
import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
@@ -34,9 +54,16 @@ import { createAppState } from "./state.js";
|
|
|
34
54
|
import { currentCalendarDate } from "./time.js";
|
|
35
55
|
import { validateWorkspace } from "./validate.js";
|
|
36
56
|
import { loadWorkspace } from "./workspace.js";
|
|
57
|
+
import {
|
|
58
|
+
assessWorkflow,
|
|
59
|
+
buildWorkflowDelta,
|
|
60
|
+
previewWorkflowMutation,
|
|
61
|
+
workflowForResource
|
|
62
|
+
} from "./workflow.js";
|
|
37
63
|
|
|
38
64
|
const BOOLEAN_FLAGS = new Set([
|
|
39
65
|
"allow-non-authoritative-writes",
|
|
66
|
+
"apply",
|
|
40
67
|
"check-docs",
|
|
41
68
|
"complete",
|
|
42
69
|
"current",
|
|
@@ -47,7 +74,10 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
47
74
|
"next",
|
|
48
75
|
"preview",
|
|
49
76
|
"require-ready",
|
|
77
|
+
"require-healthy",
|
|
78
|
+
"scaffold",
|
|
50
79
|
"summary",
|
|
80
|
+
"workflow",
|
|
51
81
|
"write-docs",
|
|
52
82
|
"yes"
|
|
53
83
|
]);
|
|
@@ -89,19 +119,19 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
89
119
|
...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
|
|
90
120
|
...(flags.owner !== undefined ? { ownerId: flags.owner } : {}),
|
|
91
121
|
...(flags.criticality !== undefined ? { criticality: flags.criticality } : {}),
|
|
92
|
-
...(flags.classification !== undefined ? {
|
|
122
|
+
...(flags.classification !== undefined ? { classificationId: flags.classification } : {}),
|
|
93
123
|
...(flags["internet-exposed"] !== undefined ? { internetExposed: flags["internet-exposed"] } : {}),
|
|
94
124
|
...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
|
|
95
125
|
...(flags.draft ? { draft: true } : {})
|
|
96
126
|
});
|
|
97
127
|
const result = flags.preview
|
|
98
128
|
? await planWorkspaceSetup(root, setupInput)
|
|
99
|
-
: await setupWorkspace(root, setupInput);
|
|
129
|
+
: await withWorkflowDelta(root, () => setupWorkspace(root, setupInput));
|
|
100
130
|
const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
|
|
101
131
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
102
132
|
else if (flags.preview) {
|
|
103
133
|
console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
|
|
104
|
-
console.log("No controls will be linked and no evidence
|
|
134
|
+
console.log("No controls will be linked and no evidence records will be created.");
|
|
105
135
|
}
|
|
106
136
|
else {
|
|
107
137
|
console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
|
|
@@ -144,6 +174,36 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
144
174
|
else console.log(source);
|
|
145
175
|
return;
|
|
146
176
|
}
|
|
177
|
+
if (command === "migrate") {
|
|
178
|
+
const targetModel = String(flags["to-model"] || "");
|
|
179
|
+
if (!["2", "3"].includes(targetModel)) throw new Error("Pass --to-model 2 or --to-model 3.");
|
|
180
|
+
const options = {
|
|
181
|
+
jobTitle: flags["job-title"],
|
|
182
|
+
startsOn: flags["starts-on"],
|
|
183
|
+
targetModelVersion: targetModel
|
|
184
|
+
};
|
|
185
|
+
const plan = await planModelMigration(root, options);
|
|
186
|
+
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
187
|
+
throw new Error(`Review migrate --to-model ${targetModel} --preview --json, then pass --yes to apply the migration.`);
|
|
188
|
+
}
|
|
189
|
+
const result = flags.preview
|
|
190
|
+
? plan
|
|
191
|
+
: plan.sourceModelVersion !== plan.targetModelVersion
|
|
192
|
+
? await migrateModel(root, options)
|
|
193
|
+
: { ...plan, applied: false };
|
|
194
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
195
|
+
else if (result.sourceModelVersion === result.targetModelVersion) {
|
|
196
|
+
console.log(`Workspace already uses model v${result.targetModelVersion}.`);
|
|
197
|
+
} else if (flags.preview) {
|
|
198
|
+
console.log(`Model migration preview: create ${result.summary.create}; update ${result.summary.update}.`);
|
|
199
|
+
console.log(result.ready
|
|
200
|
+
? "Ready to apply. Rerun with --yes."
|
|
201
|
+
: `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
147
207
|
if (command === "describe") {
|
|
148
208
|
const loaded = await loadWorkspace(root);
|
|
149
209
|
const type = positionals[0];
|
|
@@ -184,6 +244,71 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
184
244
|
else printProgramPathOutput(output, flags);
|
|
185
245
|
return output;
|
|
186
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
|
+
}
|
|
187
312
|
if (command === "scaffold") {
|
|
188
313
|
const loaded = await loadWorkspace(root);
|
|
189
314
|
const type = positionals[0];
|
|
@@ -195,12 +320,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
195
320
|
const loaded = await loadWorkspace(root);
|
|
196
321
|
const type = positionals[0];
|
|
197
322
|
if (type && !loaded.model.resources[type]) throw new Error(`Unknown resource type "${type}".`);
|
|
323
|
+
const asOf = currentCalendarDate(loaded.workspace.timezone);
|
|
198
324
|
const records = loaded.resources
|
|
199
325
|
.filter((record) => !type || record.type === type)
|
|
200
|
-
.sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
326
|
+
.sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
|
|
327
|
+
.map((record) => {
|
|
328
|
+
const effectiveStatus = effectiveResourceStatus(record, asOf);
|
|
329
|
+
return effectiveStatus && effectiveStatus !== record.status
|
|
330
|
+
? { ...record, effectiveStatus }
|
|
331
|
+
: record;
|
|
332
|
+
});
|
|
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));
|
|
340
|
+
else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.effectiveStatus ?? record.status ?? ""}\t${record.title}`);
|
|
341
|
+
return output;
|
|
204
342
|
}
|
|
205
343
|
if (command === "search") {
|
|
206
344
|
const loaded = await loadWorkspace(root);
|
|
@@ -217,11 +355,12 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
217
355
|
from: flags.from,
|
|
218
356
|
through: flags.through,
|
|
219
357
|
now: flags.now,
|
|
220
|
-
includeComplete: Boolean(flags.complete)
|
|
358
|
+
includeComplete: Boolean(flags.complete),
|
|
359
|
+
model: loaded.model
|
|
221
360
|
});
|
|
222
361
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
223
362
|
else {
|
|
224
|
-
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`);
|
|
225
364
|
for (const item of result.items) {
|
|
226
365
|
const deadline = item.dueWindowEndAt || item.dueWindowEnd;
|
|
227
366
|
if (!deadline) throw new Error(`Planned work "${item.title}" is missing a deadline.`);
|
|
@@ -230,13 +369,18 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
230
369
|
item.dueWindowStartAt || item.dueWindowStart,
|
|
231
370
|
deadline,
|
|
232
371
|
item.title,
|
|
233
|
-
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`
|
|
234
378
|
].join("\t"));
|
|
235
379
|
}
|
|
236
380
|
if (result.triggers.length) {
|
|
237
381
|
console.log("\nPolicy Events:");
|
|
238
382
|
for (const trigger of result.triggers) {
|
|
239
|
-
console.log(`${trigger.programStatus.toUpperCase()}\t${
|
|
383
|
+
console.log(`${trigger.programStatus.toUpperCase()}\t${trigger.title} (${trigger.eventType})\t${trigger.steps.length} Work Queue ${trigger.steps.length === 1 ? "task" : "tasks"}`);
|
|
240
384
|
for (const step of trigger.steps) {
|
|
241
385
|
const owners = step.ownerIds.length ? step.ownerIds.join(",") : "unassigned";
|
|
242
386
|
const proof = step.completionResourceTypes.length ? step.completionResourceTypes.join("|") : "not specified";
|
|
@@ -262,7 +406,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
262
406
|
}
|
|
263
407
|
else {
|
|
264
408
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
|
|
265
|
-
console.log(
|
|
409
|
+
console.log(
|
|
410
|
+
`${result.target.label}`
|
|
411
|
+
+ (result.target.candidateCoverage?.kind === "range"
|
|
412
|
+
? `, candidate period starts ${result.target.candidateCoverage.startsOn}`
|
|
413
|
+
: result.target.candidateCoverage?.kind === "as-of"
|
|
414
|
+
? `, candidate as-of date ${result.target.candidateCoverage.on}`
|
|
415
|
+
: "")
|
|
416
|
+
);
|
|
266
417
|
for (const stage of result.stages) {
|
|
267
418
|
console.log(`\n${stage.title}`);
|
|
268
419
|
for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
@@ -274,33 +425,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
274
425
|
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
275
426
|
return output;
|
|
276
427
|
}
|
|
277
|
-
if (command === "evidence-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const plan = planEvidenceTestDrafts(loaded);
|
|
281
|
-
const result = {
|
|
282
|
-
schemaVersion: 1,
|
|
283
|
-
preview: true,
|
|
284
|
-
total: plan.length,
|
|
285
|
-
create: plan.filter(({ existing }) => !existing).map((item) => ({
|
|
286
|
-
familyId: item.familyId,
|
|
287
|
-
title: item.title,
|
|
288
|
-
testEvidenceKind: item.testEvidenceKind,
|
|
289
|
-
controlIds: item.controlIds
|
|
290
|
-
})),
|
|
291
|
-
existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
|
|
292
|
-
id: existing.id,
|
|
293
|
-
title: existing.title,
|
|
294
|
-
status: existing.status
|
|
295
|
-
}))
|
|
296
|
-
};
|
|
297
|
-
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
298
|
-
else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
|
|
299
|
-
return result;
|
|
300
|
-
}
|
|
301
|
-
const result = await ensureEvidenceTestDrafts(root);
|
|
428
|
+
if (command === "evidence-map") {
|
|
429
|
+
const loaded = await loadWorkspace(root);
|
|
430
|
+
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"] });
|
|
302
431
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
303
|
-
else
|
|
432
|
+
else {
|
|
433
|
+
console.log(`${result.status.toUpperCase()}: ${result.counts.complete} mapped, ${result.counts.action} need action`);
|
|
434
|
+
for (const item of result.items) {
|
|
435
|
+
console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
436
|
+
if (item.status !== "action") continue;
|
|
437
|
+
if (item.sourceKinds?.length) console.log(` Source role: ${item.sourceKinds.join(" or ")}`);
|
|
438
|
+
for (const source of item.sourceSystemChecks || []) {
|
|
439
|
+
const missing = Object.entries(source.checks)
|
|
440
|
+
.filter(([, passed]) => !passed)
|
|
441
|
+
.map(([name]) => evidenceSourceCheckName(name));
|
|
442
|
+
if (missing.length) console.log(` ${source.sourceSystemId}: ${missing.join(", ")}`);
|
|
443
|
+
}
|
|
444
|
+
if (item.commands?.length) console.log(` Next: ${item.commands[0]}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
304
447
|
return result;
|
|
305
448
|
}
|
|
306
449
|
if (command === "audit-readiness") {
|
|
@@ -322,24 +465,132 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
322
465
|
if (command === "prepare-audit") {
|
|
323
466
|
const auditId = positionals[0] || flags.audit;
|
|
324
467
|
if (!auditId) throw new Error("An audit ID is required.");
|
|
325
|
-
const result = await prepareAuditWorkspace(root, { auditId });
|
|
468
|
+
const result = await withWorkflowDelta(root, () => prepareAuditWorkspace(root, { auditId }));
|
|
326
469
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
327
470
|
else console.log(`Prepared ${result.auditId}: linked ${result.linkedDocumentIds.length} management documents and created ${result.createdPopulationIds.length} population records.`);
|
|
328
471
|
return result;
|
|
329
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
|
+
}
|
|
330
577
|
if (command === "trigger") {
|
|
331
|
-
const result = await createObligationEvent(root, {
|
|
578
|
+
const result = await withWorkflowDelta(root, () => createObligationEvent(root, {
|
|
332
579
|
eventType: positionals[0],
|
|
333
580
|
occurredOn: flags["occurred-on"],
|
|
334
581
|
occurredAt: flags["occurred-at"],
|
|
582
|
+
riskLevel: flags["risk-level"],
|
|
335
583
|
subjectResourceIds: String(flags.subject || "").split(",").map((value) => value.trim()).filter(Boolean),
|
|
336
584
|
title: flags.title
|
|
337
|
-
});
|
|
585
|
+
}));
|
|
338
586
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
339
587
|
else {
|
|
340
588
|
console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
|
|
341
589
|
console.log(`Event: obligation-event/${result.event.id}`);
|
|
342
|
-
for (const action of result.actions)
|
|
590
|
+
for (const action of result.actions) {
|
|
591
|
+
const deadline = action.completionWindow?.dueAt || action.completionWindow?.dueOn;
|
|
592
|
+
console.log(`Task: action-item/${action.id}\t${action.title}\t${deadline}`);
|
|
593
|
+
}
|
|
343
594
|
}
|
|
344
595
|
return result;
|
|
345
596
|
}
|
|
@@ -391,8 +642,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
391
642
|
console.log(JSON.stringify(mutation, null, 2));
|
|
392
643
|
return mutation;
|
|
393
644
|
}
|
|
394
|
-
|
|
395
|
-
|
|
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;
|
|
396
653
|
}
|
|
397
654
|
if (command === "references") {
|
|
398
655
|
const loaded = await loadWorkspace(root);
|
|
@@ -406,36 +663,65 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
406
663
|
}
|
|
407
664
|
return result;
|
|
408
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
|
+
}
|
|
409
678
|
if (command === "create") {
|
|
410
679
|
const mutation = await readMutation(positionals[0]);
|
|
411
|
-
const result = await createResource(root, mutation.record, { content: mutation.content });
|
|
412
|
-
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));
|
|
413
682
|
else console.log(`Created ${result.record.type}/${result.record.id}`);
|
|
414
683
|
return result;
|
|
415
684
|
}
|
|
416
685
|
if (command === "complete") {
|
|
417
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
|
+
}
|
|
418
696
|
const mutation = await readMutation(file);
|
|
419
|
-
const result = await completeObligationOccurrence(root, {
|
|
697
|
+
const result = await withWorkflowDelta(root, () => completeObligationOccurrence(root, {
|
|
420
698
|
obligationId,
|
|
421
699
|
record: mutation.record,
|
|
422
700
|
content: mutation.content,
|
|
423
|
-
expectedRevision: flags
|
|
424
|
-
});
|
|
701
|
+
expectedRevision: expectedRevision(flags, mutation, `obligation/${obligationId}`)
|
|
702
|
+
}));
|
|
425
703
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
426
704
|
else console.log(`Created ${result.created.type}/${result.created.id} and linked it to obligation/${obligationId}`);
|
|
427
705
|
return result;
|
|
428
706
|
}
|
|
429
707
|
if (command === "complete-action") {
|
|
430
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
|
+
}
|
|
431
717
|
const mutation = await readMutation(file);
|
|
432
|
-
const result = await completeObligationAction(root, {
|
|
718
|
+
const result = await withWorkflowDelta(root, () => completeObligationAction(root, {
|
|
433
719
|
actionItemId,
|
|
434
720
|
completedOn: flags["completed-on"],
|
|
435
721
|
record: mutation.record,
|
|
436
722
|
content: mutation.content,
|
|
437
|
-
expectedRevision: flags
|
|
438
|
-
});
|
|
723
|
+
expectedRevision: expectedRevision(flags, mutation, `action-item/${actionItemId}`)
|
|
724
|
+
}));
|
|
439
725
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
440
726
|
else console.log(`Created ${result.created.type}/${result.created.id}, linked it to action-item/${actionItemId}, and marked the action done.`);
|
|
441
727
|
return result;
|
|
@@ -443,24 +729,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
443
729
|
if (command === "complete-event") {
|
|
444
730
|
const eventId = positionals[0];
|
|
445
731
|
if (!eventId) throw new Error("A Policy Event ID is required.");
|
|
446
|
-
const result = await completeObligationEvent(root, {
|
|
732
|
+
const result = await withWorkflowDelta(root, () => completeObligationEvent(root, {
|
|
447
733
|
eventId,
|
|
448
734
|
completedOn: flags["completed-on"],
|
|
449
|
-
expectedRevision: flags
|
|
450
|
-
});
|
|
451
|
-
if (flags.json) console.log(JSON.stringify(
|
|
735
|
+
expectedRevision: requireExpectedRevision(flags, `obligation-event/${eventId}`)
|
|
736
|
+
}));
|
|
737
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
452
738
|
else console.log(`Marked obligation-event/${eventId} complete.`);
|
|
453
739
|
return result;
|
|
454
740
|
}
|
|
455
741
|
if (command === "update") {
|
|
456
742
|
const [type, id, file] = positionals;
|
|
457
|
-
const mutation = await readMutation(file);
|
|
458
|
-
const result = await updateResource(root, type, id, mutation.record, {
|
|
743
|
+
const mutation = await readMutation(file, { requireRevision: true });
|
|
744
|
+
const result = await withWorkflowDelta(root, () => updateResource(root, type, id, mutation.record, {
|
|
459
745
|
content: mutation.content,
|
|
460
746
|
expectedRevision: mutation.revision,
|
|
461
|
-
expectedContentRevisions: mutation.contentRevisions
|
|
462
|
-
|
|
463
|
-
|
|
747
|
+
expectedContentRevisions: mutation.contentRevisions,
|
|
748
|
+
requireExpectedContentRevisions: true
|
|
749
|
+
}));
|
|
750
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
464
751
|
else console.log(`Updated ${result.record.type}/${result.record.id}`);
|
|
465
752
|
return result;
|
|
466
753
|
}
|
|
@@ -480,14 +767,21 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
480
767
|
const state = await createAppState(root);
|
|
481
768
|
const stateEntry = state.resources.find((item) => item.record.type === type && item.record.id === id);
|
|
482
769
|
const existingContentRevision = stateEntry.content?.[slot.name]?.revision;
|
|
483
|
-
await updateResource(root, type, id, record, {
|
|
770
|
+
const mutationResult = await withWorkflowDelta(root, () => updateResource(root, type, id, record, {
|
|
484
771
|
content: { [slot.name]: source },
|
|
485
772
|
expectedRevision: stateEntry.revision,
|
|
486
773
|
expectedContentRevisions: existingContentRevision
|
|
487
774
|
? { [slot.path]: flags["expected-revision"] ?? existingContentRevision }
|
|
488
775
|
: undefined
|
|
489
|
-
});
|
|
490
|
-
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
|
+
};
|
|
491
785
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
492
786
|
else console.log(`Updated ${result.path}`);
|
|
493
787
|
return result;
|
|
@@ -507,14 +801,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
507
801
|
if (command === "attach") {
|
|
508
802
|
const [evidenceId, sourcePath] = positionals;
|
|
509
803
|
if (!evidenceId || !sourcePath) throw new Error("An evidence ID and source file are required.");
|
|
510
|
-
const result = await addEvidenceAttachment(root, evidenceId, sourcePath, {
|
|
804
|
+
const result = await withWorkflowDelta(root, () => addEvidenceAttachment(root, evidenceId, sourcePath, {
|
|
511
805
|
name: flags.name,
|
|
512
|
-
expectedRevision: flags
|
|
513
|
-
});
|
|
806
|
+
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
807
|
+
}));
|
|
514
808
|
const output = {
|
|
515
809
|
evidenceId,
|
|
516
810
|
path: `data/${result.dataRelativePath}`,
|
|
517
|
-
filePaths: result.record.filePaths
|
|
811
|
+
filePaths: result.record.filePaths,
|
|
812
|
+
workflowDelta: result.workflowDelta
|
|
518
813
|
};
|
|
519
814
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
520
815
|
else console.log(`Attached ${output.path} to evidence/${evidenceId}`);
|
|
@@ -524,13 +819,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
524
819
|
const [evidenceId, attachment] = positionals;
|
|
525
820
|
if (!evidenceId || !attachment) throw new Error("An evidence ID and attachment name are required.");
|
|
526
821
|
if (!flags.yes) throw new Error("Pass --yes to confirm attachment removal.");
|
|
527
|
-
const result = await removeEvidenceAttachment(root, evidenceId, attachment, {
|
|
528
|
-
expectedRevision: flags
|
|
529
|
-
});
|
|
822
|
+
const result = await withWorkflowDelta(root, () => removeEvidenceAttachment(root, evidenceId, attachment, {
|
|
823
|
+
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
824
|
+
}));
|
|
530
825
|
const output = {
|
|
531
826
|
evidenceId,
|
|
532
827
|
removed: `data/${result.dataRelativePath}`,
|
|
533
|
-
filePaths: result.record.filePaths ?? []
|
|
828
|
+
filePaths: result.record.filePaths ?? [],
|
|
829
|
+
workflowDelta: result.workflowDelta
|
|
534
830
|
};
|
|
535
831
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
536
832
|
else console.log(`Detached and removed ${output.removed} from evidence/${evidenceId}`);
|
|
@@ -539,9 +835,13 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
539
835
|
if (command === "delete") {
|
|
540
836
|
const [type, id] = positionals;
|
|
541
837
|
if (!flags.yes) throw new Error("Pass --yes to confirm deletion. Preserve historical records unless this is a mistake or uncommitted draft.");
|
|
542
|
-
await deleteResource(root, type, id, {
|
|
543
|
-
|
|
544
|
-
|
|
838
|
+
const result = await withWorkflowDelta(root, () => deleteResource(root, type, id, {
|
|
839
|
+
expectedRevision: requireExpectedRevision(flags, `${type}/${id}`)
|
|
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;
|
|
545
845
|
}
|
|
546
846
|
throw new Error(`Unknown command "${command}". Run filegrc help.`);
|
|
547
847
|
}
|
|
@@ -567,37 +867,36 @@ function parseArgs(args) {
|
|
|
567
867
|
return { positionals, flags };
|
|
568
868
|
}
|
|
569
869
|
|
|
570
|
-
async function
|
|
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
|
+
|
|
880
|
+
async function readMutation(path, options = {}) {
|
|
571
881
|
if (!path) throw new Error("A JSON file path or - is required.");
|
|
572
882
|
const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (!parsed.record || Array.isArray(parsed.record) || typeof parsed.record !== "object") {
|
|
581
|
-
throw new Error("Mutation record must be a JSON object.");
|
|
582
|
-
}
|
|
583
|
-
if (parsed.content !== undefined && (Array.isArray(parsed.content) || typeof parsed.content !== "object" || parsed.content === null)) {
|
|
584
|
-
throw new Error("Mutation content must be an object keyed by Markdown slot.");
|
|
585
|
-
}
|
|
586
|
-
if (parsed.revision !== undefined && typeof parsed.revision !== "string") {
|
|
587
|
-
throw new Error("Mutation revision must be a string.");
|
|
883
|
+
return normalizeResourceMutation(JSON.parse(source), options);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function requireExpectedRevision(flags, target) {
|
|
887
|
+
const revision = flags["expected-revision"];
|
|
888
|
+
if (typeof revision !== "string" || revision.length === 0) {
|
|
889
|
+
throw new Error(`--expected-revision is required when changing ${target}. Reload the resource and try again.`);
|
|
588
890
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
891
|
+
return revision;
|
|
892
|
+
}
|
|
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.`);
|
|
594
898
|
}
|
|
595
|
-
return
|
|
596
|
-
record: parsed.record,
|
|
597
|
-
content: parsed.content,
|
|
598
|
-
revision: parsed.revision,
|
|
599
|
-
contentRevisions: parsed.contentRevisions
|
|
600
|
-
};
|
|
899
|
+
return revision;
|
|
601
900
|
}
|
|
602
901
|
|
|
603
902
|
async function readSetupPayload(path) {
|
|
@@ -643,11 +942,11 @@ async function completeInteractiveSetup(root, payload) {
|
|
|
643
942
|
activePeople[0].id
|
|
644
943
|
);
|
|
645
944
|
result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
|
|
646
|
-
result.
|
|
945
|
+
result.classificationId ||= classifications.length
|
|
647
946
|
? await askChoice(
|
|
648
947
|
"Data classification",
|
|
649
948
|
classifications,
|
|
650
|
-
classifications.includes("
|
|
949
|
+
classifications.includes("confidential") ? "confidential" : classifications[0]
|
|
651
950
|
)
|
|
652
951
|
: await askRequired("Data classification");
|
|
653
952
|
if (result.internetExposed === undefined) {
|
|
@@ -690,31 +989,44 @@ Usage:
|
|
|
690
989
|
filegrc build [root] [--output .filegrc/site]
|
|
691
990
|
filegrc validate [root] [--json]
|
|
692
991
|
filegrc model [--json|--write-docs|--check-docs]
|
|
992
|
+
filegrc migrate --to-model <2|3> [--preview] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
693
993
|
filegrc describe <resource-type>
|
|
694
994
|
filegrc types [--json]
|
|
695
995
|
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
696
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]
|
|
697
1000
|
filegrc scaffold <resource-type> --title text [--id resource-id]
|
|
698
|
-
filegrc list [resource-type] [--json]
|
|
1001
|
+
filegrc list [resource-type] [--workflow] [--json]
|
|
699
1002
|
filegrc search <query> [--type resource-type] [--json]
|
|
700
1003
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
701
1004
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
702
|
-
filegrc evidence-
|
|
1005
|
+
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
703
1006
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
704
1007
|
filegrc prepare-audit <audit-id> [--json]
|
|
705
|
-
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]
|
|
706
1015
|
filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
707
1016
|
filegrc get [resource-type] <id> [--mutation]
|
|
708
1017
|
filegrc references <id> [--json]
|
|
709
|
-
filegrc
|
|
1018
|
+
filegrc preview-mutation <preview.json|-> [--json]
|
|
1019
|
+
filegrc create <mutation.json|-> [--json]
|
|
1020
|
+
filegrc complete <obligation-id> --scaffold --window-start YYYY-MM-DD [--completed-on YYYY-MM-DD]
|
|
710
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]
|
|
711
1023
|
filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD [--expected-revision hash] [--json]
|
|
712
|
-
filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD
|
|
713
|
-
filegrc update <resource-type> <id> <
|
|
1024
|
+
filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD --expected-revision hash [--json]
|
|
1025
|
+
filegrc update <resource-type> <id> <mutation.json|-> [--json]
|
|
714
1026
|
filegrc content <resource-type> <id> [slot] [--write markdown-file|-] [--expected-revision hash] [--json]
|
|
715
|
-
filegrc attach <evidence-id> <source-file> [--name file-name] [--
|
|
716
|
-
filegrc detach <evidence-id> <attachment-name> --yes
|
|
717
|
-
filegrc delete <resource-type> <id> --yes
|
|
1027
|
+
filegrc attach <evidence-id> <source-file> --expected-revision hash [--name file-name] [--json]
|
|
1028
|
+
filegrc detach <evidence-id> <attachment-name> --yes --expected-revision hash [--json]
|
|
1029
|
+
filegrc delete <resource-type> <id> --yes --expected-revision hash
|
|
718
1030
|
|
|
719
1031
|
All commands accept --root <workspace>. Writes never create Git commits.`);
|
|
720
1032
|
}
|
|
@@ -751,7 +1063,7 @@ Options:
|
|
|
751
1063
|
--boundary <description> boundary
|
|
752
1064
|
--owner <person-id> ownerId
|
|
753
1065
|
--criticality <level> low, medium, high, or critical
|
|
754
|
-
--classification <
|
|
1066
|
+
--classification <level> public, internal, confidential, or restricted
|
|
755
1067
|
--internet-exposed <bool> true or false
|
|
756
1068
|
--program-goal <goal> none, readiness, type-1, or type-2
|
|
757
1069
|
--draft Save the service boundary as planned
|
|
@@ -762,14 +1074,36 @@ Options:
|
|
|
762
1074
|
--help Show this help`);
|
|
763
1075
|
return;
|
|
764
1076
|
}
|
|
1077
|
+
if (command === "migrate") {
|
|
1078
|
+
console.log(`Usage:
|
|
1079
|
+
filegrc migrate --to-model <2|3> [options]
|
|
1080
|
+
|
|
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.
|
|
1085
|
+
|
|
1086
|
+
Options:
|
|
1087
|
+
--to-model <version> Required target model; 2 for legacy v1 migration, 3 for the active model
|
|
1088
|
+
--preview Show the complete atomic record plan without writing
|
|
1089
|
+
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
1090
|
+
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
1091
|
+
--yes Apply the reviewed migration
|
|
1092
|
+
--json Print the plan or result as JSON
|
|
1093
|
+
--root <path> Workspace path
|
|
1094
|
+
--help Show this help
|
|
1095
|
+
|
|
1096
|
+
Start with:
|
|
1097
|
+
npx filegrc migrate --to-model 3 --preview --json`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
765
1100
|
if (command === "program-readiness") {
|
|
766
1101
|
console.log(`Usage:
|
|
767
1102
|
filegrc program-readiness [options]
|
|
768
1103
|
|
|
769
1104
|
Report whether management has defined scope, activated policies, implemented
|
|
770
|
-
controls,
|
|
771
|
-
|
|
772
|
-
is required.
|
|
1105
|
+
controls, and mapped every selected control to a configured authoritative evidence
|
|
1106
|
+
source. No audit ID or CPA firm is required.
|
|
773
1107
|
|
|
774
1108
|
Options:
|
|
775
1109
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
@@ -780,18 +1114,37 @@ Options:
|
|
|
780
1114
|
--help Show this help`);
|
|
781
1115
|
return;
|
|
782
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
|
+
}
|
|
783
1136
|
if (command === "program-path") {
|
|
784
1137
|
console.log(`Usage:
|
|
785
1138
|
filegrc program-path [audit-id] [options]
|
|
786
1139
|
|
|
787
|
-
Show the same
|
|
1140
|
+
Show the same five-step SOC 2 lifecycle used by the renderer. Each step includes
|
|
788
1141
|
its exact page instructions, Use and Policy Basis context, resource commands,
|
|
789
|
-
and current readiness state. Pass an audit ID to include Step
|
|
1142
|
+
and current readiness state. Pass an audit ID to include Step 5 status.
|
|
790
1143
|
|
|
791
1144
|
Options:
|
|
792
|
-
--audit <id> Audit record to use for Step
|
|
1145
|
+
--audit <id> Audit record to use for Step 5
|
|
793
1146
|
--as-of <date> Evaluate readiness on YYYY-MM-DD
|
|
794
|
-
--summary Print compact status and the first action for all
|
|
1147
|
+
--summary Print compact status and the first action for all five steps
|
|
795
1148
|
--next Print only the current step and its first action
|
|
796
1149
|
--current Print the full guide for the current step only
|
|
797
1150
|
--json Print the selected path view as JSON
|
|
@@ -799,18 +1152,18 @@ Options:
|
|
|
799
1152
|
--help Show this help`);
|
|
800
1153
|
return;
|
|
801
1154
|
}
|
|
802
|
-
if (command === "evidence-
|
|
1155
|
+
if (command === "evidence-map") {
|
|
803
1156
|
console.log(`Usage:
|
|
804
|
-
filegrc evidence-
|
|
1157
|
+
filegrc evidence-map [options]
|
|
805
1158
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1159
|
+
Inspect the evidence-source checks included in Control implementation. Each item
|
|
1160
|
+
reports the required source roles, linked Controls, authoritative source Systems,
|
|
1161
|
+
per-record checks, and exact edit commands. This diagnostic is read-only.
|
|
809
1162
|
|
|
810
1163
|
Options:
|
|
811
|
-
--
|
|
812
|
-
--json
|
|
813
|
-
--root <path>
|
|
1164
|
+
--as-of <date> Evaluate the map on YYYY-MM-DD
|
|
1165
|
+
--json Print the map as JSON
|
|
1166
|
+
--root <path> Workspace path
|
|
814
1167
|
--help Show this help`);
|
|
815
1168
|
return;
|
|
816
1169
|
}
|
|
@@ -826,27 +1179,37 @@ function agentOverview(model) {
|
|
|
826
1179
|
build: "filegrc build [root]",
|
|
827
1180
|
validate: "filegrc validate [root] --json",
|
|
828
1181
|
model: "filegrc model --json",
|
|
1182
|
+
migrate: "filegrc migrate --to-model 3 --preview --json",
|
|
829
1183
|
describe: "filegrc describe <resource-type>",
|
|
830
1184
|
types: "filegrc types --json",
|
|
831
1185
|
guide: "filegrc guide [resource-type] --json",
|
|
832
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",
|
|
833
1190
|
scaffold: "filegrc scaffold <resource-type> --title <name>",
|
|
834
1191
|
list: "filegrc list [resource-type] --json",
|
|
835
1192
|
search: "filegrc search <query> --json",
|
|
836
1193
|
obligations: "filegrc obligations --json",
|
|
837
1194
|
programReadiness: "filegrc program-readiness --json",
|
|
838
|
-
|
|
1195
|
+
evidenceMap: "filegrc evidence-map --json",
|
|
839
1196
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
840
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",
|
|
841
1203
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
842
1204
|
evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
|
|
843
1205
|
get: "filegrc get <resource-id> [--mutation]",
|
|
844
1206
|
references: "filegrc references <resource-id> --json",
|
|
845
|
-
|
|
1207
|
+
previewMutation: "filegrc preview-mutation <preview.json> --json",
|
|
1208
|
+
create: "filegrc create <mutation.json>",
|
|
846
1209
|
complete: "filegrc complete <obligation-id> <completion-mutation.json>",
|
|
847
1210
|
completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
|
|
848
1211
|
completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
|
|
849
|
-
update: "filegrc update <resource-type> <id> <
|
|
1212
|
+
update: "filegrc update <resource-type> <id> <mutation.json>",
|
|
850
1213
|
content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
|
|
851
1214
|
attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
|
|
852
1215
|
detach: "filegrc detach <evidence-id> <attachment-name> --yes",
|
|
@@ -884,6 +1247,17 @@ function printAgentGuide(result) {
|
|
|
884
1247
|
console.log(`Policy basis: ${result.policyBasis}`);
|
|
885
1248
|
console.log(`Timing: ${result.cadence}`);
|
|
886
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
|
+
}
|
|
887
1261
|
console.log("\nRequired fields:");
|
|
888
1262
|
for (const field of result.requiredAtCreation) console.log(formatGuideField(field));
|
|
889
1263
|
if (result.conditionalRequirements.length) {
|
|
@@ -922,17 +1296,36 @@ function printAgentGuide(result) {
|
|
|
922
1296
|
}
|
|
923
1297
|
console.log("\nWorkflow:");
|
|
924
1298
|
result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
|
|
1299
|
+
console.log("\nCompletion checks:");
|
|
1300
|
+
result.completionChecks.forEach((check) => console.log(`- ${check}`));
|
|
925
1301
|
}
|
|
926
1302
|
|
|
927
1303
|
function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
928
1304
|
const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
|
|
929
1305
|
const stages = buildAgentProgramPath(model).map((stage) => {
|
|
930
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
|
+
);
|
|
931
1324
|
return {
|
|
932
1325
|
...stage,
|
|
933
1326
|
status: auditReadiness?.status || "not-started",
|
|
934
1327
|
counts: auditReadiness?.counts || null,
|
|
935
|
-
nextActions:
|
|
1328
|
+
nextActions: auditAction ? [auditAction] : []
|
|
936
1329
|
};
|
|
937
1330
|
}
|
|
938
1331
|
const readinessId = stage.id === "run" ? "operation" : stage.id;
|
|
@@ -950,6 +1343,7 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
950
1343
|
const currentStep = stages.find((stage) => !["complete", "operating", "management-ready"].includes(stage.status)) || stages.at(-1);
|
|
951
1344
|
return {
|
|
952
1345
|
schemaVersion: 1,
|
|
1346
|
+
dataModelVersion: String(model.modelVersion),
|
|
953
1347
|
asOf: readiness.asOf,
|
|
954
1348
|
currentStep: { id: currentStep.id, number: currentStep.number, title: currentStep.title },
|
|
955
1349
|
evidenceReady: readiness.evidenceReady,
|
|
@@ -966,14 +1360,13 @@ function printProgramPath(result) {
|
|
|
966
1360
|
console.log(stage.summary);
|
|
967
1361
|
for (const page of stage.pages) {
|
|
968
1362
|
console.log(`${page.order ? `Step ${page.order}` : "Operating area"} · ${page.title} (${page.type || `utility:${page.utility}`})`);
|
|
969
|
-
console.log(`
|
|
970
|
-
console.log(`
|
|
971
|
-
console.log(` Policy basis: ${page.policyBasis}`);
|
|
1363
|
+
console.log(` ${page.summary}`);
|
|
1364
|
+
if (page.guide) console.log(` Details: ${page.guide}`);
|
|
972
1365
|
}
|
|
973
1366
|
if (stage.operatingRecords?.length) {
|
|
974
1367
|
console.log("Operating record guides:");
|
|
975
1368
|
for (const record of stage.operatingRecords) {
|
|
976
|
-
console.log(` ${record.type}\t${record.
|
|
1369
|
+
console.log(` ${record.type}\t${record.summary}\t${record.guide}`);
|
|
977
1370
|
}
|
|
978
1371
|
}
|
|
979
1372
|
console.log("Commands:");
|
|
@@ -997,6 +1390,7 @@ function selectProgramPathOutput(result, flags) {
|
|
|
997
1390
|
function summarizeProgramPath(result) {
|
|
998
1391
|
return {
|
|
999
1392
|
schemaVersion: result.schemaVersion,
|
|
1393
|
+
dataModelVersion: result.dataModelVersion,
|
|
1000
1394
|
asOf: result.asOf,
|
|
1001
1395
|
currentStep: result.currentStep,
|
|
1002
1396
|
evidenceReady: result.evidenceReady,
|
|
@@ -1017,6 +1411,7 @@ function nextProgramPath(result) {
|
|
|
1017
1411
|
const nextAction = stage?.nextActions[0];
|
|
1018
1412
|
return {
|
|
1019
1413
|
schemaVersion: result.schemaVersion,
|
|
1414
|
+
dataModelVersion: result.dataModelVersion,
|
|
1020
1415
|
asOf: result.asOf,
|
|
1021
1416
|
currentStep: result.currentStep,
|
|
1022
1417
|
evidenceReady: result.evidenceReady,
|
|
@@ -1069,6 +1464,15 @@ function shellArgument(value) {
|
|
|
1069
1464
|
: `'${text.replaceAll("'", "'\\''")}'`;
|
|
1070
1465
|
}
|
|
1071
1466
|
|
|
1467
|
+
function evidenceSourceCheckName(name) {
|
|
1468
|
+
return ({
|
|
1469
|
+
active: "activate source",
|
|
1470
|
+
sourceRole: "add source role",
|
|
1471
|
+
accessOwners: "add access owner",
|
|
1472
|
+
retrievalInstructions: "add retrieval instructions"
|
|
1473
|
+
})[name] || name;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1072
1476
|
function printProgramPathOutput(result, flags) {
|
|
1073
1477
|
if (flags.summary) {
|
|
1074
1478
|
console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
|
|
@@ -1090,6 +1494,20 @@ function printProgramPathOutput(result, flags) {
|
|
|
1090
1494
|
printProgramPath(result);
|
|
1091
1495
|
}
|
|
1092
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
|
+
|
|
1093
1511
|
function summarizeProgramReadiness(result) {
|
|
1094
1512
|
const ownership = result.stages
|
|
1095
1513
|
.flatMap((stage) => stage.items)
|
|
@@ -1138,13 +1556,10 @@ function summarizeProgramReadiness(result) {
|
|
|
1138
1556
|
}
|
|
1139
1557
|
|
|
1140
1558
|
function eventWindowText(window) {
|
|
1141
|
-
if (Number.isInteger(window?.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
return window.endOffsetDays === 0 ? "due on event date" : `due within ${window.endOffsetDays} days`;
|
|
1146
|
-
}
|
|
1147
|
-
return "due within 30 days";
|
|
1559
|
+
if (!Number.isInteger(window?.dueAfter)) return "deadline not configured";
|
|
1560
|
+
const unit = window.precision === "timestamp" ? "hour" : "day";
|
|
1561
|
+
if (window.dueAfter === 0) return window.precision === "timestamp" ? "due at event time" : "due on event date";
|
|
1562
|
+
return `due within ${window.dueAfter} ${unit}${window.dueAfter === 1 ? "" : "s"}`;
|
|
1148
1563
|
}
|
|
1149
1564
|
|
|
1150
1565
|
function formatGuideField(field) {
|