filegrc 0.3.4 → 0.4.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
@@ -6,7 +6,6 @@ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldRes
6
6
  import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
7
7
  import { buildWorkspace } from "./build.js";
8
8
  import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
9
- import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
10
9
  import {
11
10
  addEvidenceAttachment,
12
11
  createResource,
@@ -15,6 +14,8 @@ import {
15
14
  updateResource
16
15
  } from "./files.js";
17
16
  import { generateModelDocumentation } from "./model-docs.js";
17
+ import { migrateModel, planModelMigration } from "./model-migration.js";
18
+ import { normalizeResourceMutation } from "./mutation.js";
18
19
  import {
19
20
  completeObligationAction,
20
21
  completeObligationEvent,
@@ -23,9 +24,10 @@ import {
23
24
  planObligations
24
25
  } from "./obligations.js";
25
26
  import { relativeToWorkspace, resolveDataPath } from "./paths.js";
26
- import { buildAgentProgramPath, policyEventName } from "./program-path.js";
27
- import { assessProgramReadiness } from "./program-readiness.js";
27
+ import { buildAgentProgramPath } from "./program-path.js";
28
+ import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
28
29
  import { markdownEntries } from "./resource-markdown.js";
30
+ import { effectiveResourceStatus } from "./resource-status.js";
29
31
  import { searchResources } from "./search.js";
30
32
  import { serveWorkspace } from "./server.js";
31
33
  import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
@@ -89,7 +91,7 @@ export async function runCli(argv = process.argv.slice(2)) {
89
91
  ...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
90
92
  ...(flags.owner !== undefined ? { ownerId: flags.owner } : {}),
91
93
  ...(flags.criticality !== undefined ? { criticality: flags.criticality } : {}),
92
- ...(flags.classification !== undefined ? { dataClassification: flags.classification } : {}),
94
+ ...(flags.classification !== undefined ? { classificationId: flags.classification } : {}),
93
95
  ...(flags["internet-exposed"] !== undefined ? { internetExposed: flags["internet-exposed"] } : {}),
94
96
  ...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
95
97
  ...(flags.draft ? { draft: true } : {})
@@ -101,7 +103,7 @@ export async function runCli(argv = process.argv.slice(2)) {
101
103
  if (flags.json) console.log(JSON.stringify(output, null, 2));
102
104
  else if (flags.preview) {
103
105
  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.");
106
+ console.log("No controls will be linked and no evidence records will be created.");
105
107
  }
106
108
  else {
107
109
  console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
@@ -144,6 +146,35 @@ export async function runCli(argv = process.argv.slice(2)) {
144
146
  else console.log(source);
145
147
  return;
146
148
  }
149
+ if (command === "migrate") {
150
+ const targetModel = String(flags["to-model"] || "");
151
+ if (targetModel !== "2") throw new Error("Pass --to-model 2.");
152
+ const options = {
153
+ jobTitle: flags["job-title"],
154
+ startsOn: flags["starts-on"]
155
+ };
156
+ const plan = await planModelMigration(root, options);
157
+ if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
158
+ throw new Error("Review migrate --to-model 2 --preview --json, then pass --yes to apply the migration.");
159
+ }
160
+ const result = flags.preview
161
+ ? plan
162
+ : plan.sourceModelVersion !== plan.targetModelVersion
163
+ ? await migrateModel(root, options)
164
+ : { ...plan, applied: false };
165
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
166
+ else if (result.sourceModelVersion === result.targetModelVersion) {
167
+ console.log(`Workspace already uses model v${result.targetModelVersion}.`);
168
+ } else if (flags.preview) {
169
+ console.log(`Model migration preview: create ${result.summary.create}; update ${result.summary.update}.`);
170
+ console.log(result.ready
171
+ ? "Ready to apply. Rerun with --yes."
172
+ : `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
173
+ } else {
174
+ console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
175
+ }
176
+ return result;
177
+ }
147
178
  if (command === "describe") {
148
179
  const loaded = await loadWorkspace(root);
149
180
  const type = positionals[0];
@@ -195,11 +226,18 @@ export async function runCli(argv = process.argv.slice(2)) {
195
226
  const loaded = await loadWorkspace(root);
196
227
  const type = positionals[0];
197
228
  if (type && !loaded.model.resources[type]) throw new Error(`Unknown resource type "${type}".`);
229
+ const asOf = currentCalendarDate(loaded.workspace.timezone);
198
230
  const records = loaded.resources
199
231
  .filter((record) => !type || record.type === type)
200
- .sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`));
232
+ .sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
233
+ .map((record) => {
234
+ const effectiveStatus = effectiveResourceStatus(record, asOf);
235
+ return effectiveStatus && effectiveStatus !== record.status
236
+ ? { ...record, effectiveStatus }
237
+ : record;
238
+ });
201
239
  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}`);
240
+ else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.effectiveStatus ?? record.status ?? ""}\t${record.title}`);
203
241
  return records;
204
242
  }
205
243
  if (command === "search") {
@@ -217,7 +255,8 @@ export async function runCli(argv = process.argv.slice(2)) {
217
255
  from: flags.from,
218
256
  through: flags.through,
219
257
  now: flags.now,
220
- includeComplete: Boolean(flags.complete)
258
+ includeComplete: Boolean(flags.complete),
259
+ model: loaded.model
221
260
  });
222
261
  if (flags.json) console.log(JSON.stringify(result, null, 2));
223
262
  else {
@@ -236,7 +275,7 @@ export async function runCli(argv = process.argv.slice(2)) {
236
275
  if (result.triggers.length) {
237
276
  console.log("\nPolicy Events:");
238
277
  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"}`);
278
+ console.log(`${trigger.programStatus.toUpperCase()}\t${trigger.title} (${trigger.eventType})\t${trigger.steps.length} Work Queue ${trigger.steps.length === 1 ? "task" : "tasks"}`);
240
279
  for (const step of trigger.steps) {
241
280
  const owners = step.ownerIds.length ? step.ownerIds.join(",") : "unassigned";
242
281
  const proof = step.completionResourceTypes.length ? step.completionResourceTypes.join("|") : "not specified";
@@ -262,7 +301,14 @@ export async function runCli(argv = process.argv.slice(2)) {
262
301
  }
263
302
  else {
264
303
  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}` : ""}`);
304
+ console.log(
305
+ `${result.target.label}`
306
+ + (result.target.candidateCoverage?.kind === "range"
307
+ ? `, candidate period starts ${result.target.candidateCoverage.startsOn}`
308
+ : result.target.candidateCoverage?.kind === "as-of"
309
+ ? `, candidate as-of date ${result.target.candidateCoverage.on}`
310
+ : "")
311
+ );
266
312
  for (const stage of result.stages) {
267
313
  console.log(`\n${stage.title}`);
268
314
  for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
@@ -274,33 +320,25 @@ export async function runCli(argv = process.argv.slice(2)) {
274
320
  if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
275
321
  return output;
276
322
  }
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);
323
+ if (command === "evidence-map") {
324
+ const loaded = await loadWorkspace(root);
325
+ const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"] });
302
326
  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.`);
327
+ else {
328
+ console.log(`${result.status.toUpperCase()}: ${result.counts.complete} mapped, ${result.counts.action} need action`);
329
+ for (const item of result.items) {
330
+ console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
331
+ if (item.status !== "action") continue;
332
+ if (item.sourceKinds?.length) console.log(` Source role: ${item.sourceKinds.join(" or ")}`);
333
+ for (const source of item.sourceSystemChecks || []) {
334
+ const missing = Object.entries(source.checks)
335
+ .filter(([, passed]) => !passed)
336
+ .map(([name]) => evidenceSourceCheckName(name));
337
+ if (missing.length) console.log(` ${source.sourceSystemId}: ${missing.join(", ")}`);
338
+ }
339
+ if (item.commands?.length) console.log(` Next: ${item.commands[0]}`);
340
+ }
341
+ }
304
342
  return result;
305
343
  }
306
344
  if (command === "audit-readiness") {
@@ -339,7 +377,10 @@ export async function runCli(argv = process.argv.slice(2)) {
339
377
  else {
340
378
  console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
341
379
  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}`);
380
+ for (const action of result.actions) {
381
+ const deadline = action.completionWindow?.dueAt || action.completionWindow?.dueOn;
382
+ console.log(`Task: action-item/${action.id}\t${action.title}\t${deadline}`);
383
+ }
343
384
  }
344
385
  return result;
345
386
  }
@@ -420,7 +461,7 @@ export async function runCli(argv = process.argv.slice(2)) {
420
461
  obligationId,
421
462
  record: mutation.record,
422
463
  content: mutation.content,
423
- expectedRevision: flags["expected-revision"]
464
+ expectedRevision: requireExpectedRevision(flags, `obligation/${obligationId}`)
424
465
  });
425
466
  if (flags.json) console.log(JSON.stringify(result, null, 2));
426
467
  else console.log(`Created ${result.created.type}/${result.created.id} and linked it to obligation/${obligationId}`);
@@ -434,7 +475,7 @@ export async function runCli(argv = process.argv.slice(2)) {
434
475
  completedOn: flags["completed-on"],
435
476
  record: mutation.record,
436
477
  content: mutation.content,
437
- expectedRevision: flags["expected-revision"]
478
+ expectedRevision: requireExpectedRevision(flags, `action-item/${actionItemId}`)
438
479
  });
439
480
  if (flags.json) console.log(JSON.stringify(result, null, 2));
440
481
  else console.log(`Created ${result.created.type}/${result.created.id}, linked it to action-item/${actionItemId}, and marked the action done.`);
@@ -446,7 +487,7 @@ export async function runCli(argv = process.argv.slice(2)) {
446
487
  const result = await completeObligationEvent(root, {
447
488
  eventId,
448
489
  completedOn: flags["completed-on"],
449
- expectedRevision: flags["expected-revision"]
490
+ expectedRevision: requireExpectedRevision(flags, `obligation-event/${eventId}`)
450
491
  });
451
492
  if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
452
493
  else console.log(`Marked obligation-event/${eventId} complete.`);
@@ -454,11 +495,12 @@ export async function runCli(argv = process.argv.slice(2)) {
454
495
  }
455
496
  if (command === "update") {
456
497
  const [type, id, file] = positionals;
457
- const mutation = await readMutation(file);
498
+ const mutation = await readMutation(file, { requireRevision: true });
458
499
  const result = await updateResource(root, type, id, mutation.record, {
459
500
  content: mutation.content,
460
501
  expectedRevision: mutation.revision,
461
- expectedContentRevisions: mutation.contentRevisions
502
+ expectedContentRevisions: mutation.contentRevisions,
503
+ requireExpectedContentRevisions: true
462
504
  });
463
505
  if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
464
506
  else console.log(`Updated ${result.record.type}/${result.record.id}`);
@@ -509,7 +551,7 @@ export async function runCli(argv = process.argv.slice(2)) {
509
551
  if (!evidenceId || !sourcePath) throw new Error("An evidence ID and source file are required.");
510
552
  const result = await addEvidenceAttachment(root, evidenceId, sourcePath, {
511
553
  name: flags.name,
512
- expectedRevision: flags["expected-revision"]
554
+ expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
513
555
  });
514
556
  const output = {
515
557
  evidenceId,
@@ -525,7 +567,7 @@ export async function runCli(argv = process.argv.slice(2)) {
525
567
  if (!evidenceId || !attachment) throw new Error("An evidence ID and attachment name are required.");
526
568
  if (!flags.yes) throw new Error("Pass --yes to confirm attachment removal.");
527
569
  const result = await removeEvidenceAttachment(root, evidenceId, attachment, {
528
- expectedRevision: flags["expected-revision"]
570
+ expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
529
571
  });
530
572
  const output = {
531
573
  evidenceId,
@@ -539,7 +581,9 @@ export async function runCli(argv = process.argv.slice(2)) {
539
581
  if (command === "delete") {
540
582
  const [type, id] = positionals;
541
583
  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"] });
584
+ await deleteResource(root, type, id, {
585
+ expectedRevision: requireExpectedRevision(flags, `${type}/${id}`)
586
+ });
543
587
  console.log(`Deleted ${type}/${id}`);
544
588
  return;
545
589
  }
@@ -567,37 +611,18 @@ function parseArgs(args) {
567
611
  return { positionals, flags };
568
612
  }
569
613
 
570
- async function readMutation(path) {
614
+ async function readMutation(path, options = {}) {
571
615
  if (!path) throw new Error("A JSON file path or - is required.");
572
616
  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.");
588
- }
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.");
617
+ return normalizeResourceMutation(JSON.parse(source), options);
618
+ }
619
+
620
+ function requireExpectedRevision(flags, target) {
621
+ const revision = flags["expected-revision"];
622
+ if (typeof revision !== "string" || revision.length === 0) {
623
+ throw new Error(`--expected-revision is required when changing ${target}. Reload the resource and try again.`);
594
624
  }
595
- return {
596
- record: parsed.record,
597
- content: parsed.content,
598
- revision: parsed.revision,
599
- contentRevisions: parsed.contentRevisions
600
- };
625
+ return revision;
601
626
  }
602
627
 
603
628
  async function readSetupPayload(path) {
@@ -643,11 +668,11 @@ async function completeInteractiveSetup(root, payload) {
643
668
  activePeople[0].id
644
669
  );
645
670
  result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
646
- result.dataClassification ||= classifications.length
671
+ result.classificationId ||= classifications.length
647
672
  ? await askChoice(
648
673
  "Data classification",
649
674
  classifications,
650
- classifications.includes("Confidential") ? "Confidential" : classifications[0]
675
+ classifications.includes("confidential") ? "confidential" : classifications[0]
651
676
  )
652
677
  : await askRequired("Data classification");
653
678
  if (result.internetExposed === undefined) {
@@ -690,6 +715,7 @@ Usage:
690
715
  filegrc build [root] [--output .filegrc/site]
691
716
  filegrc validate [root] [--json]
692
717
  filegrc model [--json|--write-docs|--check-docs]
718
+ filegrc migrate --to-model 2 [--preview] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
693
719
  filegrc describe <resource-type>
694
720
  filegrc types [--json]
695
721
  filegrc guide [resource-type] [--id resource-id] [--json]
@@ -699,22 +725,22 @@ Usage:
699
725
  filegrc search <query> [--type resource-type] [--json]
700
726
  filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
701
727
  filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
702
- filegrc evidence-test-drafts [--preview] [--json]
728
+ filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
703
729
  filegrc audit-readiness [audit-id] [--require-ready] [--json]
704
730
  filegrc prepare-audit <audit-id> [--json]
705
731
  filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
706
732
  filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
707
733
  filegrc get [resource-type] <id> [--mutation]
708
734
  filegrc references <id> [--json]
709
- filegrc create <record-or-mutation.json|-> [--json]
710
- filegrc complete <obligation-id> <completion-record.json|-> [--expected-revision hash] [--json]
711
- 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]
735
+ filegrc create <mutation.json|-> [--json]
736
+ filegrc complete <obligation-id> <completion-record.json|-> --expected-revision hash [--json]
737
+ filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD --expected-revision hash [--json]
738
+ filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD --expected-revision hash [--json]
739
+ filegrc update <resource-type> <id> <mutation.json|-> [--json]
714
740
  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]
741
+ filegrc attach <evidence-id> <source-file> --expected-revision hash [--name file-name] [--json]
742
+ filegrc detach <evidence-id> <attachment-name> --yes --expected-revision hash [--json]
743
+ filegrc delete <resource-type> <id> --yes --expected-revision hash
718
744
 
719
745
  All commands accept --root <workspace>. Writes never create Git commits.`);
720
746
  }
@@ -751,7 +777,7 @@ Options:
751
777
  --boundary <description> boundary
752
778
  --owner <person-id> ownerId
753
779
  --criticality <level> low, medium, high, or critical
754
- --classification <name> dataClassification
780
+ --classification <id> classificationId
755
781
  --internet-exposed <bool> true or false
756
782
  --program-goal <goal> none, readiness, type-1, or type-2
757
783
  --draft Save the service boundary as planned
@@ -762,14 +788,37 @@ Options:
762
788
  --help Show this help`);
763
789
  return;
764
790
  }
791
+ if (command === "migrate") {
792
+ console.log(`Usage:
793
+ filegrc migrate --to-model 2 [options]
794
+
795
+ Upgrade a model v1 workspace to model v2. The migration moves reverse
796
+ relationships to their authoritative records, converts the former Policy Owner
797
+ seed role into a dated Appointment, makes repository behavior explicit, rewrites
798
+ the moved program-page ID, removes obsolete collection-test fields and per-record
799
+ schemaVersion keys, and changes dataModelVersion last. It writes no Git commit.
800
+
801
+ Options:
802
+ --to-model <version> Required target model; currently 2
803
+ --preview Show the complete atomic record plan without writing
804
+ --job-title <title> Actual job title for the former Policy Owner seed person
805
+ --starts-on <date> Effective date of a new Policy Owner Appointment
806
+ --yes Apply the reviewed migration
807
+ --json Print the plan or result as JSON
808
+ --root <path> Workspace path
809
+ --help Show this help
810
+
811
+ Start with:
812
+ npx filegrc migrate --to-model 2 --preview --json`);
813
+ return;
814
+ }
765
815
  if (command === "program-readiness") {
766
816
  console.log(`Usage:
767
817
  filegrc program-readiness [options]
768
818
 
769
819
  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.
820
+ controls, and mapped every selected control to a configured authoritative evidence
821
+ source. No audit ID or CPA firm is required.
773
822
 
774
823
  Options:
775
824
  --as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
@@ -784,14 +833,14 @@ Options:
784
833
  console.log(`Usage:
785
834
  filegrc program-path [audit-id] [options]
786
835
 
787
- Show the same six-step SOC 2 lifecycle used by the renderer. Each step includes
836
+ Show the same five-step SOC 2 lifecycle used by the renderer. Each step includes
788
837
  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.
838
+ and current readiness state. Pass an audit ID to include Step 5 status.
790
839
 
791
840
  Options:
792
- --audit <id> Audit record to use for Step 6
841
+ --audit <id> Audit record to use for Step 5
793
842
  --as-of <date> Evaluate readiness on YYYY-MM-DD
794
- --summary Print compact status and the first action for all six steps
843
+ --summary Print compact status and the first action for all five steps
795
844
  --next Print only the current step and its first action
796
845
  --current Print the full guide for the current step only
797
846
  --json Print the selected path view as JSON
@@ -799,18 +848,18 @@ Options:
799
848
  --help Show this help`);
800
849
  return;
801
850
  }
802
- if (command === "evidence-test-drafts") {
851
+ if (command === "evidence-map") {
803
852
  console.log(`Usage:
804
- filegrc evidence-test-drafts [options]
853
+ filegrc evidence-map [options]
805
854
 
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.
855
+ Inspect the evidence-source checks included in Control implementation. Each item
856
+ reports the required source roles, linked Controls, authoritative source Systems,
857
+ per-record checks, and exact edit commands. This diagnostic is read-only.
809
858
 
810
859
  Options:
811
- --preview Report proposed drafts without creating them
812
- --json Print created and existing records as JSON
813
- --root <path> Workspace path
860
+ --as-of <date> Evaluate the map on YYYY-MM-DD
861
+ --json Print the map as JSON
862
+ --root <path> Workspace path
814
863
  --help Show this help`);
815
864
  return;
816
865
  }
@@ -826,6 +875,7 @@ function agentOverview(model) {
826
875
  build: "filegrc build [root]",
827
876
  validate: "filegrc validate [root] --json",
828
877
  model: "filegrc model --json",
878
+ migrate: "filegrc migrate --to-model 2 --preview --json",
829
879
  describe: "filegrc describe <resource-type>",
830
880
  types: "filegrc types --json",
831
881
  guide: "filegrc guide [resource-type] --json",
@@ -835,18 +885,18 @@ function agentOverview(model) {
835
885
  search: "filegrc search <query> --json",
836
886
  obligations: "filegrc obligations --json",
837
887
  programReadiness: "filegrc program-readiness --json",
838
- evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
888
+ evidenceMap: "filegrc evidence-map --json",
839
889
  auditReadiness: "filegrc audit-readiness <audit-id> --json",
840
890
  prepareAudit: "filegrc prepare-audit <audit-id>",
841
891
  trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
842
892
  evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
843
893
  get: "filegrc get <resource-id> [--mutation]",
844
894
  references: "filegrc references <resource-id> --json",
845
- create: "filegrc create <record-or-mutation.json>",
895
+ create: "filegrc create <mutation.json>",
846
896
  complete: "filegrc complete <obligation-id> <completion-mutation.json>",
847
897
  completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
848
898
  completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
849
- update: "filegrc update <resource-type> <id> <record-or-mutation.json>",
899
+ update: "filegrc update <resource-type> <id> <mutation.json>",
850
900
  content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
851
901
  attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
852
902
  detach: "filegrc detach <evidence-id> <attachment-name> --yes",
@@ -922,6 +972,8 @@ function printAgentGuide(result) {
922
972
  }
923
973
  console.log("\nWorkflow:");
924
974
  result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
975
+ console.log("\nCompletion checks:");
976
+ result.completionChecks.forEach((check) => console.log(`- ${check}`));
925
977
  }
926
978
 
927
979
  function buildProgramPathResult(model, readiness, auditReadiness) {
@@ -1069,6 +1121,15 @@ function shellArgument(value) {
1069
1121
  : `'${text.replaceAll("'", "'\\''")}'`;
1070
1122
  }
1071
1123
 
1124
+ function evidenceSourceCheckName(name) {
1125
+ return ({
1126
+ active: "activate source",
1127
+ sourceRole: "add source role",
1128
+ accessOwners: "add access owner",
1129
+ retrievalInstructions: "add retrieval instructions"
1130
+ })[name] || name;
1131
+ }
1132
+
1072
1133
  function printProgramPathOutput(result, flags) {
1073
1134
  if (flags.summary) {
1074
1135
  console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
@@ -1138,13 +1199,10 @@ function summarizeProgramReadiness(result) {
1138
1199
  }
1139
1200
 
1140
1201
  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";
1202
+ if (!Number.isInteger(window?.dueAfter)) return "deadline not configured";
1203
+ const unit = window.precision === "timestamp" ? "hour" : "day";
1204
+ if (window.dueAfter === 0) return window.precision === "timestamp" ? "due at event time" : "due on event date";
1205
+ return `due within ${window.dueAfter} ${unit}${window.dueAfter === 1 ? "" : "s"}`;
1148
1206
  }
1149
1207
 
1150
1208
  function formatGuideField(field) {
@@ -0,0 +1,50 @@
1
+ export function coverageBounds(coverage) {
2
+ if (coverage?.kind === "as-of" && typeof coverage.on === "string") {
3
+ return { start: coverage.on, end: coverage.on };
4
+ }
5
+ if (
6
+ coverage?.kind === "range"
7
+ && typeof coverage.startsOn === "string"
8
+ && typeof coverage.endsOn === "string"
9
+ ) {
10
+ return { start: coverage.startsOn, end: coverage.endsOn };
11
+ }
12
+ return { start: null, end: null };
13
+ }
14
+
15
+ export function coverageStart(coverage) {
16
+ return coverageBounds(coverage).start;
17
+ }
18
+
19
+ export function coverageEnd(coverage) {
20
+ return coverageBounds(coverage).end;
21
+ }
22
+
23
+ export function coverageMatches(coverage, start, end = start) {
24
+ const bounds = coverageBounds(coverage);
25
+ return bounds.start === start && bounds.end === end;
26
+ }
27
+
28
+ export function coverageOverlaps(coverage, start, end = start) {
29
+ const bounds = coverageBounds(coverage);
30
+ return Boolean(bounds.start && bounds.end && bounds.start <= end && bounds.end >= start);
31
+ }
32
+
33
+ export function coverageContains(coverage, date) {
34
+ return coverageOverlaps(coverage, date, date);
35
+ }
36
+
37
+ export function coverageLabel(coverage) {
38
+ const bounds = coverageBounds(coverage);
39
+ if (!bounds.start || !bounds.end) return "";
40
+ return bounds.start === bounds.end ? bounds.start : `${bounds.start} through ${bounds.end}`;
41
+ }
42
+
43
+ export function legacyCoverage(record, options = {}) {
44
+ const asOf = options.asOfFields?.map((field) => record[field]).find(Boolean);
45
+ if (asOf) return { kind: "as-of", on: asOf };
46
+ const start = options.startFields?.map((field) => record[field]).find(Boolean);
47
+ const end = options.endFields?.map((field) => record[field]).find(Boolean);
48
+ if (start && end) return { kind: "range", startsOn: start, endsOn: end };
49
+ return null;
50
+ }