filegrc 0.2.0 → 0.3.1

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,11 +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";
8
- import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
9
+ import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
9
10
  import {
10
11
  addEvidenceAttachment,
11
12
  createResource,
@@ -27,7 +28,7 @@ import { assessProgramReadiness } from "./program-readiness.js";
27
28
  import { markdownEntries } from "./resource-markdown.js";
28
29
  import { searchResources } from "./search.js";
29
30
  import { serveWorkspace } from "./server.js";
30
- import { setupWorkspace } from "./setup.js";
31
+ import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
31
32
  import { createAppState } from "./state.js";
32
33
  import { currentCalendarDate } from "./time.js";
33
34
  import { validateWorkspace } from "./validate.js";
@@ -42,6 +43,7 @@ const BOOLEAN_FLAGS = new Set([
42
43
  "mutation",
43
44
  "preview",
44
45
  "require-ready",
46
+ "summary",
45
47
  "write-docs",
46
48
  "yes"
47
49
  ]);
@@ -60,7 +62,7 @@ export async function runCli(argv = process.argv.slice(2)) {
60
62
  host: flags.host ?? process.env.FILEGRC_HOST,
61
63
  port: flags.port ?? process.env.FILEGRC_PORT
62
64
  });
63
- console.log(`FileGRC workspace: ${result.url}`);
65
+ console.log(`filegrc workspace: ${result.url}`);
64
66
  console.log(`Data: ${result.root}/data`);
65
67
  return await new Promise((resolvePromise) => {
66
68
  const stop = () => result.server.close(resolvePromise);
@@ -70,7 +72,7 @@ export async function runCli(argv = process.argv.slice(2)) {
70
72
  }
71
73
  if (command === "setup") {
72
74
  const payload = positionals[0] ? await readSetupPayload(positionals[0]) : {};
73
- const result = await setupWorkspace(root, {
75
+ const setupInput = await completeInteractiveSetup(root, {
74
76
  ...payload,
75
77
  ...(flags["service-name"] !== undefined ? { serviceName: flags["service-name"] } : {}),
76
78
  ...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
@@ -81,14 +83,22 @@ export async function runCli(argv = process.argv.slice(2)) {
81
83
  ...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
82
84
  ...(flags.draft ? { draft: true } : {})
83
85
  });
84
- if (flags.json) console.log(JSON.stringify(result, null, 2));
86
+ const result = flags.preview
87
+ ? await planWorkspaceSetup(root, setupInput)
88
+ : await setupWorkspace(root, setupInput);
89
+ const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
90
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
91
+ else if (flags.preview) {
92
+ console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
93
+ console.log("No controls will be linked and no evidence drafts will be created.");
94
+ }
85
95
  else {
86
96
  console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
87
97
  console.log(`System: ${result.system.id} (${result.system.status})`);
88
98
  console.log(`Target: ${result.workspace.assuranceGoal}`);
89
99
  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
100
  }
91
- return result;
101
+ return output;
92
102
  }
93
103
  if (command === "build") {
94
104
  const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
@@ -226,7 +236,15 @@ export async function runCli(argv = process.argv.slice(2)) {
226
236
  if (command === "program-readiness") {
227
237
  const loaded = await loadWorkspace(root);
228
238
  const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
229
- if (flags.json) console.log(JSON.stringify(result, null, 2));
239
+ const output = flags.summary ? summarizeProgramReadiness(result) : result;
240
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
241
+ else if (flags.summary) {
242
+ console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
243
+ for (const stage of output.stages) {
244
+ console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} actions`);
245
+ }
246
+ if (output.firstAction) console.log(`Next: ${output.firstAction.title}\t${output.firstAction.message}`);
247
+ }
230
248
  else {
231
249
  console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
232
250
  console.log(`${result.target.label}${result.target.candidatePeriodStart ? `, candidate period starts ${result.target.candidatePeriodStart}` : ""}`);
@@ -239,9 +257,32 @@ export async function runCli(argv = process.argv.slice(2)) {
239
257
  }
240
258
  }
241
259
  if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
242
- return result;
260
+ return output;
243
261
  }
244
262
  if (command === "evidence-test-drafts") {
263
+ if (flags.preview) {
264
+ const loaded = await loadWorkspace(root);
265
+ const plan = planEvidenceTestDrafts(loaded);
266
+ const result = {
267
+ schemaVersion: 1,
268
+ preview: true,
269
+ total: plan.length,
270
+ create: plan.filter(({ existing }) => !existing).map((item) => ({
271
+ familyId: item.familyId,
272
+ title: item.title,
273
+ testEvidenceKind: item.testEvidenceKind,
274
+ controlIds: item.controlIds
275
+ })),
276
+ existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
277
+ id: existing.id,
278
+ title: existing.title,
279
+ status: existing.status
280
+ }))
281
+ };
282
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
283
+ else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
284
+ return result;
285
+ }
245
286
  const result = await ensureEvidenceTestDrafts(root);
246
287
  if (flags.json) console.log(JSON.stringify(result, null, 2));
247
288
  else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
@@ -554,6 +595,56 @@ async function readSetupPayload(path) {
554
595
  return parsed;
555
596
  }
556
597
 
598
+ async function completeInteractiveSetup(root, payload) {
599
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return payload;
600
+ const loaded = await loadWorkspace(root);
601
+ const activePeople = loaded.resources.filter(({ type, status }) => type === "person" && status === "active");
602
+ if (!activePeople.length) throw new Error("Setup requires at least one active person who can own the service.");
603
+ const classifications = Object.keys(loaded.workspace.classificationDefinitions || {});
604
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
605
+ const result = { ...payload };
606
+ const askRequired = async (label, defaultValue = "") => {
607
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
608
+ let value = "";
609
+ while (!value) value = (await prompt.question(`${label}${suffix}: `)).trim() || defaultValue;
610
+ return value;
611
+ };
612
+ const askChoice = async (label, choices, defaultValue = "") => {
613
+ let value = "";
614
+ while (!choices.includes(value)) {
615
+ value = await askRequired(`${label} (${choices.join("/")})`, defaultValue);
616
+ }
617
+ return value;
618
+ };
619
+ try {
620
+ result.serviceName ||= await askRequired(
621
+ "Service name",
622
+ loaded.workspace.organizationName ? `${loaded.workspace.organizationName} service` : ""
623
+ );
624
+ result.boundary ||= await askRequired("Service boundary");
625
+ result.ownerId ||= await askChoice(
626
+ "Service owner ID",
627
+ activePeople.map(({ id }) => id),
628
+ activePeople[0].id
629
+ );
630
+ result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
631
+ result.dataClassification ||= classifications.length
632
+ ? await askChoice(
633
+ "Data classification",
634
+ classifications,
635
+ classifications.includes("Confidential") ? "Confidential" : classifications[0]
636
+ )
637
+ : await askRequired("Data classification");
638
+ if (result.internetExposed === undefined) {
639
+ result.internetExposed = (await askChoice("Internet exposed", ["yes", "no"])) === "yes";
640
+ }
641
+ result.programGoal ||= await askChoice("Program goal", ["none", "readiness", "type-1", "type-2"]);
642
+ } finally {
643
+ prompt.close();
644
+ }
645
+ return result;
646
+ }
647
+
557
648
  async function readTextInput(path) {
558
649
  if (path === true || !path) throw new Error("Pass --write <markdown-file|->.");
559
650
  return path === "-" ? readStdin() : readFile(resolve(String(path)), "utf8");
@@ -576,11 +667,11 @@ async function printVersion() {
576
667
  }
577
668
 
578
669
  function printHelp() {
579
- console.log(`FileGRC - Git-native GRC workspace
670
+ console.log(`filegrc - Git-native GRC workspace
580
671
 
581
672
  Usage:
582
673
  filegrc serve [root] [--host 127.0.0.1] [--port 8787]
583
- filegrc setup [setup.json|-] [setup options] [--draft] [--json]
674
+ filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
584
675
  filegrc build [root] [--output .filegrc/site]
585
676
  filegrc validate [root] [--json]
586
677
  filegrc model [--json|--write-docs|--check-docs]
@@ -592,8 +683,8 @@ Usage:
592
683
  filegrc list [resource-type] [--json]
593
684
  filegrc search <query> [--type resource-type] [--json]
594
685
  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]
686
+ filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
687
+ filegrc evidence-test-drafts [--preview] [--json]
597
688
  filegrc audit-readiness [audit-id] [--require-ready] [--json]
598
689
  filegrc prepare-audit <audit-id> [--json]
599
690
  filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
@@ -634,7 +725,8 @@ Safety:
634
725
  filegrc setup [setup.json|-] [options]
635
726
 
636
727
  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.
728
+ used by browser onboarding. Run without input in an interactive terminal for guided
729
+ setup. JSON keys use the camelCase forms shown below.
638
730
 
639
731
  Options:
640
732
  --service-name <name> serviceName
@@ -645,6 +737,8 @@ Options:
645
737
  --internet-exposed <bool> true or false
646
738
  --program-goal <goal> none, readiness, type-1, or type-2
647
739
  --draft Save the service boundary as planned
740
+ --preview Validate and report planned writes without saving
741
+ --summary Omit full workspace relationship arrays
648
742
  --json Print the result as JSON
649
743
  --root <path> Workspace path
650
744
  --help Show this help`);
@@ -662,6 +756,7 @@ is required.
662
756
  Options:
663
757
  --as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
664
758
  --require-ready Exit with code 2 unless the Evidence Ready gate passes
759
+ --summary Omit item details and print stage counts and next actions
665
760
  --json Print the result as JSON
666
761
  --root <path> Workspace path
667
762
  --help Show this help`);
@@ -687,10 +782,12 @@ Options:
687
782
  console.log(`Usage:
688
783
  filegrc evidence-test-drafts [options]
689
784
 
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.
785
+ Preview or create missing draft External Evidence records for collection that
786
+ does not already have a dedicated Step 5 operating record. Existing tests are
787
+ preserved. Run after confirming applicable controls and source systems.
692
788
 
693
789
  Options:
790
+ --preview Report proposed drafts without creating them
694
791
  --json Print created and existing records as JSON
695
792
  --root <path> Workspace path
696
793
  --help Show this help`);
@@ -707,7 +804,7 @@ function agentOverview(model) {
707
804
  help: "filegrc help",
708
805
  version: "filegrc version",
709
806
  serve: "filegrc serve [root]",
710
- setup: "filegrc setup [setup.json|-] [--draft] [--json]",
807
+ setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
711
808
  build: "filegrc build [root]",
712
809
  validate: "filegrc validate [root] --json",
713
810
  model: "filegrc model --json",
@@ -720,7 +817,7 @@ function agentOverview(model) {
720
817
  search: "filegrc search <query> --json",
721
818
  obligations: "filegrc obligations --json",
722
819
  programReadiness: "filegrc program-readiness --json",
723
- evidenceTestDrafts: "filegrc evidence-test-drafts --json",
820
+ evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
724
821
  auditReadiness: "filegrc audit-readiness <audit-id> --json",
725
822
  prepareAudit: "filegrc prepare-audit <audit-id>",
726
823
  trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
@@ -860,6 +957,46 @@ function printProgramPath(result) {
860
957
  }
861
958
  }
862
959
 
960
+ function summarizeProgramReadiness(result) {
961
+ const ownership = result.stages
962
+ .flatMap((stage) => stage.items)
963
+ .find((item) => item.id === "program-ownership");
964
+ const summarizeItem = (item) => item ? {
965
+ id: item.id,
966
+ status: item.status,
967
+ title: item.title,
968
+ message: item.message,
969
+ ...(item.resourceType ? { resourceType: item.resourceType } : {}),
970
+ ...(item.resourceId ? { resourceId: item.resourceId } : {}),
971
+ ...(item.unresolvedAssignments?.length ? { unresolvedAssignments: item.unresolvedAssignments } : {})
972
+ } : null;
973
+ return {
974
+ schemaVersion: result.schemaVersion,
975
+ generatedAt: result.generatedAt,
976
+ asOf: result.asOf,
977
+ status: result.status,
978
+ evidenceReady: result.evidenceReady,
979
+ operating: result.operating,
980
+ canStartCandidatePeriod: result.canStartCandidatePeriod,
981
+ suggestedCandidatePeriodStart: result.suggestedCandidatePeriodStart,
982
+ target: result.target,
983
+ progress: result.progress,
984
+ counts: result.counts,
985
+ scopeCounts: Object.fromEntries(
986
+ Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
987
+ ),
988
+ unresolvedOwnership: ownership?.unresolvedAssignments || [],
989
+ firstAction: summarizeItem(result.firstAction),
990
+ stages: result.stages.map((stage) => ({
991
+ id: stage.id,
992
+ title: stage.title,
993
+ status: stage.status,
994
+ counts: stage.counts,
995
+ firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"))
996
+ }))
997
+ };
998
+ }
999
+
863
1000
  function eventWindowText(window) {
864
1001
  if (Number.isInteger(window?.endOffsetHours)) {
865
1002
  return window.endOffsetHours === 0 ? "due at event time" : `due within ${window.endOffsetHours} hours`;
@@ -245,7 +245,7 @@ export async function prepareEvidencePacket(input, options = {}) {
245
245
  end,
246
246
  timezone: loaded.workspace.timezone
247
247
  });
248
- const fileGRCRecords = datedRecords.filter((record) => (
248
+ const filegrcRecords = datedRecords.filter((record) => (
249
249
  !NON_EVIDENCE_RECORD_TYPES.has(record.type)
250
250
  && controlIdsForRecord(byId.get(record.id), byId).size
251
251
  ));
@@ -319,7 +319,7 @@ export async function prepareEvidencePacket(input, options = {}) {
319
319
  },
320
320
  summary: {
321
321
  datedRecords: datedRecords.length,
322
- fileGRCRecords: fileGRCRecords.length,
322
+ filegrcRecords: filegrcRecords.length,
323
323
  records: packetRecords.length,
324
324
  policies: policyIds.size,
325
325
  controls: controlIds.size,
@@ -335,7 +335,7 @@ export async function prepareEvidencePacket(input, options = {}) {
335
335
  warnings: warningCount
336
336
  },
337
337
  datedRecords: datedRecords.sort((a, b) => a.primaryDate.localeCompare(b.primaryDate) || byTitle(a, b)),
338
- fileGRCRecords: fileGRCRecords.sort((a, b) => a.primaryDate.localeCompare(b.primaryDate) || byTitle(a, b)),
338
+ filegrcRecords: filegrcRecords.sort((a, b) => a.primaryDate.localeCompare(b.primaryDate) || byTitle(a, b)),
339
339
  policies: [...policyIds].map((id) => recordSummary(byId.get(id))).filter(Boolean).sort(byTitle),
340
340
  controls: [...controlIds].map((id) => recordSummary(byId.get(id))).filter(Boolean).sort(byTitle),
341
341
  obligations,
@@ -557,7 +557,7 @@ async function writeChecksums(output, files) {
557
557
 
558
558
  function controlMatrixCsv(packet) {
559
559
  return csv([
560
- ["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "Frequency", "Operation Mode", "System IDs", "Requirement IDs", "Policy IDs", "Risk IDs", "FileGRC Evidence IDs", "External Evidence IDs", "Control Test IDs", "Test Outcomes", "Population IDs", "Population Counts", "Sample Sizes", "Exception Counts", "Population Evidence IDs", "Sample Evidence IDs"],
560
+ ["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "Frequency", "Operation Mode", "System IDs", "Requirement IDs", "Policy IDs", "Risk IDs", "filegrc Evidence IDs", "External Evidence IDs", "Control Test IDs", "Test Outcomes", "Population IDs", "Population Counts", "Sample Sizes", "Exception Counts", "Population Evidence IDs", "Sample Evidence IDs"],
561
561
  ...packet.controlCoverage.map((control) => [
562
562
  control.id,
563
563
  control.code,
@@ -592,15 +592,15 @@ function packetHandlingMarkdown(packet) {
592
592
  "",
593
593
  `Evidence classifications: ${packet.handling.classifications.join(", ") || "none recorded"}`,
594
594
  `External references present: ${packet.handling.containsExternalReferences ? "yes" : "no"}`,
595
- "Encrypted by FileGRC: no",
595
+ "Encrypted by filegrc: no",
596
596
  "",
597
597
  "Review every included record and attachment for secrets, unnecessary personal data, customer data, and material outside the audit scope before transfer.",
598
598
  "",
599
- "Review `external-evidence-index.csv` before delivery. It identifies references that FileGRC did not copy. Reconcile those items to the auditor portal or other approved system so the engagement team can confirm it received the same evidence indexed here.",
599
+ "Review `external-evidence-index.csv` before delivery. It identifies references that filegrc did not copy. Reconcile those items to the auditor portal or other approved system so the engagement team can confirm it received the same evidence indexed here.",
600
600
  "",
601
601
  "Transfer this directory through the auditor's approved encrypted channel. Do not email an unencrypted packet. Give access only to the engagement team and retain or remove exported copies under the organization's evidence-retention rules.",
602
602
  "",
603
- "After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`. FileGRC does not sign or encrypt the packet because those operations require organization-controlled keys and transfer-system choices.",
603
+ "After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`. filegrc does not sign or encrypt the packet because those operations require organization-controlled keys and transfer-system choices.",
604
604
  ""
605
605
  ].join("\n");
606
606
  }
@@ -972,7 +972,7 @@ function packetGaps({
972
972
  if (controlNeedsExternalEvidence(control, model) && !coverage.evidenceIds.length) {
973
973
  gaps.push(gap("error", "control-missing-external-evidence", `${coverage.code || coverage.title} relies on an external system but has no linked External Evidence in the packet.`, coverage.id));
974
974
  } else if (!controlNeedsExternalEvidence(control, model) && !coverage.operatingRecordIds.length) {
975
- gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated FileGRC operating record in the packet.`, coverage.id));
975
+ gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated filegrc operating record in the packet.`, coverage.id));
976
976
  }
977
977
  if (audit?.auditKind !== "soc-2-type-1" && coverage.status === "implemented" && !coverage.operatingRecordIds.length && coverage.operationMode !== "automated") {
978
978
  gaps.push(gap("warning", "control-missing-operating-record", `${coverage.code || coverage.title} has no dated operating record in the packet period.`, coverage.id));
@@ -1523,7 +1523,7 @@ function recordSummary(record) {
1523
1523
 
1524
1524
  function packetMarkdown(packet) {
1525
1525
  const readiness = packet.readiness.status === "delivery-ready"
1526
- ? "FileGRC management checks passed. The engagement team still determines whether the evidence is sufficient and appropriate."
1526
+ ? "filegrc management checks passed. The engagement team still determines whether the evidence is sufficient and appropriate."
1527
1527
  : `${packet.readiness.errors} errors and ${packet.readiness.warnings} warnings require review. This is a draft packet.`;
1528
1528
  const periodLabel = packet.period.basis === "as-of"
1529
1529
  ? `as of ${packet.period.start}`
@@ -1543,7 +1543,7 @@ function packetMarkdown(packet) {
1543
1543
  "",
1544
1544
  "## Coverage",
1545
1545
  "",
1546
- `- ${packet.summary.fileGRCRecords} FileGRC Evidence records`,
1546
+ `- ${packet.summary.filegrcRecords} filegrc Evidence records`,
1547
1547
  `- ${packet.summary.obligationOccurrences} recurring obligation occurrences`,
1548
1548
  `- ${packet.summary.eventRuns} event runs`,
1549
1549
  `- ${packet.summary.evidence} External Evidence records`,
@@ -1554,7 +1554,7 @@ function packetMarkdown(packet) {
1554
1554
  `- ${packet.summary.systems} in-scope systems`,
1555
1555
  `- ${packet.summary.sourceSystems} cataloged source systems`,
1556
1556
  "",
1557
- "Open `index.html` for the auditor-oriented index. `control-matrix.csv` cross-references criteria, controls, FileGRC Evidence, External Evidence, and tests. `source-system-index.csv` identifies the systems of record used to produce External Evidence. `external-evidence-index.csv` lists material that must be delivered or accessed outside this packet. For Type 2, `population-index.csv` records management's population reconciliation and fixed source exports. FileGRC records, governed Markdown, fixed attachments, and committed historical versions are included in their respective directories.",
1557
+ "Open `index.html` for the auditor-oriented index. `control-matrix.csv` cross-references criteria, controls, filegrc Evidence, External Evidence, and tests. `source-system-index.csv` identifies the systems of record used to produce External Evidence. `external-evidence-index.csv` lists material that must be delivered or accessed outside this packet. For Type 2, `population-index.csv` records management's population reconciliation and fixed source exports. filegrc records, governed Markdown, fixed attachments, and committed historical versions are included in their respective directories.",
1558
1558
  "",
1559
1559
  "After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`. The checksum file covers every other packet file.",
1560
1560
  ""
@@ -1569,7 +1569,7 @@ function packetHtml(packet) {
1569
1569
  : "<p>None.</p>";
1570
1570
  const gaps = packet.gaps.length
1571
1571
  ? `<ul>${packet.gaps.map((item) => `<li class="${item.severity}"><strong>${escapeHtml(item.severity)}</strong> ${escapeHtml(item.message)}</li>`).join("")}</ul>`
1572
- : "<p>FileGRC management checks passed. The engagement team still evaluates sufficiency and appropriateness.</p>";
1572
+ : "<p>filegrc management checks passed. The engagement team still evaluates sufficiency and appropriateness.</p>";
1573
1573
  const engagementDate = packet.period.basis === "as-of"
1574
1574
  ? `As of ${escapeHtml(packet.period.start)}`
1575
1575
  : `${escapeHtml(packet.period.start)} through ${escapeHtml(packet.period.end)}`;
@@ -1592,25 +1592,25 @@ function packetHtml(packet) {
1592
1592
  ? packet.eventRuns.map((run) => `<article><h3><a href="records/obligation-event/${encodeURIComponent(run.id)}.json">${escapeHtml(run.title)}</a></h3><p>${escapeHtml(run.occurredAt || run.occurredOn)} · ${escapeHtml(run.status)} · ${run.completeCount} of ${run.actions.length} complete</p><table><thead><tr><th>Required action</th><th>Policy cutoff</th><th>Status</th></tr></thead><tbody>${run.actions.map((action) => `<tr><td><a href="records/action-item/${encodeURIComponent(action.actionItemId)}.json">${escapeHtml(action.title)}</a></td><td>${escapeHtml(action.dueWindowEndAt || action.dueWindowEnd)}</td><td>${escapeHtml(action.status)}</td></tr>`).join("")}</tbody></table></article>`).join("")
1593
1593
  : "<p>No event workflows intersect this period.</p>";
1594
1594
  const recordsById = new Map(packet.records.map((record) => [record.id, record]));
1595
- const fileGRCRecords = packet.fileGRCRecords.length
1596
- ? `<table><thead><tr><th>Date</th><th>FileGRC record</th><th>Latest committed change</th></tr></thead><tbody>${packet.fileGRCRecords.map((item) => {
1595
+ const filegrcRecords = packet.filegrcRecords.length
1596
+ ? `<table><thead><tr><th>Date</th><th>filegrc record</th><th>Latest committed change</th></tr></thead><tbody>${packet.filegrcRecords.map((item) => {
1597
1597
  const history = recordsById.get(item.id)?.history?.[0];
1598
1598
  const source = history
1599
1599
  ? `${history.timestamp} · ${history.author} · ${history.subject}`
1600
1600
  : "No committed file history";
1601
1601
  return `<tr><td>${escapeHtml(item.primaryDate)}</td><td><a href="records/${encodeURIComponent(item.type)}/${encodeURIComponent(item.id)}.json">${escapeHtml(item.title)}</a><br><small>${escapeHtml(item.type)}</small></td><td>${escapeHtml(source)}</td></tr>`;
1602
1602
  }).join("")}</tbody></table>`
1603
- : "<p>No FileGRC Evidence records matched this period.</p>";
1603
+ : "<p>No filegrc Evidence records matched this period.</p>";
1604
1604
  const controlCoverage = packet.controlCoverage.length
1605
- ? `<p><a href="control-matrix.csv">Download control matrix CSV</a></p><table><thead><tr><th>Control</th><th>Status and scope</th><th>Criteria</th><th>FileGRC Evidence</th><th>External Evidence</th><th>Tests</th></tr></thead><tbody>${packet.controlCoverage.map((control) => `<tr><td><a href="records/control/${encodeURIComponent(control.id)}.json">${escapeHtml(control.code || control.id)}</a><small>${escapeHtml(control.title)}</small></td><td>${escapeHtml(control.status)}<small>${control.systemIds.map(escapeHtml).join(", ") || "No system scope"}</small></td><td>${control.requirementIds.map(escapeHtml).join("<br>") || "None"}</td><td>${control.operatingRecordIds.length}</td><td>${control.evidenceIds.length}</td><td>${control.tests.length}</td></tr>`).join("")}</tbody></table>`
1605
+ ? `<p><a href="control-matrix.csv">Download control matrix CSV</a></p><table><thead><tr><th>Control</th><th>Status and scope</th><th>Criteria</th><th>filegrc Evidence</th><th>External Evidence</th><th>Tests</th></tr></thead><tbody>${packet.controlCoverage.map((control) => `<tr><td><a href="records/control/${encodeURIComponent(control.id)}.json">${escapeHtml(control.code || control.id)}</a><small>${escapeHtml(control.title)}</small></td><td>${escapeHtml(control.status)}<small>${control.systemIds.map(escapeHtml).join(", ") || "No system scope"}</small></td><td>${control.requirementIds.map(escapeHtml).join("<br>") || "None"}</td><td>${control.operatingRecordIds.length}</td><td>${control.evidenceIds.length}</td><td>${control.tests.length}</td></tr>`).join("")}</tbody></table>`
1606
1606
  : "<p>No controls were selected.</p>";
1607
- const readinessLabel = packet.readiness.status === "delivery-ready" ? "FileGRC management checks passed" : "Draft, do not deliver";
1607
+ const readinessLabel = packet.readiness.status === "delivery-ready" ? "filegrc management checks passed" : "Draft, do not deliver";
1608
1608
  const packetDate = packet.period.basis === "as-of"
1609
1609
  ? `As of ${escapeHtml(packet.period.start)}`
1610
1610
  : `${escapeHtml(packet.period.start)} through ${escapeHtml(packet.period.end)}`;
1611
1611
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Evidence packet</title><style>
1612
1612
  body{font:14px/1.5 system-ui,sans-serif;color:#161825;max-width:1120px;margin:auto;padding:40px;background:#f7f8fc}header,section{background:#fff;border:1px solid #dfe3ef;border-radius:10px;padding:24px;margin:14px 0}h1,h2{margin-top:0}h1{font-size:26px}h2{font-size:17px}ul{padding-left:20px}li{margin:8px 0}small{display:block;color:#656c7e}.attachment{margin-right:10px;font-size:12px}.error{color:#8a2f28}.warning{color:#76500d}.readiness{display:inline-block;padding:5px 9px;border-radius:999px;background:#f7e4e2;color:#7a2520;font-weight:700}.readiness.ready{background:#e2f1e8;color:#245d3b}table{width:100%;border-collapse:collapse}th,td{padding:9px;border:1px solid #dfe3ef;text-align:left;vertical-align:top}code{overflow-wrap:anywhere}dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px}dt{font-weight:700}dd{margin:0}
1613
- </style></head><body><header><p>SOC 2 evidence packet</p><span class="readiness ${packet.readiness.status === "delivery-ready" ? "ready" : ""}">${escapeHtml(readinessLabel)}</span><h1>${packetDate}</h1><p>${escapeHtml(packet.workspace.organizationName)} · revision <code>${escapeHtml(packet.revision.commit || "uncommitted")}</code></p></header>${section("Engagement scope", engagement)}${section("Review status", gaps)}${section("Control coverage", controlCoverage)}${section("Systems of record", sourceSystems)}${packet.period.basis === "period" ? section("Management population reconciliation", populations) : ""}${packet.period.basis === "period" ? section("Recurring obligation coverage", obligations) : ""}${packet.period.basis === "period" ? section("Event workflow coverage", eventRuns) : ""}${section("Policies", links(packet.policies))}${section("FileGRC Evidence", fileGRCRecords)}${section("External Evidence", evidence)}${section("Integrity and history", "<p>Verify all transferred files with <code>SHA256SUMS</code>. Committed prior versions are under <code>history/</code> with an index that records their source paths and Git metadata.</p>")}</body></html>`;
1613
+ </style></head><body><header><p>SOC 2 evidence packet</p><span class="readiness ${packet.readiness.status === "delivery-ready" ? "ready" : ""}">${escapeHtml(readinessLabel)}</span><h1>${packetDate}</h1><p>${escapeHtml(packet.workspace.organizationName)} · revision <code>${escapeHtml(packet.revision.commit || "uncommitted")}</code></p></header>${section("Engagement scope", engagement)}${section("Review status", gaps)}${section("Control coverage", controlCoverage)}${section("Systems of record", sourceSystems)}${packet.period.basis === "period" ? section("Management population reconciliation", populations) : ""}${packet.period.basis === "period" ? section("Recurring obligation coverage", obligations) : ""}${packet.period.basis === "period" ? section("Event workflow coverage", eventRuns) : ""}${section("Policies", links(packet.policies))}${section("filegrc Evidence", filegrcRecords)}${section("External Evidence", evidence)}${section("Integrity and history", "<p>Verify all transferred files with <code>SHA256SUMS</code>. Committed prior versions are under <code>history/</code> with an index that records their source paths and Git metadata.</p>")}</body></html>`;
1614
1614
  }
1615
1615
 
1616
1616
  async function writePacketFile(output, relativePath, source, files) {
package/src/git.js CHANGED
@@ -227,7 +227,7 @@ async function pushWorkspaceUnlocked(root) {
227
227
 
228
228
  function syncReadySummary(root, action) {
229
229
  const summary = getGitSummary(root);
230
- if (!summary.available) throw new Error(`Git history is unavailable for this workspace, so FileGRC cannot ${action}.`);
230
+ if (!summary.available) throw new Error(`Git history is unavailable for this workspace, so filegrc cannot ${action}.`);
231
231
  if (!summary.branch) throw new Error(`Check out a branch before trying to ${action}.`);
232
232
  if (!summary.clean) throw new Error(`Commit or discard workspace changes before trying to ${action}.`);
233
233
  return summary;
package/src/index.js CHANGED
@@ -50,8 +50,8 @@ export {
50
50
  nextCalendarOccurrence
51
51
  } from "./recurrence.js";
52
52
  export { searchResources, searchableValues } from "./search.js";
53
- export { createFileGRCServer, serveWorkspace } from "./server.js";
54
- export { normalizeSetupPayload, setupWorkspace } from "./setup.js";
53
+ export { createFilegrcServer, serveWorkspace } from "./server.js";
54
+ export { normalizeSetupPayload, planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
55
55
  export { createAppState } from "./state.js";
56
56
  export { currentCalendarDate, formatCalendarDate, formatLocalDateTime } from "./time.js";
57
57
  export { validateWorkspace } from "./validate.js";
package/src/model-docs.js CHANGED
@@ -67,7 +67,7 @@ export function generateModelDocumentation(model) {
67
67
  "",
68
68
  ...model.evidenceSourceFamilies.map((item) => (
69
69
  item.collectionTestRequired === false
70
- ? `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} FileGRC operating records: ${item.operationRecordTypes.map((type) => `\`${type}\``).join(", ")}. No separate collection test is required. ${item.timing}`
70
+ ? `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} filegrc operating records: ${item.operationRecordTypes.map((type) => `\`${type}\``).join(", ")}. No separate collection test is required. ${item.timing}`
71
71
  : `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} Test external collection: ${item.testPrompt} ${item.timing}`
72
72
  )),
73
73
  "",
package/src/parties.js ADDED
@@ -0,0 +1,46 @@
1
+ const CURRENT_PERSON_STATUSES = new Set(["active", "external"]);
2
+ const CURRENT_TEAM_STATUSES = new Set(["active"]);
3
+
4
+ export function partyPeople(ids = [], byId, options = {}, seen = new Set()) {
5
+ const people = new Set();
6
+ for (const id of ids) {
7
+ if (seen.has(id)) continue;
8
+ seen.add(id);
9
+ const record = byId.get(id);
10
+ if (
11
+ record?.type === "person"
12
+ && (!options.personStatuses || options.personStatuses.has(record.status))
13
+ ) {
14
+ people.add(id);
15
+ }
16
+ if (
17
+ record?.type === "team"
18
+ && (!options.teamStatuses || options.teamStatuses.has(record.status))
19
+ ) {
20
+ for (const personId of partyPeople(
21
+ [...(record.memberIds || []), ...(record.chairIds || [])],
22
+ byId,
23
+ options,
24
+ seen
25
+ )) {
26
+ people.add(personId);
27
+ }
28
+ }
29
+ }
30
+ return people;
31
+ }
32
+
33
+ export function currentPartyPeople(ids = [], byId) {
34
+ return partyPeople(ids, byId, {
35
+ personStatuses: CURRENT_PERSON_STATUSES,
36
+ teamStatuses: CURRENT_TEAM_STATUSES
37
+ });
38
+ }
39
+
40
+ export function partiesIndependent(ownerIds = [], approverIds = [], byId) {
41
+ const owners = partyPeople(ownerIds, byId);
42
+ const approvers = partyPeople(approverIds, byId);
43
+ return owners.size > 0
44
+ && approvers.size > 0
45
+ && ![...owners].some((id) => approvers.has(id));
46
+ }
package/src/paths.js CHANGED
@@ -15,7 +15,7 @@ export function resolveWorkspaceRoot(input = process.cwd()) {
15
15
  current = parent;
16
16
  }
17
17
 
18
- throw new Error("No FileGRC workspace was found from the requested path.");
18
+ throw new Error("No filegrc workspace was found from the requested path.");
19
19
  }
20
20
 
21
21
  export function isWithin(parent, candidate) {
@@ -1,4 +1,7 @@
1
+ import { currentPartyPeople } from "./parties.js";
2
+
1
3
  export function obligationProgramStatus(obligation, byId, asOf) {
4
+ if (currentPartyPeople(obligation.ownerIds || [], byId).size === 0) return "proposed";
2
5
  const policyIds = obligation.policyIds || [];
3
6
  const policiesReady = policyIds.every((id) => {
4
7
  const policy = byId.get(id);