filegrc 0.2.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.
@@ -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,7 +50,7 @@ export {
50
50
  nextCalendarOccurrence
51
51
  } from "./recurrence.js";
52
52
  export { searchResources, searchableValues } from "./search.js";
53
- export { createFileGRCServer, serveWorkspace } from "./server.js";
53
+ export { createFilegrcServer, serveWorkspace } from "./server.js";
54
54
  export { normalizeSetupPayload, setupWorkspace } from "./setup.js";
55
55
  export { createAppState } from "./state.js";
56
56
  export { currentCalendarDate, formatCalendarDate, formatLocalDateTime } from "./time.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);
@@ -1,5 +1,5 @@
1
1
  export const RESOURCE_INSTRUCTIONS = {
2
- person: "Confirm the people who will own, approve, review, or operate the program. Replace the starter names and contact details with the organization’s actual people.",
2
+ person: "Confirm the policy owner created during setup, then add the actual people who will approve, review, or operate the program.",
3
3
  team: "Review the starter Security and Risk Oversight team, including its members and chair. Add another team only when the organization assigns shared responsibility to it.",
4
4
  system: "Catalog all in-scope systems for the program. Treat anything that operates a control or produces evidence as a System, including software provided by a vendor (like HR software).",
5
5
  vendor: "Catalog the companies that provide in-scope software or services. Link each vendor-provided System to the company that provides it.",
@@ -14,7 +14,7 @@ export const RESOURCE_INSTRUCTIONS = {
14
14
  "risk-assessment": "Complete and approve an assessment of the risks to the in-scope service, systems, vendors, and commitments.",
15
15
  risk: "Record each risk identified by an assessment or operating activity. Assign an owner, rate it, and document the chosen response.",
16
16
  obligation: "Review the recurring work proposed by effective policies. Confirm who owns it, when it is due, and what proof completion requires.",
17
- "obligation-event": "When a policy-triggering event occurs, record it here and complete the actions FileGRC creates for it.",
17
+ "obligation-event": "When a policy-triggering event occurs, record it here and complete the actions filegrc creates for it.",
18
18
  "policy-review": "Record scheduled and change-driven reviews of policies and governed documents, including the decision and any follow-up.",
19
19
  meeting: "Record required oversight meetings, including attendees, decisions, minutes, and follow-up work.",
20
20
  exception: "Record and approve any time-limited departure from a policy or control before the departure begins.",
@@ -64,13 +64,13 @@ export const PROGRAM_PATH = [
64
64
  description: "Ownership, criteria, and service boundary",
65
65
  summary: "Confirm the people and teams responsible for the program, set the management goal, review the criteria and customer commitments in scope, then define the customer-facing service, supporting systems, and supplier dependencies.",
66
66
  sections: [
67
- { id: "ownership", title: "Program Ownership", description: "Confirm the people and teams that own, approve, review, and operate the program.", steps: ["Replace the starter people with the organization’s actual owners and reviewers.", "Review the starter Security and Risk Oversight team, its members, and its chair.", "Add other teams only when the organization assigns shared responsibility to them."], types: ["person", "team"], defaultOpen: true },
67
+ { id: "ownership", title: "Program Ownership", description: "Confirm the people and teams that own, approve, review, and operate the program.", steps: ["Confirm the policy owner created during setup, then add the organization’s actual reviewers and operators.", "Review the starter Security and Risk Oversight team, its members, and its chair.", "Add other teams only when the organization assigns shared responsibility to them."], types: ["person", "team"], defaultOpen: true },
68
68
  { id: "criteria", title: "Criteria", description: "Confirm the criteria used for the program, resolve whether each requirement applies, and record customer commitments that shape the service or control design.", steps: ["Review the included Security criteria references.", "Mark each requirement applicable or not applicable with a rationale.", "Record customer commitments and keep optional criteria out until management deliberately adds them."], types: ["framework", "requirement", "commitment"], defaultOpen: true },
69
69
  { id: "boundary", title: "Service Boundary", description: "Record the service and its supporting technology and providers. An application or platform is a System because it operates controls or produces evidence; the company providing it is a Vendor because contracts, due diligence, and supplier risk belong to that relationship.", steps: ["Create a Vendor record for each material provider.", "Create System records for the customer-facing service and each supporting application, platform, or internal system that is in scope or produces evidence, then connect vendor-provided Systems to their providers.", "Assign owners, classification, dependencies, and a clear in-scope decision to each System, and keep supplier reviews with the Vendor."], types: ["vendor", "system"], defaultOpen: false }
70
70
  ],
71
71
  resourceTypes: ["person", "team", "framework", "requirement", "commitment", "vendor", "system"],
72
72
  commands: [
73
- "filegrc setup --help",
73
+ "filegrc setup",
74
74
  "filegrc guide person --json",
75
75
  "filegrc guide system --json",
76
76
  "filegrc list system --json"
@@ -99,7 +99,7 @@ export const PROGRAM_PATH = [
99
99
  description: "Tailor and finish the starter control set",
100
100
  summary: "Review the starter catalog against the scoped service, then give every applicable internal control an actual procedure, owner, system scope, cadence, policy and criteria mappings, authoritative evidence source, and implementation date. Mark it implemented only after the procedure is operating, then record any controls that customers or carved-out providers must perform.",
101
101
  sections: [
102
- { id: "catalog", title: "Control Catalog", description: "Finish the starter controls, record applicable complementary controls, and see whether FileGRC tracks operation through Work Queue or evidence records.", steps: ["Open every planned control and confirm its mappings and suggested frequency.", "Write the real procedure in Record Markdown and add system scope and evidence sources.", "Record any required customer or carved-out provider controls as Complementary Controls."], types: ["control", "complementary-control"], defaultOpen: true }
102
+ { id: "catalog", title: "Control Catalog", description: "Finish the starter controls, record applicable complementary controls, and see whether filegrc tracks operation through Work Queue or evidence records.", steps: ["Open every planned control and confirm its mappings and suggested frequency.", "Write the real procedure in Record Markdown and add system scope and evidence sources.", "Record any required customer or carved-out provider controls as Complementary Controls."], types: ["control", "complementary-control"], defaultOpen: true }
103
103
  ],
104
104
  resourceTypes: ["control", "complementary-control"],
105
105
  commands: [
@@ -170,7 +170,7 @@ export const PROGRAM_PATH = [
170
170
  {
171
171
  id: "policy-events",
172
172
  title: "Policy Events",
173
- instructions: "Trigger the matching workflow when an event occurs. FileGRC adds every required action to the Work Queue with its owner and deadline.",
173
+ instructions: "Trigger the matching workflow when an event occurs. filegrc adds every required action to the Work Queue with its owner and deadline.",
174
174
  use: "Preview the full workflow before triggering it, then create the event and every linked task in one validated write.",
175
175
  policyBasis: "Active event obligations translate policy-triggering changes into owned, deadline-bound Action Items. Proposed workflows remain unavailable until their governing policies and linked controls are ready.",
176
176
  commands: ["filegrc obligations --json", "filegrc trigger EVENT_TYPE (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --subject RESOURCE_ID --json"]
@@ -201,14 +201,14 @@ export const PROGRAM_PATH = [
201
201
  summary: "After the program is collecting reliable evidence, create an Audit record for the real CPA engagement, keep the firm-agreed report period separate from management’s candidate dates, complete management documents, populations, requests, evidence delivery, and fieldwork, then preserve the findings, responses, opinion, and final report.",
202
202
  sections: [
203
203
  { id: "engagement", title: "Engagement", description: "Record the actual CPA engagement, formal scope and dates, requests, and management responses.", steps: ["Create the Audit after the CPA firm is engaged.", "Record the firm-agreed type, scope, systems, criteria, and dates.", "Track incoming requests and approved response material."], types: ["audit", "audit-request"], defaultOpen: true },
204
- { id: "fieldwork", title: "Fieldwork", description: "Prepare management documents, reconcile Type 2 populations, review both evidence paths, support testing, and build the indexed packet.", steps: ["Initialize engagement-specific management documents and populations.", "Review dated FileGRC Evidence and verified External Evidence for the formal period.", "Reconcile complete populations, link samples, and resolve fieldwork requests and findings.", "Build the packet from a clean Git revision; it includes FileGRC records, Markdown, External Evidence, attachments, indexes, history, and checksums."], types: ["audit-population", "control-test"], utility: "audit-packet", defaultOpen: true }
204
+ { id: "fieldwork", title: "Fieldwork", description: "Prepare management documents, reconcile Type 2 populations, review both evidence paths, support testing, and build the indexed packet.", steps: ["Initialize engagement-specific management documents and populations.", "Review dated filegrc Evidence and verified External Evidence for the formal period.", "Reconcile complete populations, link samples, and resolve fieldwork requests and findings.", "Build the packet from a clean Git revision; it includes filegrc records, Markdown, External Evidence, attachments, indexes, history, and checksums."], types: ["audit-population", "control-test"], utility: "audit-packet", defaultOpen: true }
205
205
  ],
206
206
  resourceTypes: ["audit", "audit-request", "audit-population", "control-test"],
207
207
  utilities: [
208
208
  {
209
209
  id: "audit-packet",
210
210
  title: "Audit Evidence & Packet",
211
- instructions: "Review FileGRC Evidence and External Evidence for the formal period, complete engagement preparation, and build the indexed audit packet.",
211
+ instructions: "Review filegrc Evidence and External Evidence for the formal period, complete engagement preparation, and build the indexed audit packet.",
212
212
  use: "Prepare management documents and populations, answer fieldwork requests, review both evidence paths, and compile a delivery bound to a clean Git revision.",
213
213
  policyBasis: "Management prepares the scoped records, evidence, populations, assertions, and responses. The CPA firm selects samples, evaluates evidence and exceptions, and issues the report.",
214
214
  commands: ["filegrc audit-readiness AUDIT_ID --json", "filegrc evidence-packet --audit AUDIT_ID --preview --json"]
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { planObligations } from "./obligations.js";
3
3
  import { resolveDataPath } from "./paths.js";
4
4
  import { obligationIsRunning } from "./program-lifecycle.js";
5
+ import { currentPartyPeople, partiesIndependent, partyPeople } from "./parties.js";
5
6
  import { markdownEntries } from "./resource-markdown.js";
6
7
  import { currentCalendarDate } from "./time.js";
7
8
  import { loadWorkspace } from "./workspace.js";
@@ -33,7 +34,7 @@ export async function assessProgramReadiness(input, options = {}) {
33
34
  [...sourceStage.items, ...collectionStage.items]
34
35
  );
35
36
  const evidenceGateStages = [
36
- scopeStage(workspace, scope, records),
37
+ scopeStage(workspace, scope, records, byId),
37
38
  await policiesStage(scope, byId, readMarkdown, asOf),
38
39
  await controlsStage(scope, byId, readMarkdown, asOf),
39
40
  evidenceStage
@@ -116,7 +117,7 @@ function programScope(workspace, records, byId) {
116
117
  };
117
118
  }
118
119
 
119
- function scopeStage(workspace, scope, records) {
120
+ function scopeStage(workspace, scope, records, byId) {
120
121
  const items = [];
121
122
  const goal = workspace?.assuranceGoal || "none";
122
123
  items.push(item(
@@ -129,6 +130,8 @@ function scopeStage(workspace, scope, records) {
129
130
  workspace || { type: "workspace" }
130
131
  ));
131
132
 
133
+ items.push(programOwnershipItem(records, byId));
134
+
132
135
  const completeSystems = scope.systems.filter((system) => (
133
136
  system.status === "active"
134
137
  && system.inScope === true
@@ -175,7 +178,44 @@ function scopeStage(workspace, scope, records) {
175
178
  workspace || { type: "workspace" }
176
179
  ));
177
180
 
178
- return stage("scope", "Define Scope", "Set the management objective, service boundary, criteria, controls, and dependencies.", items);
181
+ return stage("scope", "Define Scope", "Set program ownership, the management objective, service boundary, criteria, controls, and dependencies.", items);
182
+ }
183
+
184
+ function programOwnershipItem(records, byId) {
185
+ const ownedRecords = records.filter((record) => (
186
+ ["policy", "control", "obligation"].includes(record.type)
187
+ && !["retired", "superseded", "not-applicable"].includes(record.status)
188
+ ));
189
+ const unresolved = ownedRecords.filter((record) => currentPartyPeople(record.ownerIds, byId).size === 0);
190
+ const currentOwners = new Set(ownedRecords.flatMap((record) => [...currentPartyPeople(record.ownerIds, byId)]));
191
+ const oversight = byId.get("team-security-risk-oversight");
192
+ const policyOwnerIds = new Set(records
193
+ .filter((record) => record.type === "policy" && !["retired", "superseded"].includes(record.status))
194
+ .flatMap((record) => [...partyPeople(record.ownerIds || [], byId)]));
195
+ const oversightChairs = oversight?.type === "team"
196
+ ? currentPartyPeople(oversight.chairIds || [], byId)
197
+ : new Set();
198
+ const oversightComplete = !oversight || (
199
+ oversight.type === "team"
200
+ && oversight.status === "active"
201
+ && currentPartyPeople(oversight.memberIds || [], byId).size > 0
202
+ && oversightChairs.size > 0
203
+ && ![...oversightChairs].some((id) => policyOwnerIds.has(id))
204
+ );
205
+ const complete = currentOwners.size > 0 && unresolved.length === 0 && oversightComplete;
206
+ const detail = [];
207
+ if (!currentOwners.size) detail.push("No current person owns the program records.");
208
+ if (unresolved.length) detail.push(`${unresolved.length} ownership ${unresolved.length === 1 ? "assignment does" : "assignments do"} not resolve to a current person.`);
209
+ if (!oversightComplete) detail.push("Finish and activate Security and Risk Oversight with a current chair who is separate from policy ownership.");
210
+ return item(
211
+ "program-ownership",
212
+ complete ? "complete" : "action",
213
+ "Confirm program owners and oversight",
214
+ complete
215
+ ? `${currentOwners.size} current ${currentOwners.size === 1 ? "person owns" : "people own"} the program records.${oversight ? " Security and Risk Oversight has a separate current chair." : ""}`
216
+ : detail.join(" "),
217
+ !oversightComplete ? oversight : unresolved[0] || { type: "person" }
218
+ );
179
219
  }
180
220
 
181
221
  async function policiesStage(scope, byId, readMarkdown, asOf) {
@@ -183,14 +223,11 @@ async function policiesStage(scope, byId, readMarkdown, asOf) {
183
223
  const policies = [...linkedPolicyIds].map((id) => byId.get(id)).filter((record) => (
184
224
  record?.type === "policy" && !["superseded", "retired"].includes(record.status)
185
225
  ));
186
- const approvers = policies.flatMap((policy) => partyPeople(policy.approverIds || [], byId))
226
+ const appointedReviewer = policies
227
+ .filter((policy) => partiesIndependent(policy.ownerIds, policy.approverIds, byId))
228
+ .flatMap((policy) => [...currentPartyPeople(policy.approverIds || [], byId)])
187
229
  .map((id) => byId.get(id))
188
- .filter(Boolean);
189
- const appointedReviewer = approvers.find((person) => (
190
- ["active", "external"].includes(person.status)
191
- && person.email
192
- && !(person.id === "person-independent-approver" && ["Independent Approver", "Independent Reviewer"].includes(person.title))
193
- ));
230
+ .find(Boolean);
194
231
  const items = [
195
232
  item(
196
233
  "independent-reviewer",
@@ -199,7 +236,7 @@ async function policiesStage(scope, byId, readMarkdown, asOf) {
199
236
  appointedReviewer
200
237
  ? `${appointedReviewer.title} is recorded as a reviewer separate from policy ownership.`
201
238
  : "Appoint a reviewer who is separate from the policy owner. The reviewer may be another person in the organization or an external person, and is separate from the CPA firm that may later perform the audit.",
202
- appointedReviewer || { type: "person", id: "person-independent-approver" }
239
+ appointedReviewer || { type: "person" }
203
240
  )
204
241
  ];
205
242
  if (!policies.length) {
@@ -355,7 +392,7 @@ function evidenceCollectionStage(scope, records, byId, model) {
355
392
  ? `${capture.title} is a draft. Open it, select the authoritative source System, collect the named artifact, and have another person verify it.`
356
393
  : capture
357
394
  ? `${capture.title} is ${capture.status} but must be verified before this family is ready.`
358
- : `Run and verify one test export or test capture from an authoritative source outside FileGRC, then link it to a family control.`,
395
+ : `Run and verify one test export or test capture from an authoritative source outside filegrc, then link it to a family control.`,
359
396
  capture || { type: "evidence" },
360
397
  {
361
398
  familyId: family.id,
@@ -549,26 +586,6 @@ function substantiveMarkdown(source) {
549
586
  return (source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || []).length >= 10;
550
587
  }
551
588
 
552
- function partiesIndependent(ownerIds = [], approverIds = [], byId) {
553
- const owners = new Set(partyPeople(ownerIds, byId));
554
- const approvers = partyPeople(approverIds, byId);
555
- return owners.size > 0 && approvers.length > 0 && !approvers.some((id) => owners.has(id));
556
- }
557
-
558
- function partyPeople(ids = [], byId, seen = new Set()) {
559
- const people = [];
560
- for (const id of ids) {
561
- if (seen.has(id)) continue;
562
- seen.add(id);
563
- const record = byId.get(id);
564
- if (record?.type === "person") people.push(id);
565
- if (record?.type === "team") {
566
- people.push(...partyPeople([...(record.memberIds || []), ...(record.chairIds || [])], byId, seen));
567
- }
568
- }
569
- return [...new Set(people)];
570
- }
571
-
572
589
  function policyCheckLabel(name) {
573
590
  return ({
574
591
  reviewed: "draft review",
package/src/server.js CHANGED
@@ -15,7 +15,7 @@ import { setupWorkspace } from "./setup.js";
15
15
  import { loadWorkspace } from "./workspace.js";
16
16
  import { APP_SCRIPT, APP_STYLES, renderIndex } from "./web.js";
17
17
 
18
- export function createFileGRCServer(input = process.cwd(), options = {}) {
18
+ export function createFilegrcServer(input = process.cwd(), options = {}) {
19
19
  return createHttpServer(async (request, response) => {
20
20
  try {
21
21
  if (!expectedHost(request, options.allowedHosts)) {
@@ -184,7 +184,7 @@ export async function serveWorkspace(input = process.cwd(), options = {}) {
184
184
  }
185
185
  const loaded = await loadWorkspace(input);
186
186
  getResourceDefinition(loaded.model, "workspace");
187
- const server = createFileGRCServer(loaded.root, { allowedHosts: [host] });
187
+ const server = createFilegrcServer(loaded.root, { allowedHosts: [host] });
188
188
  await new Promise((resolve, reject) => {
189
189
  server.once("error", reject);
190
190
  server.listen(port, host, resolve);
package/src/state.js CHANGED
@@ -47,7 +47,7 @@ export async function createAppState(input = process.cwd(), options = {}) {
47
47
  dataModelVersion: loaded.model.modelVersion,
48
48
  id: "workspace",
49
49
  type: "workspace",
50
- title: "FileGRC workspace",
50
+ title: "filegrc workspace",
51
51
  organizationName: "Workspace configuration unavailable",
52
52
  timezone: "UTC"
53
53
  };
package/src/validate.js CHANGED
@@ -3,6 +3,7 @@ import { getResourceDefinition } from "../model/index.js";
3
3
  import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
4
4
  import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
5
5
  import { obligationIsRunning } from "./program-lifecycle.js";
6
+ import { partyPeople } from "./parties.js";
6
7
  import { isMarkdownChoice, markdownEntries } from "./resource-markdown.js";
7
8
  import { currentCalendarDate, isRfc3339Timestamp } from "./time.js";
8
9
  import { indexResources, loadWorkspace } from "./workspace.js";
@@ -223,9 +224,10 @@ function validateEvidencePaths(record, path, diagnostics) {
223
224
  }
224
225
 
225
226
  function validateIndependentApproval(record, byId, path, diagnostics) {
226
- if (!["policy", "document"].includes(record.type) || !(record.approverIds || []).length) return;
227
- const owners = expandPeople(record.ownerIds || [], byId);
228
- const approvers = expandPeople(record.approverIds || [], byId);
227
+ if (!["policy", "document"].includes(record.type)) return;
228
+ if (!(record.approverIds || []).length) return;
229
+ const owners = partyPeople(record.ownerIds || [], byId);
230
+ const approvers = partyPeople(record.approverIds || [], byId);
229
231
  const overlap = [...owners].filter((id) => approvers.has(id));
230
232
  if (overlap.length) {
231
233
  diagnostics.push(error(
@@ -234,37 +236,6 @@ function validateIndependentApproval(record, byId, path, diagnostics) {
234
236
  `Approvers must be separate from owners, including through team membership: ${overlap.join(", ")}.`
235
237
  ));
236
238
  }
237
- if (["approved", "active"].includes(record.status)) {
238
- const incompleteStarterApprover = [...approvers]
239
- .map((id) => byId.get(id))
240
- .find((person) => (
241
- person?.id === "person-independent-approver"
242
- && ["Independent Approver", "Independent Reviewer"].includes(person.title)
243
- ));
244
- if (incompleteStarterApprover) {
245
- diagnostics.push(error(
246
- "independent-approver-not-appointed",
247
- path,
248
- `The selected approver "${incompleteStarterApprover.title}" is still the starter placeholder. Open People and replace it with the reviewer's actual name before approving this record. The reviewer may be internal or external but must be separate from the owner.`
249
- ));
250
- }
251
- }
252
- }
253
-
254
- function expandPeople(ids, byId, seen = new Set()) {
255
- const people = new Set();
256
- for (const id of ids) {
257
- if (seen.has(id)) continue;
258
- seen.add(id);
259
- const record = byId.get(id);
260
- if (record?.type === "person") people.add(id);
261
- if (record?.type === "team") {
262
- for (const personId of expandPeople([...(record.memberIds || []), ...(record.chairIds || [])], byId, seen)) {
263
- people.add(personId);
264
- }
265
- }
266
- }
267
- return people;
268
239
  }
269
240
 
270
241
  function validateObligation(record, path, diagnostics) {