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/LICENSE +1 -1
- package/README.md +24 -5
- package/model/v1.json +163 -169
- package/package.json +2 -2
- package/src/agent.js +4 -0
- package/src/audit-preparation.js +217 -139
- package/src/cli.js +364 -9
- package/src/evidence-packet.js +78 -25
- package/src/evidence-tests.js +69 -0
- package/src/git.js +2 -1
- package/src/index.js +12 -1
- package/src/model-docs.js +51 -6
- package/src/obligations.js +87 -11
- package/src/parties.js +46 -0
- package/src/paths.js +1 -1
- package/src/program-lifecycle.js +25 -0
- package/src/program-path.js +275 -0
- package/src/program-readiness.js +652 -0
- package/src/resource-markdown.js +11 -4
- package/src/server.js +10 -2
- package/src/setup.js +187 -0
- package/src/state.js +9 -2
- package/src/validate.js +57 -24
- package/src/web.js +827 -440
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, {
|
|
53
|
-
|
|
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("\
|
|
171
|
-
for (const trigger of result.triggers)
|
|
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
|
|
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("
|
|
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(`
|
|
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
|
-
|
|
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,
|