filegrc 0.1.0 → 0.3.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
@@ -1,10 +1,12 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { loadModel } from "../model/index.js";
4
5
  import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
5
6
  import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
6
7
  import { buildWorkspace } from "./build.js";
7
8
  import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
9
+ import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
8
10
  import {
9
11
  addEvidenceAttachment,
10
12
  createResource,
@@ -21,9 +23,12 @@ import {
21
23
  planObligations
22
24
  } from "./obligations.js";
23
25
  import { relativeToWorkspace, resolveDataPath } from "./paths.js";
26
+ import { buildAgentProgramPath, policyEventName } from "./program-path.js";
27
+ import { assessProgramReadiness } from "./program-readiness.js";
24
28
  import { markdownEntries } from "./resource-markdown.js";
25
29
  import { searchResources } from "./search.js";
26
30
  import { serveWorkspace } from "./server.js";
31
+ import { setupWorkspace } from "./setup.js";
27
32
  import { createAppState } from "./state.js";
28
33
  import { currentCalendarDate } from "./time.js";
29
34
  import { validateWorkspace } from "./validate.js";
@@ -32,10 +37,13 @@ import { loadWorkspace } from "./workspace.js";
32
37
  const BOOLEAN_FLAGS = new Set([
33
38
  "check-docs",
34
39
  "complete",
40
+ "draft",
41
+ "help",
35
42
  "json",
36
43
  "mutation",
37
44
  "preview",
38
45
  "require-ready",
46
+ "summary",
39
47
  "write-docs",
40
48
  "yes"
41
49
  ]);
@@ -47,10 +55,14 @@ export async function runCli(argv = process.argv.slice(2)) {
47
55
 
48
56
  if (["help", "--help", "-h"].includes(command)) return printHelp();
49
57
  if (["version", "--version", "-v"].includes(command)) return printVersion();
58
+ if (flags.help || args.includes("-h")) return printCommandHelp(command);
50
59
 
51
60
  if (command === "serve") {
52
- const result = await serveWorkspace(positionals[0] ?? root, { host: flags.host, port: flags.port });
53
- console.log(`FileGRC workspace: ${result.url}`);
61
+ const result = await serveWorkspace(positionals[0] ?? root, {
62
+ host: flags.host ?? process.env.FILEGRC_HOST,
63
+ port: flags.port ?? process.env.FILEGRC_PORT
64
+ });
65
+ console.log(`filegrc workspace: ${result.url}`);
54
66
  console.log(`Data: ${result.root}/data`);
55
67
  return await new Promise((resolvePromise) => {
56
68
  const stop = () => result.server.close(resolvePromise);
@@ -58,6 +70,29 @@ export async function runCli(argv = process.argv.slice(2)) {
58
70
  process.once("SIGTERM", stop);
59
71
  });
60
72
  }
73
+ if (command === "setup") {
74
+ const payload = positionals[0] ? await readSetupPayload(positionals[0]) : {};
75
+ const setupInput = await completeInteractiveSetup(root, {
76
+ ...payload,
77
+ ...(flags["service-name"] !== undefined ? { serviceName: flags["service-name"] } : {}),
78
+ ...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
79
+ ...(flags.owner !== undefined ? { ownerId: flags.owner } : {}),
80
+ ...(flags.criticality !== undefined ? { criticality: flags.criticality } : {}),
81
+ ...(flags.classification !== undefined ? { dataClassification: flags.classification } : {}),
82
+ ...(flags["internet-exposed"] !== undefined ? { internetExposed: flags["internet-exposed"] } : {}),
83
+ ...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
84
+ ...(flags.draft ? { draft: true } : {})
85
+ });
86
+ const result = await setupWorkspace(root, setupInput);
87
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
88
+ else {
89
+ console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
90
+ console.log(`System: ${result.system.id} (${result.system.status})`);
91
+ console.log(`Target: ${result.workspace.assuranceGoal}`);
92
+ console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run filegrc program-path for the full path.");
93
+ }
94
+ return result;
95
+ }
61
96
  if (command === "build") {
62
97
  const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
63
98
  console.log(`Built read-only site at ${result.output}`);
@@ -117,6 +152,16 @@ export async function runCli(argv = process.argv.slice(2)) {
117
152
  else printAgentGuide(result);
118
153
  return result;
119
154
  }
155
+ if (command === "program-path") {
156
+ const loaded = await loadWorkspace(root);
157
+ const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
158
+ const auditId = positionals[0] || flags.audit;
159
+ const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
160
+ const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
161
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
162
+ else printProgramPath(result);
163
+ return result;
164
+ }
120
165
  if (command === "scaffold") {
121
166
  const loaded = await loadWorkspace(root);
122
167
  const type = positionals[0];
@@ -154,7 +199,7 @@ export async function runCli(argv = process.argv.slice(2)) {
154
199
  });
155
200
  if (flags.json) console.log(JSON.stringify(result, null, 2));
156
201
  else {
157
- console.log(`${result.counts.overdue} overdue, ${result.counts.due} due, ${result.counts.upcoming} upcoming`);
202
+ console.log(`${result.counts.overdue} overdue, ${result.counts.due} due, ${result.counts.upcoming} upcoming, ${result.counts.proposed} starter proposals`);
158
203
  for (const item of result.items) {
159
204
  const deadline = item.dueWindowEndAt || item.dueWindowEnd;
160
205
  if (!deadline) throw new Error(`Planned work "${item.title}" is missing a deadline.`);
@@ -167,10 +212,50 @@ export async function runCli(argv = process.argv.slice(2)) {
167
212
  ].join("\t"));
168
213
  }
169
214
  if (result.triggers.length) {
170
- console.log("\nEvent reminders:");
171
- for (const trigger of result.triggers) console.log(`${trigger.eventType}\t${trigger.prompt}\t${trigger.steps.length} actions`);
215
+ console.log("\nPolicy Events:");
216
+ for (const trigger of result.triggers) {
217
+ console.log(`${trigger.programStatus.toUpperCase()}\t${policyEventName(trigger.eventType)} (${trigger.eventType})\t${trigger.steps.length} Work Queue ${trigger.steps.length === 1 ? "task" : "tasks"}`);
218
+ for (const step of trigger.steps) {
219
+ const owners = step.ownerIds.length ? step.ownerIds.join(",") : "unassigned";
220
+ const proof = step.completionResourceTypes.length ? step.completionResourceTypes.join("|") : "not specified";
221
+ console.log(` ${step.title}\t${eventWindowText(step.window)}\towner=${owners}\tproof=${proof}`);
222
+ }
223
+ if (trigger.programStatus !== "proposed") console.log(` Trigger: filegrc trigger ${trigger.eventType} --occurred-on YYYY-MM-DD --subject RESOURCE_ID --json`);
224
+ }
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ if (command === "program-readiness") {
230
+ const loaded = await loadWorkspace(root);
231
+ const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
232
+ const output = flags.summary ? summarizeProgramReadiness(result) : result;
233
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
234
+ else if (flags.summary) {
235
+ console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
236
+ for (const stage of output.stages) {
237
+ console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} actions`);
238
+ }
239
+ if (output.firstAction) console.log(`Next: ${output.firstAction.title}\t${output.firstAction.message}`);
240
+ }
241
+ else {
242
+ console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
243
+ console.log(`${result.target.label}${result.target.candidatePeriodStart ? `, candidate period starts ${result.target.candidatePeriodStart}` : ""}`);
244
+ for (const stage of result.stages) {
245
+ console.log(`\n${stage.title}`);
246
+ for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
247
+ }
248
+ if (result.canStartCandidatePeriod && !result.operating) {
249
+ console.log(`\nEvidence Ready: management can start the candidate Type 2 period on or after ${result.suggestedCandidatePeriodStart || result.asOf}.`);
172
250
  }
173
251
  }
252
+ if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
253
+ return output;
254
+ }
255
+ if (command === "evidence-test-drafts") {
256
+ const result = await ensureEvidenceTestDrafts(root);
257
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
258
+ else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
174
259
  return result;
175
260
  }
176
261
  if (command === "audit-readiness") {
@@ -206,7 +291,11 @@ export async function runCli(argv = process.argv.slice(2)) {
206
291
  title: flags.title
207
292
  });
208
293
  if (flags.json) console.log(JSON.stringify(result, null, 2));
209
- else console.log(`Created ${result.event.id} with ${result.actions.length} action items.`);
294
+ else {
295
+ console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
296
+ console.log(`Event: obligation-event/${result.event.id}`);
297
+ 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}`);
298
+ }
210
299
  return result;
211
300
  }
212
301
  if (command === "evidence-packet") {
@@ -308,7 +397,7 @@ export async function runCli(argv = process.argv.slice(2)) {
308
397
  }
309
398
  if (command === "complete-event") {
310
399
  const eventId = positionals[0];
311
- if (!eventId) throw new Error("An obligation event ID is required.");
400
+ if (!eventId) throw new Error("A Policy Event ID is required.");
312
401
  const result = await completeObligationEvent(root, {
313
402
  eventId,
314
403
  completedOn: flags["completed-on"],
@@ -466,6 +555,66 @@ async function readMutation(path) {
466
555
  };
467
556
  }
468
557
 
558
+ async function readSetupPayload(path) {
559
+ if (!path) return {};
560
+ const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
561
+ const parsed = JSON.parse(source);
562
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
563
+ throw new Error("Setup input must be a JSON object.");
564
+ }
565
+ return parsed;
566
+ }
567
+
568
+ async function completeInteractiveSetup(root, payload) {
569
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return payload;
570
+ const loaded = await loadWorkspace(root);
571
+ const activePeople = loaded.resources.filter(({ type, status }) => type === "person" && status === "active");
572
+ if (!activePeople.length) throw new Error("Setup requires at least one active person who can own the service.");
573
+ const classifications = Object.keys(loaded.workspace.classificationDefinitions || {});
574
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
575
+ const result = { ...payload };
576
+ const askRequired = async (label, defaultValue = "") => {
577
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
578
+ let value = "";
579
+ while (!value) value = (await prompt.question(`${label}${suffix}: `)).trim() || defaultValue;
580
+ return value;
581
+ };
582
+ const askChoice = async (label, choices, defaultValue = "") => {
583
+ let value = "";
584
+ while (!choices.includes(value)) {
585
+ value = await askRequired(`${label} (${choices.join("/")})`, defaultValue);
586
+ }
587
+ return value;
588
+ };
589
+ try {
590
+ result.serviceName ||= await askRequired(
591
+ "Service name",
592
+ loaded.workspace.organizationName ? `${loaded.workspace.organizationName} service` : ""
593
+ );
594
+ result.boundary ||= await askRequired("Service boundary");
595
+ result.ownerId ||= await askChoice(
596
+ "Service owner ID",
597
+ activePeople.map(({ id }) => id),
598
+ activePeople[0].id
599
+ );
600
+ result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
601
+ result.dataClassification ||= classifications.length
602
+ ? await askChoice(
603
+ "Data classification",
604
+ classifications,
605
+ classifications.includes("Confidential") ? "Confidential" : classifications[0]
606
+ )
607
+ : await askRequired("Data classification");
608
+ if (result.internetExposed === undefined) {
609
+ result.internetExposed = (await askChoice("Internet exposed", ["yes", "no"])) === "yes";
610
+ }
611
+ result.programGoal ||= await askChoice("Program goal", ["none", "readiness", "type-1", "type-2"]);
612
+ } finally {
613
+ prompt.close();
614
+ }
615
+ return result;
616
+ }
617
+
469
618
  async function readTextInput(path) {
470
619
  if (path === true || !path) throw new Error("Pass --write <markdown-file|->.");
471
620
  return path === "-" ? readStdin() : readFile(resolve(String(path)), "utf8");
@@ -488,20 +637,24 @@ async function printVersion() {
488
637
  }
489
638
 
490
639
  function printHelp() {
491
- console.log(`FileGRC - Git-native GRC workspace
640
+ console.log(`filegrc - Git-native GRC workspace
492
641
 
493
642
  Usage:
494
643
  filegrc serve [root] [--host 127.0.0.1] [--port 8787]
644
+ filegrc setup [setup.json|-] [setup options] [--draft] [--json]
495
645
  filegrc build [root] [--output .filegrc/site]
496
646
  filegrc validate [root] [--json]
497
647
  filegrc model [--json|--write-docs|--check-docs]
498
648
  filegrc describe <resource-type>
499
649
  filegrc types [--json]
500
650
  filegrc guide [resource-type] [--id resource-id] [--json]
651
+ filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--json]
501
652
  filegrc scaffold <resource-type> --title text [--id resource-id]
502
653
  filegrc list [resource-type] [--json]
503
654
  filegrc search <query> [--type resource-type] [--json]
504
655
  filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
656
+ filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
657
+ filegrc evidence-test-drafts [--json]
505
658
  filegrc audit-readiness [audit-id] [--require-ready] [--json]
506
659
  filegrc prepare-audit <audit-id> [--json]
507
660
  filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
@@ -521,23 +674,116 @@ Usage:
521
674
  All commands accept --root <workspace>. Writes never create Git commits.`);
522
675
  }
523
676
 
677
+ function printCommandHelp(command) {
678
+ if (command === "serve") {
679
+ console.log(`Usage:
680
+ filegrc serve [root] [--host address] [--port number]
681
+
682
+ Options:
683
+ --host <address> Bind address. Defaults to FILEGRC_HOST or 127.0.0.1.
684
+ --port <number> Port. Defaults to FILEGRC_PORT or 8787. Use 0 for an available port.
685
+ --root <path> Workspace path when no positional root is given.
686
+ --help Show this help without starting the server.
687
+
688
+ Safety:
689
+ The editable server has no authentication and binds to loopback by default.
690
+ Do not bind it to an untrusted network without trusted authentication.`);
691
+ return;
692
+ }
693
+ if (command === "setup") {
694
+ console.log(`Usage:
695
+ filegrc setup [setup.json|-] [options]
696
+
697
+ Create or update the initial service boundary through the same validated operation
698
+ used by browser onboarding. Run without input in an interactive terminal for guided
699
+ setup. JSON keys use the camelCase forms shown below.
700
+
701
+ Options:
702
+ --service-name <name> serviceName
703
+ --boundary <description> boundary
704
+ --owner <person-id> ownerId
705
+ --criticality <level> low, medium, high, or critical
706
+ --classification <name> dataClassification
707
+ --internet-exposed <bool> true or false
708
+ --program-goal <goal> none, readiness, type-1, or type-2
709
+ --draft Save the service boundary as planned
710
+ --json Print the result as JSON
711
+ --root <path> Workspace path
712
+ --help Show this help`);
713
+ return;
714
+ }
715
+ if (command === "program-readiness") {
716
+ console.log(`Usage:
717
+ filegrc program-readiness [options]
718
+
719
+ Report whether management has defined scope, activated policies, implemented
720
+ controls, configured authoritative evidence sources, and verified test collection
721
+ for external evidence without a dedicated Step 5 record. No audit ID or CPA firm
722
+ is required.
723
+
724
+ Options:
725
+ --as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
726
+ --require-ready Exit with code 2 unless the Evidence Ready gate passes
727
+ --summary Omit item details and print stage counts and next actions
728
+ --json Print the result as JSON
729
+ --root <path> Workspace path
730
+ --help Show this help`);
731
+ return;
732
+ }
733
+ if (command === "program-path") {
734
+ console.log(`Usage:
735
+ filegrc program-path [audit-id] [options]
736
+
737
+ Show the same six-step SOC 2 lifecycle used by the renderer. Each step includes
738
+ its exact page instructions, Use and Policy Basis context, resource commands,
739
+ and current readiness state. Pass an audit ID to include Step 6 status.
740
+
741
+ Options:
742
+ --audit <id> Audit record to use for Step 6
743
+ --as-of <date> Evaluate readiness on YYYY-MM-DD
744
+ --json Print the full agent-oriented path as JSON
745
+ --root <path> Workspace path
746
+ --help Show this help`);
747
+ return;
748
+ }
749
+ if (command === "evidence-test-drafts") {
750
+ console.log(`Usage:
751
+ filegrc evidence-test-drafts [options]
752
+
753
+ Create missing draft External Evidence records for collection that does not
754
+ already have a dedicated Step 5 operating record. Existing tests are preserved.
755
+
756
+ Options:
757
+ --json Print created and existing records as JSON
758
+ --root <path> Workspace path
759
+ --help Show this help`);
760
+ return;
761
+ }
762
+ printHelp();
763
+ }
764
+
524
765
  function agentOverview(model) {
525
766
  return {
526
767
  rule: "Treat data/ as the source of truth. Run guide before creating an unfamiliar type, validate after every write, review the Git diff, then commit a focused change.",
768
+ programPath: buildAgentProgramPath(model),
527
769
  actions: {
528
770
  help: "filegrc help",
529
771
  version: "filegrc version",
530
772
  serve: "filegrc serve [root]",
773
+ setup: "filegrc setup [setup.json|-] [--draft] [--json]",
531
774
  build: "filegrc build [root]",
532
775
  validate: "filegrc validate [root] --json",
533
776
  model: "filegrc model --json",
534
777
  describe: "filegrc describe <resource-type>",
535
778
  types: "filegrc types --json",
536
779
  guide: "filegrc guide [resource-type] --json",
780
+ programPath: "filegrc program-path [audit-id] --json",
537
781
  scaffold: "filegrc scaffold <resource-type> --title <name>",
538
782
  list: "filegrc list [resource-type] --json",
539
783
  search: "filegrc search <query> --json",
540
784
  obligations: "filegrc obligations --json",
785
+ programReadiness: "filegrc program-readiness --json",
786
+ evidenceTestDrafts: "filegrc evidence-test-drafts --json",
541
787
  auditReadiness: "filegrc audit-readiness <audit-id> --json",
542
788
  prepareAudit: "filegrc prepare-audit <audit-id>",
543
789
  trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
@@ -561,6 +807,8 @@ function agentOverview(model) {
561
807
 
562
808
  function printAgentOverview(result) {
563
809
  console.log(result.rule);
810
+ console.log("\nProgram path:");
811
+ for (const stage of result.programPath) console.log(`${stage.number}. ${stage.title}\t${stage.summary}`);
564
812
  console.log("\nActions:");
565
813
  for (const [name, command] of Object.entries(result.actions)) console.log(`${name}\t${command}`);
566
814
  console.log("\nResource types:");
@@ -569,7 +817,11 @@ function printAgentOverview(result) {
569
817
 
570
818
  function printAgentGuide(result) {
571
819
  console.log(`${result.title} (${result.type})`);
572
- console.log(`Purpose: ${result.purpose}`);
820
+ if (result.programStep) {
821
+ console.log(`Program step: ${result.programStep.order ? `Step ${result.programStep.order}` : `Step ${result.programStep.number}`} · ${result.programStep.title}`);
822
+ }
823
+ console.log(`Instructions: ${result.instructions}`);
824
+ console.log(`Use: ${result.use}`);
573
825
  console.log(`Policy basis: ${result.policyBasis}`);
574
826
  console.log(`Timing: ${result.cadence}`);
575
827
  console.log(`JSON: ${result.location}`);
@@ -613,6 +865,109 @@ function printAgentGuide(result) {
613
865
  result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
614
866
  }
615
867
 
868
+ function buildProgramPathResult(model, readiness, auditReadiness) {
869
+ const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
870
+ const stages = buildAgentProgramPath(model).map((stage) => {
871
+ if (stage.id === "audit") {
872
+ return {
873
+ ...stage,
874
+ status: auditReadiness?.status || "not-started",
875
+ counts: auditReadiness?.counts || null,
876
+ nextActions: auditReadiness?.firstAction ? [auditReadiness.firstAction] : []
877
+ };
878
+ }
879
+ const readinessId = stage.id === "run" ? "operation" : stage.id;
880
+ const current = readinessById.get(readinessId);
881
+ const status = stage.id === "run" && readiness.operating
882
+ ? "operating"
883
+ : current?.status || "not-started";
884
+ return {
885
+ ...stage,
886
+ status,
887
+ counts: current?.counts || null,
888
+ nextActions: (current?.items || []).filter((item) => item.status === "action")
889
+ };
890
+ });
891
+ const currentStep = stages.find((stage) => !["complete", "operating", "management-ready"].includes(stage.status)) || stages.at(-1);
892
+ return {
893
+ schemaVersion: 1,
894
+ asOf: readiness.asOf,
895
+ currentStep: { id: currentStep.id, number: currentStep.number, title: currentStep.title },
896
+ evidenceReady: readiness.evidenceReady,
897
+ operating: readiness.operating,
898
+ stages
899
+ };
900
+ }
901
+
902
+ function printProgramPath(result) {
903
+ console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
904
+ console.log(`Evidence Ready: ${result.evidenceReady ? "yes" : "no"}; operating: ${result.operating ? "yes" : "no"}`);
905
+ for (const stage of result.stages) {
906
+ console.log(`\nStep ${stage.number}. ${stage.title} [${String(stage.status).toUpperCase()}]`);
907
+ console.log(stage.summary);
908
+ for (const page of stage.pages) {
909
+ console.log(`${page.order ? `Step ${page.order}` : "Operating area"} · ${page.title} (${page.type || `utility:${page.utility}`})`);
910
+ console.log(` Instructions: ${page.instructions}`);
911
+ console.log(` Use: ${page.use}`);
912
+ console.log(` Policy basis: ${page.policyBasis}`);
913
+ }
914
+ if (stage.operatingRecords?.length) {
915
+ console.log("Operating record guides:");
916
+ for (const record of stage.operatingRecords) {
917
+ console.log(` ${record.type}\t${record.instructions}\t${record.guide}`);
918
+ }
919
+ }
920
+ console.log("Commands:");
921
+ for (const command of stage.commands) console.log(` ${command}`);
922
+ for (const action of stage.nextActions) console.log(`Next: ${action.title} · ${action.message}`);
923
+ }
924
+ }
925
+
926
+ function summarizeProgramReadiness(result) {
927
+ const summarizeItem = (item) => item ? {
928
+ id: item.id,
929
+ status: item.status,
930
+ title: item.title,
931
+ message: item.message,
932
+ ...(item.resourceType ? { resourceType: item.resourceType } : {}),
933
+ ...(item.resourceId ? { resourceId: item.resourceId } : {})
934
+ } : null;
935
+ return {
936
+ schemaVersion: result.schemaVersion,
937
+ generatedAt: result.generatedAt,
938
+ asOf: result.asOf,
939
+ status: result.status,
940
+ evidenceReady: result.evidenceReady,
941
+ operating: result.operating,
942
+ canStartCandidatePeriod: result.canStartCandidatePeriod,
943
+ suggestedCandidatePeriodStart: result.suggestedCandidatePeriodStart,
944
+ target: result.target,
945
+ progress: result.progress,
946
+ counts: result.counts,
947
+ scopeCounts: Object.fromEntries(
948
+ Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
949
+ ),
950
+ firstAction: summarizeItem(result.firstAction),
951
+ stages: result.stages.map((stage) => ({
952
+ id: stage.id,
953
+ title: stage.title,
954
+ status: stage.status,
955
+ counts: stage.counts,
956
+ firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"))
957
+ }))
958
+ };
959
+ }
960
+
961
+ function eventWindowText(window) {
962
+ if (Number.isInteger(window?.endOffsetHours)) {
963
+ return window.endOffsetHours === 0 ? "due at event time" : `due within ${window.endOffsetHours} hours`;
964
+ }
965
+ if (Number.isInteger(window?.endOffsetDays)) {
966
+ return window.endOffsetDays === 0 ? "due on event date" : `due within ${window.endOffsetDays} days`;
967
+ }
968
+ return "due within 30 days";
969
+ }
970
+
616
971
  function formatGuideField(field) {
617
972
  const details = [
618
973
  field.values?.length ? `one of ${field.values.join("|")}` : field.type,