filegrc 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/model/v1.json +159 -165
- package/package.json +1 -1
- package/src/agent.js +4 -0
- package/src/audit-preparation.js +217 -116
- package/src/cli.js +264 -7
- package/src/evidence-packet.js +72 -19
- package/src/evidence-tests.js +69 -0
- package/src/git.js +1 -0
- package/src/index.js +11 -0
- package/src/model-docs.js +51 -6
- package/src/obligations.js +87 -11
- package/src/program-lifecycle.js +22 -0
- package/src/program-path.js +275 -0
- package/src/program-readiness.js +635 -0
- package/src/resource-markdown.js +11 -4
- package/src/server.js +8 -0
- package/src/setup.js +187 -0
- package/src/state.js +8 -1
- package/src/validate.js +67 -5
- package/src/web.js +757 -428
package/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldRes
|
|
|
5
5
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
6
6
|
import { buildWorkspace } from "./build.js";
|
|
7
7
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
8
|
+
import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
|
|
8
9
|
import {
|
|
9
10
|
addEvidenceAttachment,
|
|
10
11
|
createResource,
|
|
@@ -21,9 +22,12 @@ import {
|
|
|
21
22
|
planObligations
|
|
22
23
|
} from "./obligations.js";
|
|
23
24
|
import { relativeToWorkspace, resolveDataPath } from "./paths.js";
|
|
25
|
+
import { buildAgentProgramPath, policyEventName } from "./program-path.js";
|
|
26
|
+
import { assessProgramReadiness } from "./program-readiness.js";
|
|
24
27
|
import { markdownEntries } from "./resource-markdown.js";
|
|
25
28
|
import { searchResources } from "./search.js";
|
|
26
29
|
import { serveWorkspace } from "./server.js";
|
|
30
|
+
import { setupWorkspace } from "./setup.js";
|
|
27
31
|
import { createAppState } from "./state.js";
|
|
28
32
|
import { currentCalendarDate } from "./time.js";
|
|
29
33
|
import { validateWorkspace } from "./validate.js";
|
|
@@ -32,6 +36,8 @@ import { loadWorkspace } from "./workspace.js";
|
|
|
32
36
|
const BOOLEAN_FLAGS = new Set([
|
|
33
37
|
"check-docs",
|
|
34
38
|
"complete",
|
|
39
|
+
"draft",
|
|
40
|
+
"help",
|
|
35
41
|
"json",
|
|
36
42
|
"mutation",
|
|
37
43
|
"preview",
|
|
@@ -47,9 +53,13 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
47
53
|
|
|
48
54
|
if (["help", "--help", "-h"].includes(command)) return printHelp();
|
|
49
55
|
if (["version", "--version", "-v"].includes(command)) return printVersion();
|
|
56
|
+
if (flags.help || args.includes("-h")) return printCommandHelp(command);
|
|
50
57
|
|
|
51
58
|
if (command === "serve") {
|
|
52
|
-
const result = await serveWorkspace(positionals[0] ?? root, {
|
|
59
|
+
const result = await serveWorkspace(positionals[0] ?? root, {
|
|
60
|
+
host: flags.host ?? process.env.FILEGRC_HOST,
|
|
61
|
+
port: flags.port ?? process.env.FILEGRC_PORT
|
|
62
|
+
});
|
|
53
63
|
console.log(`FileGRC workspace: ${result.url}`);
|
|
54
64
|
console.log(`Data: ${result.root}/data`);
|
|
55
65
|
return await new Promise((resolvePromise) => {
|
|
@@ -58,6 +68,28 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
58
68
|
process.once("SIGTERM", stop);
|
|
59
69
|
});
|
|
60
70
|
}
|
|
71
|
+
if (command === "setup") {
|
|
72
|
+
const payload = positionals[0] ? await readSetupPayload(positionals[0]) : {};
|
|
73
|
+
const result = await setupWorkspace(root, {
|
|
74
|
+
...payload,
|
|
75
|
+
...(flags["service-name"] !== undefined ? { serviceName: flags["service-name"] } : {}),
|
|
76
|
+
...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
|
|
77
|
+
...(flags.owner !== undefined ? { ownerId: flags.owner } : {}),
|
|
78
|
+
...(flags.criticality !== undefined ? { criticality: flags.criticality } : {}),
|
|
79
|
+
...(flags.classification !== undefined ? { dataClassification: flags.classification } : {}),
|
|
80
|
+
...(flags["internet-exposed"] !== undefined ? { internetExposed: flags["internet-exposed"] } : {}),
|
|
81
|
+
...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
|
|
82
|
+
...(flags.draft ? { draft: true } : {})
|
|
83
|
+
});
|
|
84
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
85
|
+
else {
|
|
86
|
+
console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
|
|
87
|
+
console.log(`System: ${result.system.id} (${result.system.status})`);
|
|
88
|
+
console.log(`Target: ${result.workspace.assuranceGoal}`);
|
|
89
|
+
console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run filegrc program-path for the full path.");
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
61
93
|
if (command === "build") {
|
|
62
94
|
const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
|
|
63
95
|
console.log(`Built read-only site at ${result.output}`);
|
|
@@ -117,6 +149,16 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
117
149
|
else printAgentGuide(result);
|
|
118
150
|
return result;
|
|
119
151
|
}
|
|
152
|
+
if (command === "program-path") {
|
|
153
|
+
const loaded = await loadWorkspace(root);
|
|
154
|
+
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
155
|
+
const auditId = positionals[0] || flags.audit;
|
|
156
|
+
const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
|
|
157
|
+
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
158
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
159
|
+
else printProgramPath(result);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
120
162
|
if (command === "scaffold") {
|
|
121
163
|
const loaded = await loadWorkspace(root);
|
|
122
164
|
const type = positionals[0];
|
|
@@ -154,7 +196,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
154
196
|
});
|
|
155
197
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
156
198
|
else {
|
|
157
|
-
console.log(`${result.counts.overdue} overdue, ${result.counts.due} due, ${result.counts.upcoming} upcoming`);
|
|
199
|
+
console.log(`${result.counts.overdue} overdue, ${result.counts.due} due, ${result.counts.upcoming} upcoming, ${result.counts.proposed} starter proposals`);
|
|
158
200
|
for (const item of result.items) {
|
|
159
201
|
const deadline = item.dueWindowEndAt || item.dueWindowEnd;
|
|
160
202
|
if (!deadline) throw new Error(`Planned work "${item.title}" is missing a deadline.`);
|
|
@@ -167,10 +209,42 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
167
209
|
].join("\t"));
|
|
168
210
|
}
|
|
169
211
|
if (result.triggers.length) {
|
|
170
|
-
console.log("\
|
|
171
|
-
for (const trigger of result.triggers)
|
|
212
|
+
console.log("\nPolicy Events:");
|
|
213
|
+
for (const trigger of result.triggers) {
|
|
214
|
+
console.log(`${trigger.programStatus.toUpperCase()}\t${policyEventName(trigger.eventType)} (${trigger.eventType})\t${trigger.steps.length} Work Queue ${trigger.steps.length === 1 ? "task" : "tasks"}`);
|
|
215
|
+
for (const step of trigger.steps) {
|
|
216
|
+
const owners = step.ownerIds.length ? step.ownerIds.join(",") : "unassigned";
|
|
217
|
+
const proof = step.completionResourceTypes.length ? step.completionResourceTypes.join("|") : "not specified";
|
|
218
|
+
console.log(` ${step.title}\t${eventWindowText(step.window)}\towner=${owners}\tproof=${proof}`);
|
|
219
|
+
}
|
|
220
|
+
if (trigger.programStatus !== "proposed") console.log(` Trigger: filegrc trigger ${trigger.eventType} --occurred-on YYYY-MM-DD --subject RESOURCE_ID --json`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
if (command === "program-readiness") {
|
|
227
|
+
const loaded = await loadWorkspace(root);
|
|
228
|
+
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
229
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
230
|
+
else {
|
|
231
|
+
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
|
|
232
|
+
console.log(`${result.target.label}${result.target.candidatePeriodStart ? `, candidate period starts ${result.target.candidatePeriodStart}` : ""}`);
|
|
233
|
+
for (const stage of result.stages) {
|
|
234
|
+
console.log(`\n${stage.title}`);
|
|
235
|
+
for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
236
|
+
}
|
|
237
|
+
if (result.canStartCandidatePeriod && !result.operating) {
|
|
238
|
+
console.log(`\nEvidence Ready: management can start the candidate Type 2 period on or after ${result.suggestedCandidatePeriodStart || result.asOf}.`);
|
|
172
239
|
}
|
|
173
240
|
}
|
|
241
|
+
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
if (command === "evidence-test-drafts") {
|
|
245
|
+
const result = await ensureEvidenceTestDrafts(root);
|
|
246
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
247
|
+
else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
|
|
174
248
|
return result;
|
|
175
249
|
}
|
|
176
250
|
if (command === "audit-readiness") {
|
|
@@ -206,7 +280,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
206
280
|
title: flags.title
|
|
207
281
|
});
|
|
208
282
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
209
|
-
else
|
|
283
|
+
else {
|
|
284
|
+
console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
|
|
285
|
+
console.log(`Event: obligation-event/${result.event.id}`);
|
|
286
|
+
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}`);
|
|
287
|
+
}
|
|
210
288
|
return result;
|
|
211
289
|
}
|
|
212
290
|
if (command === "evidence-packet") {
|
|
@@ -308,7 +386,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
308
386
|
}
|
|
309
387
|
if (command === "complete-event") {
|
|
310
388
|
const eventId = positionals[0];
|
|
311
|
-
if (!eventId) throw new Error("
|
|
389
|
+
if (!eventId) throw new Error("A Policy Event ID is required.");
|
|
312
390
|
const result = await completeObligationEvent(root, {
|
|
313
391
|
eventId,
|
|
314
392
|
completedOn: flags["completed-on"],
|
|
@@ -466,6 +544,16 @@ async function readMutation(path) {
|
|
|
466
544
|
};
|
|
467
545
|
}
|
|
468
546
|
|
|
547
|
+
async function readSetupPayload(path) {
|
|
548
|
+
if (!path) return {};
|
|
549
|
+
const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
|
|
550
|
+
const parsed = JSON.parse(source);
|
|
551
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
552
|
+
throw new Error("Setup input must be a JSON object.");
|
|
553
|
+
}
|
|
554
|
+
return parsed;
|
|
555
|
+
}
|
|
556
|
+
|
|
469
557
|
async function readTextInput(path) {
|
|
470
558
|
if (path === true || !path) throw new Error("Pass --write <markdown-file|->.");
|
|
471
559
|
return path === "-" ? readStdin() : readFile(resolve(String(path)), "utf8");
|
|
@@ -492,16 +580,20 @@ function printHelp() {
|
|
|
492
580
|
|
|
493
581
|
Usage:
|
|
494
582
|
filegrc serve [root] [--host 127.0.0.1] [--port 8787]
|
|
583
|
+
filegrc setup [setup.json|-] [setup options] [--draft] [--json]
|
|
495
584
|
filegrc build [root] [--output .filegrc/site]
|
|
496
585
|
filegrc validate [root] [--json]
|
|
497
586
|
filegrc model [--json|--write-docs|--check-docs]
|
|
498
587
|
filegrc describe <resource-type>
|
|
499
588
|
filegrc types [--json]
|
|
500
589
|
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
590
|
+
filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--json]
|
|
501
591
|
filegrc scaffold <resource-type> --title text [--id resource-id]
|
|
502
592
|
filegrc list [resource-type] [--json]
|
|
503
593
|
filegrc search <query> [--type resource-type] [--json]
|
|
504
594
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
595
|
+
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--json]
|
|
596
|
+
filegrc evidence-test-drafts [--json]
|
|
505
597
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
506
598
|
filegrc prepare-audit <audit-id> [--json]
|
|
507
599
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
@@ -521,23 +613,114 @@ Usage:
|
|
|
521
613
|
All commands accept --root <workspace>. Writes never create Git commits.`);
|
|
522
614
|
}
|
|
523
615
|
|
|
616
|
+
function printCommandHelp(command) {
|
|
617
|
+
if (command === "serve") {
|
|
618
|
+
console.log(`Usage:
|
|
619
|
+
filegrc serve [root] [--host address] [--port number]
|
|
620
|
+
|
|
621
|
+
Options:
|
|
622
|
+
--host <address> Bind address. Defaults to FILEGRC_HOST or 127.0.0.1.
|
|
623
|
+
--port <number> Port. Defaults to FILEGRC_PORT or 8787. Use 0 for an available port.
|
|
624
|
+
--root <path> Workspace path when no positional root is given.
|
|
625
|
+
--help Show this help without starting the server.
|
|
626
|
+
|
|
627
|
+
Safety:
|
|
628
|
+
The editable server has no authentication and binds to loopback by default.
|
|
629
|
+
Do not bind it to an untrusted network without trusted authentication.`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (command === "setup") {
|
|
633
|
+
console.log(`Usage:
|
|
634
|
+
filegrc setup [setup.json|-] [options]
|
|
635
|
+
|
|
636
|
+
Create or update the initial service boundary through the same validated operation
|
|
637
|
+
used by browser onboarding. JSON keys use the camelCase forms shown below.
|
|
638
|
+
|
|
639
|
+
Options:
|
|
640
|
+
--service-name <name> serviceName
|
|
641
|
+
--boundary <description> boundary
|
|
642
|
+
--owner <person-id> ownerId
|
|
643
|
+
--criticality <level> low, medium, high, or critical
|
|
644
|
+
--classification <name> dataClassification
|
|
645
|
+
--internet-exposed <bool> true or false
|
|
646
|
+
--program-goal <goal> none, readiness, type-1, or type-2
|
|
647
|
+
--draft Save the service boundary as planned
|
|
648
|
+
--json Print the result as JSON
|
|
649
|
+
--root <path> Workspace path
|
|
650
|
+
--help Show this help`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (command === "program-readiness") {
|
|
654
|
+
console.log(`Usage:
|
|
655
|
+
filegrc program-readiness [options]
|
|
656
|
+
|
|
657
|
+
Report whether management has defined scope, activated policies, implemented
|
|
658
|
+
controls, configured authoritative evidence sources, and verified test collection
|
|
659
|
+
for external evidence without a dedicated Step 5 record. No audit ID or CPA firm
|
|
660
|
+
is required.
|
|
661
|
+
|
|
662
|
+
Options:
|
|
663
|
+
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
664
|
+
--require-ready Exit with code 2 unless the Evidence Ready gate passes
|
|
665
|
+
--json Print the result as JSON
|
|
666
|
+
--root <path> Workspace path
|
|
667
|
+
--help Show this help`);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (command === "program-path") {
|
|
671
|
+
console.log(`Usage:
|
|
672
|
+
filegrc program-path [audit-id] [options]
|
|
673
|
+
|
|
674
|
+
Show the same six-step SOC 2 lifecycle used by the renderer. Each step includes
|
|
675
|
+
its exact page instructions, Use and Policy Basis context, resource commands,
|
|
676
|
+
and current readiness state. Pass an audit ID to include Step 6 status.
|
|
677
|
+
|
|
678
|
+
Options:
|
|
679
|
+
--audit <id> Audit record to use for Step 6
|
|
680
|
+
--as-of <date> Evaluate readiness on YYYY-MM-DD
|
|
681
|
+
--json Print the full agent-oriented path as JSON
|
|
682
|
+
--root <path> Workspace path
|
|
683
|
+
--help Show this help`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (command === "evidence-test-drafts") {
|
|
687
|
+
console.log(`Usage:
|
|
688
|
+
filegrc evidence-test-drafts [options]
|
|
689
|
+
|
|
690
|
+
Create missing draft External Evidence records for collection that does not
|
|
691
|
+
already have a dedicated Step 5 operating record. Existing tests are preserved.
|
|
692
|
+
|
|
693
|
+
Options:
|
|
694
|
+
--json Print created and existing records as JSON
|
|
695
|
+
--root <path> Workspace path
|
|
696
|
+
--help Show this help`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
printHelp();
|
|
700
|
+
}
|
|
701
|
+
|
|
524
702
|
function agentOverview(model) {
|
|
525
703
|
return {
|
|
526
704
|
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.",
|
|
705
|
+
programPath: buildAgentProgramPath(model),
|
|
527
706
|
actions: {
|
|
528
707
|
help: "filegrc help",
|
|
529
708
|
version: "filegrc version",
|
|
530
709
|
serve: "filegrc serve [root]",
|
|
710
|
+
setup: "filegrc setup [setup.json|-] [--draft] [--json]",
|
|
531
711
|
build: "filegrc build [root]",
|
|
532
712
|
validate: "filegrc validate [root] --json",
|
|
533
713
|
model: "filegrc model --json",
|
|
534
714
|
describe: "filegrc describe <resource-type>",
|
|
535
715
|
types: "filegrc types --json",
|
|
536
716
|
guide: "filegrc guide [resource-type] --json",
|
|
717
|
+
programPath: "filegrc program-path [audit-id] --json",
|
|
537
718
|
scaffold: "filegrc scaffold <resource-type> --title <name>",
|
|
538
719
|
list: "filegrc list [resource-type] --json",
|
|
539
720
|
search: "filegrc search <query> --json",
|
|
540
721
|
obligations: "filegrc obligations --json",
|
|
722
|
+
programReadiness: "filegrc program-readiness --json",
|
|
723
|
+
evidenceTestDrafts: "filegrc evidence-test-drafts --json",
|
|
541
724
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
542
725
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
|
543
726
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
@@ -561,6 +744,8 @@ function agentOverview(model) {
|
|
|
561
744
|
|
|
562
745
|
function printAgentOverview(result) {
|
|
563
746
|
console.log(result.rule);
|
|
747
|
+
console.log("\nProgram path:");
|
|
748
|
+
for (const stage of result.programPath) console.log(`${stage.number}. ${stage.title}\t${stage.summary}`);
|
|
564
749
|
console.log("\nActions:");
|
|
565
750
|
for (const [name, command] of Object.entries(result.actions)) console.log(`${name}\t${command}`);
|
|
566
751
|
console.log("\nResource types:");
|
|
@@ -569,7 +754,11 @@ function printAgentOverview(result) {
|
|
|
569
754
|
|
|
570
755
|
function printAgentGuide(result) {
|
|
571
756
|
console.log(`${result.title} (${result.type})`);
|
|
572
|
-
|
|
757
|
+
if (result.programStep) {
|
|
758
|
+
console.log(`Program step: ${result.programStep.order ? `Step ${result.programStep.order}` : `Step ${result.programStep.number}`} · ${result.programStep.title}`);
|
|
759
|
+
}
|
|
760
|
+
console.log(`Instructions: ${result.instructions}`);
|
|
761
|
+
console.log(`Use: ${result.use}`);
|
|
573
762
|
console.log(`Policy basis: ${result.policyBasis}`);
|
|
574
763
|
console.log(`Timing: ${result.cadence}`);
|
|
575
764
|
console.log(`JSON: ${result.location}`);
|
|
@@ -613,6 +802,74 @@ function printAgentGuide(result) {
|
|
|
613
802
|
result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
|
|
614
803
|
}
|
|
615
804
|
|
|
805
|
+
function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
806
|
+
const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
|
|
807
|
+
const stages = buildAgentProgramPath(model).map((stage) => {
|
|
808
|
+
if (stage.id === "audit") {
|
|
809
|
+
return {
|
|
810
|
+
...stage,
|
|
811
|
+
status: auditReadiness?.status || "not-started",
|
|
812
|
+
counts: auditReadiness?.counts || null,
|
|
813
|
+
nextActions: auditReadiness?.firstAction ? [auditReadiness.firstAction] : []
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const readinessId = stage.id === "run" ? "operation" : stage.id;
|
|
817
|
+
const current = readinessById.get(readinessId);
|
|
818
|
+
const status = stage.id === "run" && readiness.operating
|
|
819
|
+
? "operating"
|
|
820
|
+
: current?.status || "not-started";
|
|
821
|
+
return {
|
|
822
|
+
...stage,
|
|
823
|
+
status,
|
|
824
|
+
counts: current?.counts || null,
|
|
825
|
+
nextActions: (current?.items || []).filter((item) => item.status === "action")
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
const currentStep = stages.find((stage) => !["complete", "operating", "management-ready"].includes(stage.status)) || stages.at(-1);
|
|
829
|
+
return {
|
|
830
|
+
schemaVersion: 1,
|
|
831
|
+
asOf: readiness.asOf,
|
|
832
|
+
currentStep: { id: currentStep.id, number: currentStep.number, title: currentStep.title },
|
|
833
|
+
evidenceReady: readiness.evidenceReady,
|
|
834
|
+
operating: readiness.operating,
|
|
835
|
+
stages
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function printProgramPath(result) {
|
|
840
|
+
console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
|
|
841
|
+
console.log(`Evidence Ready: ${result.evidenceReady ? "yes" : "no"}; operating: ${result.operating ? "yes" : "no"}`);
|
|
842
|
+
for (const stage of result.stages) {
|
|
843
|
+
console.log(`\nStep ${stage.number}. ${stage.title} [${String(stage.status).toUpperCase()}]`);
|
|
844
|
+
console.log(stage.summary);
|
|
845
|
+
for (const page of stage.pages) {
|
|
846
|
+
console.log(`${page.order ? `Step ${page.order}` : "Operating area"} · ${page.title} (${page.type || `utility:${page.utility}`})`);
|
|
847
|
+
console.log(` Instructions: ${page.instructions}`);
|
|
848
|
+
console.log(` Use: ${page.use}`);
|
|
849
|
+
console.log(` Policy basis: ${page.policyBasis}`);
|
|
850
|
+
}
|
|
851
|
+
if (stage.operatingRecords?.length) {
|
|
852
|
+
console.log("Operating record guides:");
|
|
853
|
+
for (const record of stage.operatingRecords) {
|
|
854
|
+
console.log(` ${record.type}\t${record.instructions}\t${record.guide}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
console.log("Commands:");
|
|
858
|
+
for (const command of stage.commands) console.log(` ${command}`);
|
|
859
|
+
for (const action of stage.nextActions) console.log(`Next: ${action.title} · ${action.message}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function eventWindowText(window) {
|
|
864
|
+
if (Number.isInteger(window?.endOffsetHours)) {
|
|
865
|
+
return window.endOffsetHours === 0 ? "due at event time" : `due within ${window.endOffsetHours} hours`;
|
|
866
|
+
}
|
|
867
|
+
if (Number.isInteger(window?.endOffsetDays)) {
|
|
868
|
+
return window.endOffsetDays === 0 ? "due on event date" : `due within ${window.endOffsetDays} days`;
|
|
869
|
+
}
|
|
870
|
+
return "due within 30 days";
|
|
871
|
+
}
|
|
872
|
+
|
|
616
873
|
function formatGuideField(field) {
|
|
617
874
|
const details = [
|
|
618
875
|
field.values?.length ? `one of ${field.values.join("|")}` : field.type,
|