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/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, policyEventName } from "./program-path.js";
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 ? { dataClassification: flags.classification } : {}),
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 drafts will be created.");
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
- if (flags.json) console.log(JSON.stringify(records, null, 2));
202
- else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.status ?? ""}\t${record.title}`);
203
- return records;
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${policyEventName(trigger.eventType)} (${trigger.eventType})\t${trigger.steps.length} Work Queue ${trigger.steps.length === 1 ? "task" : "tasks"}`);
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(`${result.target.label}${result.target.candidatePeriodStart ? `, candidate period starts ${result.target.candidatePeriodStart}` : ""}`);
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-test-drafts") {
278
- if (flags.preview) {
279
- const loaded = await loadWorkspace(root);
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 console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
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) console.log(`Task: action-item/${action.id}\t${action.title}\t${action.dueWindowEndAt || action.dueWindowEnd || action.overdueAt || action.overdueOn}`);
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
- console.log(JSON.stringify(record, null, 2));
395
- return record;
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({ record: result.record }, null, 2));
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["expected-revision"]
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["expected-revision"]
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["expected-revision"]
450
- });
451
- if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
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
- if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
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 = { type, id, slot: slot.name, path: `data/${slot.path}`, written: true };
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["expected-revision"]
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["expected-revision"]
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, { expectedRevision: flags["expected-revision"] });
543
- console.log(`Deleted ${type}/${id}`);
544
- return;
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 readMutation(path) {
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
- const parsed = JSON.parse(source);
574
- if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
575
- throw new Error("A resource record or { record, content } mutation object is required.");
576
- }
577
- if (!Object.hasOwn(parsed, "record")) {
578
- return { record: parsed, content: undefined, revision: undefined, contentRevisions: undefined };
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
- if (
590
- parsed.contentRevisions !== undefined
591
- && (Array.isArray(parsed.contentRevisions) || typeof parsed.contentRevisions !== "object" || parsed.contentRevisions === null)
592
- ) {
593
- throw new Error("Mutation contentRevisions must be an object keyed by data-relative Markdown path.");
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.dataClassification ||= classifications.length
945
+ result.classificationId ||= classifications.length
647
946
  ? await askChoice(
648
947
  "Data classification",
649
948
  classifications,
650
- classifications.includes("Confidential") ? "Confidential" : classifications[0]
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-test-drafts [--preview] [--json]
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 trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
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 create <record-or-mutation.json|-> [--json]
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 [--expected-revision hash] [--json]
713
- filegrc update <resource-type> <id> <record-or-mutation.json|-> [--json]
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] [--expected-revision hash] [--json]
716
- filegrc detach <evidence-id> <attachment-name> --yes [--expected-revision hash] [--json]
717
- filegrc delete <resource-type> <id> --yes [--expected-revision hash]
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 <name> dataClassification
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, configured authoritative evidence sources, and verified test collection
771
- for external evidence without a dedicated Step 5 record. No audit ID or CPA firm
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 six-step SOC 2 lifecycle used by the renderer. Each step includes
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 6 status.
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 6
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 six steps
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-test-drafts") {
1155
+ if (command === "evidence-map") {
803
1156
  console.log(`Usage:
804
- filegrc evidence-test-drafts [options]
1157
+ filegrc evidence-map [options]
805
1158
 
806
- Preview or create missing draft External Evidence records for collection that
807
- does not already have a dedicated Step 5 operating record. Existing tests are
808
- preserved. Run after confirming applicable controls and source systems.
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
- --preview Report proposed drafts without creating them
812
- --json Print created and existing records as JSON
813
- --root <path> Workspace 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
- evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
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
- create: "filegrc create <record-or-mutation.json>",
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> <record-or-mutation.json>",
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: auditReadiness?.firstAction ? [auditReadiness.firstAction] : []
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(` Instructions: ${page.instructions}`);
970
- console.log(` Use: ${page.use}`);
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.instructions}\t${record.guide}`);
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?.endOffsetHours)) {
1142
- return window.endOffsetHours === 0 ? "due at event time" : `due within ${window.endOffsetHours} hours`;
1143
- }
1144
- if (Number.isInteger(window?.endOffsetDays)) {
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) {