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/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, { host: flags.host, port: flags.port });
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("\nEvent reminders:");
171
- for (const trigger of result.triggers) console.log(`${trigger.eventType}\t${trigger.prompt}\t${trigger.steps.length} actions`);
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 console.log(`Created ${result.event.id} with ${result.actions.length} action items.`);
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("An obligation event ID is required.");
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
- console.log(`Purpose: ${result.purpose}`);
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,