filegrc 0.16.3 → 0.16.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { modelSupports } from "../model/index.js";
2
2
  import { createResourceId } from "./id.js";
3
3
  import { markdownEntries } from "./resource-markdown.js";
4
- import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
4
+ import { RESOURCE_INSTRUCTIONS, RESOURCE_OUTPUTS, resourceProgramContext } from "./program-path.js";
5
5
  import { assessCollectionReview } from "./collection-review.js";
6
6
  import { resolveProgram } from "./program.js";
7
7
 
@@ -80,6 +80,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
80
80
  title: definition.title,
81
81
  pluralTitle: definition.pluralTitle,
82
82
  instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
83
+ output: RESOURCE_OUTPUTS[type] || `A validated ${definition.title} record.`,
83
84
  use: definition.description,
84
85
  purpose: definition.description,
85
86
  programStep: resourceProgramContext(type, loaded.model),
@@ -69,8 +69,13 @@ export async function assessAuditPreparation(input, options = {}) {
69
69
  ? audits.find((record) => record.id === options.auditId)
70
70
  : options.selectDefault === false
71
71
  ? null
72
- : selectDefaultAudit(audits, calendarAsOf);
72
+ : selectDefaultAudit(options.programId && modelSupports(loaded.model, "program-scope")
73
+ ? audits.filter((record) => record.programId === options.programId)
74
+ : audits, calendarAsOf);
73
75
  if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
76
+ if (audit && options.programId && modelSupports(loaded.model, "program-scope") && audit.programId !== options.programId) {
77
+ throw new Error(`Audit "${audit.id}" does not belong to Program "${options.programId}".`);
78
+ }
74
79
 
75
80
  const formalPeriodEnd = audit?.auditKind === "soc-2-type-2" ? coverageEnd(audit.coverage) : null;
76
81
  const readinessAsOf = formalPeriodEnd && formalPeriodEnd < calendarAsOf ? formalPeriodEnd : calendarAsOf;
package/src/cli.js CHANGED
@@ -289,10 +289,11 @@ export async function runCli(argv = process.argv.slice(2)) {
289
289
  const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
290
290
  const programId = flags.program || audit?.programId;
291
291
  const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
292
- const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
292
+ const auditReadiness = await assessAuditPreparation(loaded, {
293
293
  auditId,
294
- asOf: flags["as-of"]
295
- }) : null;
294
+ asOf: flags["as-of"],
295
+ programId
296
+ });
296
297
  const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
297
298
  const output = selectProgramPathOutput(result, flags);
298
299
  if (flags.json) console.log(JSON.stringify(output, null, 2));
@@ -458,15 +459,29 @@ export async function runCli(argv = process.argv.slice(2)) {
458
459
  if (command === "program-readiness") {
459
460
  const loaded = await loadWorkspace(root);
460
461
  const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId: flags.program });
461
- const output = flags.summary ? summarizeProgramReadiness(result) : result;
462
+ if (flags.control && flags.summary) throw new Error("Use either --control or --summary for program-readiness.");
463
+ const controlItem = flags.control
464
+ ? result.stages.find(({ id }) => id === "controls")?.items.find(({ resourceType, resourceId }) => (
465
+ resourceType === "control" && resourceId === flags.control
466
+ ))
467
+ : null;
468
+ if (flags.control && !controlItem) throw new Error(`Control "${flags.control}" is not selected in this Program.`);
469
+ const output = controlItem
470
+ ? { asOf: result.asOf, programId: result.program?.id, ...controlItem }
471
+ : flags.summary ? summarizeProgramReadiness(result) : result;
462
472
  const headlineStatus = result.progress.status || result.status;
463
473
  if (flags.json) console.log(JSON.stringify(output, null, 2));
474
+ else if (controlItem) {
475
+ console.log(`${controlItem.status.toUpperCase()}\t${controlItem.title}`);
476
+ if (!controlItem.nextSteps?.length) console.log(` ${controlItem.message}`);
477
+ printGuidanceSteps(controlItem);
478
+ }
464
479
  else if (flags.summary) {
465
480
  console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
466
481
  for (const stage of output.stages) {
467
482
  console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} checks need action`);
468
483
  }
469
- if (output.firstAction) console.log(`Next: ${output.firstAction.title}\t${output.firstAction.message}`);
484
+ if (output.firstAction) printNextAction(output.firstAction);
470
485
  }
471
486
  else {
472
487
  console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
@@ -480,7 +495,10 @@ export async function runCli(argv = process.argv.slice(2)) {
480
495
  );
481
496
  for (const stage of result.stages) {
482
497
  console.log(`\n${stage.title}`);
483
- for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
498
+ for (const item of stage.items) {
499
+ console.log(`${item.status.toUpperCase()}\t${item.title}${item.nextSteps?.length ? "" : `\t${item.message}`}`);
500
+ printGuidanceSteps(item);
501
+ }
484
502
  }
485
503
  if (result.policyActivations.length) {
486
504
  console.log("\nPolicy activation assessments");
@@ -492,7 +510,7 @@ export async function runCli(argv = process.argv.slice(2)) {
492
510
  console.log(`\nEvidence Ready: management can start the candidate Type 2 period on or after ${result.suggestedCandidatePeriodStart || result.asOf}.`);
493
511
  }
494
512
  }
495
- if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
513
+ if (flags["require-ready"] && (controlItem ? controlItem.status !== "complete" : !result.evidenceReady)) process.exitCode = 2;
496
514
  return output;
497
515
  }
498
516
  if (command === "reporting-route-sets") {
@@ -1347,7 +1365,7 @@ Usage:
1347
1365
  filegrc list [resource-type] [--workflow] [--json]
1348
1366
  filegrc search <query> [--type resource-type] [--json]
1349
1367
  filegrc obligations [--program program-id] [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
1350
- filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
1368
+ filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary | --control CONTROL_ID] [--json]
1351
1369
  filegrc program-amendment <source-resource-id> [--json]
1352
1370
  filegrc review-bindings <retention-or-mapping-id> [--json]
1353
1371
  filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
@@ -1365,7 +1383,7 @@ Usage:
1365
1383
  filegrc policy-library [--json | --accept proposal-id --proposal-revision revision --yes]
1366
1384
  filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--program program-id] [--risk-level normal|high] [--subject resource-id[,resource-id]] [--title text] [--json]
1367
1385
  filegrc evidence-packet [--audit audit-id | --program program-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
1368
- filegrc get [resource-type] <id> [--mutation]
1386
+ filegrc get [resource-type] <id> [--mutation|--workflow]
1369
1387
  filegrc references <id> [--json]
1370
1388
  filegrc preview-mutation <preview.json|-> [--json]
1371
1389
  filegrc create <mutation.json|-> [--json]
@@ -1496,8 +1514,9 @@ source. No audit ID or CPA firm is required.
1496
1514
  Options:
1497
1515
  --program <id> Program to assess when more than one active Program exists
1498
1516
  --as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
1499
- --require-ready Exit with code 2 unless the Evidence Ready gate passes
1517
+ --require-ready Exit with code 2 unless the selected Control, or the full program, is ready
1500
1518
  --summary Omit item details and print stage counts and next actions
1519
+ --control <id> Show the current setup steps for one selected Control
1501
1520
  --json Print the result as JSON
1502
1521
  --root <path> Workspace path
1503
1522
  --help Show this help`);
@@ -1749,6 +1768,7 @@ function printAgentGuide(result) {
1749
1768
  console.log(`Program step: ${result.programStep.order ? `Step ${result.programStep.order}` : `Step ${result.programStep.number}`} · ${result.programStep.title}`);
1750
1769
  }
1751
1770
  console.log(`Instructions: ${result.instructions}`);
1771
+ console.log(`Output: ${result.output}`);
1752
1772
  console.log(`Use: ${result.use}`);
1753
1773
  console.log(`Policy basis: ${result.policyBasis}`);
1754
1774
  console.log(`Timing: ${result.cadence}`);
@@ -1810,23 +1830,21 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
1810
1830
  const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
1811
1831
  const stages = buildAgentProgramPath(model).map((stage) => {
1812
1832
  if (stage.id === "audit") {
1813
- const auditAction = auditReadiness?.firstAction || (
1814
- auditReadiness?.status === "not-started"
1815
- ? {
1816
- id: "create-audit",
1817
- status: "action",
1818
- title: "Create the planned CPA engagement",
1819
- message: "Create a planned Audit from the current management scope, then replace the remaining scaffold values with the CPA firm and firm-agreed scope and dates.",
1820
- resourceType: "audit",
1821
- commands: [
1822
- "npx filegrc guide audit --json",
1823
- "npx filegrc scaffold audit --title \"YEAR SOC 2 TYPE\" > audit-mutation.json",
1824
- "npx filegrc create audit-mutation.json --json",
1825
- "npx filegrc prepare-audit AUDIT_ID --json"
1826
- ]
1827
- }
1828
- : null
1829
- );
1833
+ const auditAction = auditReadiness?.status === "not-started"
1834
+ ? {
1835
+ id: "create-audit",
1836
+ status: "action",
1837
+ title: "Record the CPA engagement",
1838
+ message: "After engaging the CPA firm, create an Audit with the firm-agreed type, scope, and dates.",
1839
+ resourceType: "audit",
1840
+ commands: [
1841
+ "npx filegrc guide audit --json",
1842
+ "npx filegrc scaffold audit --title \"YEAR SOC 2 TYPE\" > audit-mutation.json",
1843
+ "npx filegrc create audit-mutation.json --json",
1844
+ "npx filegrc prepare-audit AUDIT_ID --json"
1845
+ ]
1846
+ }
1847
+ : auditReadiness?.stages?.flatMap(({ items }) => items).find(({ status }) => status === "action") || null;
1830
1848
  return {
1831
1849
  ...stage,
1832
1850
  status: auditReadiness?.status || "not-started",
@@ -1866,12 +1884,29 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
1866
1884
  };
1867
1885
  }
1868
1886
 
1887
+ function printGuidanceSteps(item) {
1888
+ item.nextSteps?.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
1889
+ }
1890
+
1891
+ function printNextAction(action) {
1892
+ if (action.nextSteps?.length) {
1893
+ console.log(`Next: ${action.title}`);
1894
+ printGuidanceSteps(action);
1895
+ } else {
1896
+ console.log(`Next: ${action.title} · ${action.message}`);
1897
+ }
1898
+ }
1899
+
1869
1900
  function printProgramPath(result) {
1870
1901
  console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
1871
1902
  console.log(`Evidence Ready: ${result.evidenceReady ? "yes" : "no"}; operating: ${result.operating ? "yes" : "no"}`);
1872
1903
  for (const stage of result.stages) {
1873
1904
  console.log(`\nStep ${stage.number}. ${stage.title} [${String(stage.status).toUpperCase()}]`);
1874
1905
  console.log(stage.summary);
1906
+ for (const section of stage.sections) {
1907
+ console.log(` ${section.title}`);
1908
+ section.steps.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
1909
+ }
1875
1910
  for (const page of stage.pages) {
1876
1911
  console.log(`${page.order ? `Step ${page.order}` : "Operating area"} · ${page.title} (${page.type || `utility:${page.utility}`})`);
1877
1912
  console.log(` ${page.summary}`);
@@ -1885,7 +1920,7 @@ function printProgramPath(result) {
1885
1920
  }
1886
1921
  console.log("Commands:");
1887
1922
  for (const command of stage.commands) console.log(` ${command}`);
1888
- for (const action of stage.nextActions) console.log(`Next: ${action.title} · ${action.message}`);
1923
+ for (const action of stage.nextActions) printNextAction(action);
1889
1924
  }
1890
1925
  }
1891
1926
 
@@ -1953,6 +1988,7 @@ function summarizePathAction(action) {
1953
1988
  status: action.status,
1954
1989
  title: action.title,
1955
1990
  message: action.message,
1991
+ ...(action.nextSteps?.length ? { nextSteps: action.nextSteps } : {}),
1956
1992
  ...(action.resourceType ? { resourceType: action.resourceType } : {}),
1957
1993
  ...(action.resourceId ? { resourceId: action.resourceId } : {})
1958
1994
  };
@@ -1998,13 +2034,13 @@ function printProgramPathOutput(result, flags) {
1998
2034
  console.log(`${String(stage.status).toUpperCase()}\tStep ${stage.number}\t${stage.title}`);
1999
2035
  }
2000
2036
  const current = result.stages.find(({ id }) => id === result.currentStep.id);
2001
- if (current?.nextAction) console.log(`Next: ${current.nextAction.title} · ${current.nextAction.message}`);
2037
+ if (current?.nextAction) printNextAction(current.nextAction);
2002
2038
  return;
2003
2039
  }
2004
2040
  if (flags.next) {
2005
2041
  console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
2006
2042
  if (result.step?.nextAction) {
2007
- console.log(`Next: ${result.step.nextAction.title} · ${result.step.nextAction.message}`);
2043
+ printNextAction(result.step.nextAction);
2008
2044
  }
2009
2045
  for (const command of result.step?.commands || []) console.log(` ${command}`);
2010
2046
  return;
@@ -2020,7 +2056,8 @@ function printWorkflow(result) {
2020
2056
  console.log(`\n${result.counts.findings.ready || 0} ready findings, ${result.counts.workItems.overdue || 0} overdue Work Items`);
2021
2057
  if (result.recommended) {
2022
2058
  console.log(`Next: ${result.recommended.title}`);
2023
- if (result.recommended.message) console.log(` ${result.recommended.message}`);
2059
+ if (!result.recommended.nextSteps?.length && result.recommended.message) console.log(` ${result.recommended.message}`);
2060
+ printGuidanceSteps(result.recommended);
2024
2061
  const command = result.recommended.nextAction?.command || result.recommended.actions?.[0]?.command;
2025
2062
  if (command) console.log(` ${command}`);
2026
2063
  }
@@ -2035,6 +2072,7 @@ function summarizeProgramReadiness(result) {
2035
2072
  status: item.status,
2036
2073
  title: item.title,
2037
2074
  ...(options.message === false ? {} : { message: item.message }),
2075
+ ...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
2038
2076
  ...(item.resourceType ? { resourceType: item.resourceType } : {}),
2039
2077
  ...(item.resourceId ? { resourceId: item.resourceId } : {})
2040
2078
  } : null;
package/src/index.js CHANGED
@@ -104,6 +104,7 @@ export {
104
104
  buildAgentProgramPath,
105
105
  PROGRAM_PATH,
106
106
  RESOURCE_INSTRUCTIONS,
107
+ RESOURCE_OUTPUTS,
107
108
  RESOURCE_PAGE_SUMMARIES,
108
109
  resourceProgramContext
109
110
  } from "./program-path.js";
@@ -1,53 +1,103 @@
1
1
  import { ACTIVE_MODEL_VERSION, modelSupports } from "../model/index.js";
2
2
 
3
3
  export const RESOURCE_INSTRUCTIONS = {
4
- program: "Define one management compliance or assurance Program with its goal, bounded Systems, selected Frameworks, Requirement applicability decisions, Controls, owners, risk method, and candidate period.",
5
- person: "Record each person’s actual organizational job title. Keep named program authority, such as CISO, DPO, Policy Owner, or team chair, in dated Appointment records.",
6
- appointment: "Record one person’s dated appointment to a named organizational or program responsibility. Scope it to the workspace, a team, or the records governed by that appointment.",
7
- team: "Review the starter Security and Risk Oversight team, including its members and chair. Membership and chairs are authoritative on the Team record.",
8
- system: "Start with the complete bounded System management governs or the auditor will examine. Record its purpose, services, boundary, exclusions, Information Types, owners, and any applicable continuity objectives.",
9
- component: "Add a Component only when it materially delivers a selected System, supports a Control, produces authoritative Evidence, or supports relevant operations. Give every System use a role and rationale.",
10
- vendor: "Catalog material external provider relationships. Link a supplied Component when it meets the Component inclusion rules, but do not mirror every Vendor into a Component.",
11
- classification: "Review the starter information-handling categories and ordering against management's actual protection decisions, then use the approved Classifications on inventory and Evidence Artifacts.",
12
- "information-type": "Define a stable category of information and its default Classification, then link it from Systems, Components, and Vendors.",
13
- framework: "Confirm the criteria framework and version used for the program.",
14
- requirement: "Keep the published criterion as catalog content. Record management applicability and rationale on the selected Program.",
15
- commitment: "Record supplemental customer promises and service requirements that shape the scope or control design. The Commitment’s systemIds and controlIds are authoritative for what fulfills it.",
16
- "requirement-mapping": "Use a Requirement Mapping when supplemental policies, contracts, privacy promises, frameworks, or other sources need explicit coverage semantics. Choose the comparison method and relationship, explain the rationale, and bind the review to every mapped source revision.",
17
- "reporting-route-set": "Prepare the normal and fallback ways people will send each report required by proposed program content, including where each channel goes and the role that keeps it usable. Commit the proposal in Step 1, approve it before cutover, use separate approval and ongoing authority Appointments, and create a successor when either channel changes.",
18
- policy: "Tailor each Policy to match what the company is committing to. Clear placeholders, assign an owner and separate approver, then bind approval to the reviewed content. Approval does not prove implementation. Activate the Policy during the Step 3 cutover after reviewing its implementation gaps.",
19
- document: "Complete required program Documents in Step 2, assign an owner and separate approver, and bind approval to the intended values and exact Markdown. Implement the linked requirements and activate that approved revision in Step 3. Prepare Audit Documents in Step 5.",
20
- control: "Finish each applicable starter Control with the procedure people follow, its owner, bounded System scope, operating Components, authoritative evidence-source Components, governing Policy and Requirement mappings, and implementation date. Put calendar and event schedules in Obligations.",
21
- "complementary-control": "Review whether any in-scope Control depends on a customer or carved-out provider action. Record each real dependency, or confirm that the current scope has none.",
22
- evidence: "Create an Evidence Artifact when a real export, report, screenshot, signed file, or approved external reference exists. Select its authoritative source Component, link the Controls and operating records it supports, retain the fixed artifact or reference, and have another person verify it before audit use.",
23
- "risk-assessment": "Complete and approve an assessment of the risks to the in-scope service, systems, vendors, and commitments.",
24
- risk: "Record each risk identified by an assessment or operating activity. Assign an owner, rate it, document the chosen response, and link the Controls that treat it from the Risk record.",
25
- obligation: "Review the recurring work proposed by effective policies. Confirm who owns it, when it is due, and what proof completion requires.",
26
- "retention-schedule-item": "Use one structured row for each reviewed retention rule. Name its Information Types, scope, cutoff, period, disposition, sources, owner, and approval. Keep unknown organization values planned for management review.",
27
- "obligation-event": "When a policy-triggering event occurs, record it here and complete the actions filegrc creates for it.",
28
- "policy-review": "Record scheduled and change-driven reviews of policies and governed documents, including the decision and any follow-up.",
29
- meeting: "Record required oversight meetings, including attendees, decisions, minutes, and follow-up work.",
30
- exception: "Record and approve any time-limited departure from a policy or control before the departure begins.",
31
- asset: "Keep the inventory of important devices, software, media, and records current, including ownership, custody, and status.",
32
- "vendor-review": "Document due diligence before relying on a provider, then repeat the review on schedule or after a material change.",
33
- "access-grant": "Record each person’s or service account’s access to a Component, including approval, provisioning, changes, and removal.",
34
- "access-review": "Review access on schedule, record each decision, and assign any access changes that result.",
35
- "service-account": "Catalog non-human accounts that need separate tracking, including their owner, purpose, System, privilege, and expiry.",
36
- training: "Review and approve the exact Training content in Step 2, then activate the unchanged revision during Step 3 after its linked Controls and assignment Obligations are ready.",
37
- attestation: "Record each person’s completion or acknowledgement against the exact policy or training revision.",
38
- "vulnerability-scan": "Record each required scan, including its scope, timing, result, and evidence.",
39
- vulnerability: "Track confirmed weaknesses that need separate remediation, acceptance, or closure.",
40
- "penetration-test": "Record each penetration test, including its provider, scope, period, result, and evidence.",
41
- incident: "Record qualifying security or privacy events and manage their response and follow-up.",
42
- "backup-test": "Record each restore test, including the Systems tested, result, timing, evidence, and follow-up.",
43
- exercise: "Record each incident or continuity exercise, including its objective, participants, result, and follow-up.",
44
- finding: "Create a Finding only for a confirmed gap that needs separate remediation tracking. Keep the report details in the source record’s Markdown, then assign the Finding, set its due date, and verify closure.",
45
- "action-item": "Create an Action Item only when follow-up needs its own assignee, deadline, and completion proof. Point it to the record that created the work, then work it from Work Queue.",
46
- audit: "Create this record after engaging the CPA firm, then select the Program and record the agreed scope, criteria, Systems, subservice treatments, and report period. Control Tests and Evidence Artifacts link back with auditId or auditIds.",
47
- "audit-request": "When FileGRC is the approved request tracker, record each request from the audit team, assign an owner and due date, and link the approved response and evidence.",
48
- "data-request": "Record privacy or contractual requests when they apply to the audit scope or the organization’s commitments.",
49
- "control-test": "Record a management Control Test only when management performs and reviews one. The CPA firm records its own independent testing separately.",
50
- "audit-population": "Record each complete Type 2 population with its source Component, fixed export, query, count, and reconciliation."
4
+ program: "Set the Program goal, owners, Systems, Frameworks, criteria decisions, and risk method.",
5
+ person: "Record the person’s actual job title and current status.",
6
+ appointment: "Assign a named authority to one Person with its start date and scope.",
7
+ team: "Confirm the Team’s members and chair.",
8
+ system: "Define the service boundary, exclusions, owners, and information it handles.",
9
+ component: "Record a material service or evidence source and its role in each linked System.",
10
+ vendor: "Record each material external provider and the Components it supplies.",
11
+ classification: "Confirm the handling categories and apply them to information and evidence.",
12
+ "information-type": "Name a stable category of information, set its default Classification, and link its uses.",
13
+ framework: "Select the criteria Framework and version used by the Program.",
14
+ requirement: "Review whether this criterion applies to the Program and record the decision and rationale.",
15
+ commitment: "Record a real customer or service promise and link the Systems and Controls that fulfill it.",
16
+ "requirement-mapping": "Compare the supplemental source with the Requirement or Control, record the relationship and rationale, then review the source revisions.",
17
+ "reporting-route-set": "Replace the reporting-route placeholders with real normal and fallback channels, name the responsible role, and commit the proposal.",
18
+ policy: "Replace Policy placeholders with actual commitments, then have a separate approver approve the exact Markdown revision.",
19
+ document: "Complete the Document Markdown and intended values, assign an owner, and have a separate approver approve the exact revision.",
20
+ control: "Open this Control’s current next steps. Set up the activity, record how it works, and mark it Implemented when it works.",
21
+ "complementary-control": "Record each real customer or carved-out provider dependency, or confirm that the current scope has none.",
22
+ evidence: "Record a real artifact or approved reference, link its source and supported work, then have another person verify it.",
23
+ "risk-assessment": "Assess the in-scope service, record conclusions, and obtain an independent review.",
24
+ risk: "Name the threat and impact, assign an owner, rate the risk, and record its response.",
25
+ obligation: "Confirm the owner, cadence or trigger, deadline, and proof, then enable the rule when its Control is ready.",
26
+ "retention-schedule-item": "Set the Information Types, scope, cutoff, period, disposition, and sources for one retention rule.",
27
+ "obligation-event": "Record the actual event date and subject, then complete its linked Work Queue actions.",
28
+ "policy-review": "Record the review date, reviewer, decision, and any required follow-up.",
29
+ meeting: "Record attendees, decisions, minutes, and assigned follow-up for the meeting.",
30
+ exception: "Record the scope, owner, approval, and expiry before the departure begins.",
31
+ asset: "Record an important Asset’s owner, custody, location, and status.",
32
+ "vendor-review": "Review one Vendor’s current risk and evidence, then record the decision and follow-up.",
33
+ "access-grant": "Record the approved privilege, recipient, Component, and provisioning or removal dates.",
34
+ "access-review": "Review the current access list, record each decision, and assign the required changes.",
35
+ "service-account": "Record a non-human account’s owner, purpose, System, privilege, and expiry.",
36
+ training: "Complete the content and audience, then have a separate approver approve the exact revision.",
37
+ attestation: "Record the person’s dated acknowledgement of the exact active content revision.",
38
+ "vulnerability-scan": "Record the scan scope, date, result, and supporting evidence.",
39
+ vulnerability: "Track a confirmed weakness through its owner, response, and closure proof.",
40
+ "penetration-test": "Record the test provider, scope, period, results, evidence, and follow-up.",
41
+ incident: "Record the event, response decisions, dates, evidence, and follow-up.",
42
+ "backup-test": "Record the restore target, date, result, evidence, and follow-up.",
43
+ exercise: "Record the scenario, participants, date, result, and follow-up.",
44
+ finding: "Record a confirmed gap that needs its own owner, due date, remediation, and verified closure.",
45
+ "action-item": "Assign separate follow-up with an owner, deadline, source record, and completion proof.",
46
+ audit: "After engaging the CPA firm, record the agreed type, scope, Systems, criteria, and period.",
47
+ "audit-request": "Record the auditor’s request, owner, due date, and approved response.",
48
+ "data-request": "Record the request scope, decision, response, and supporting evidence.",
49
+ "control-test": "Record a management test only when it was performed and reviewed; link its result and evidence.",
50
+ "audit-population": "Record the complete Type 2 population, source query, count, fixed export, and reconciliation."
51
+ };
52
+
53
+ export const RESOURCE_OUTPUTS = {
54
+ program: "A Program record with the selected goal, scope, owners, criteria decisions, and risk method.",
55
+ person: "A Person record with their actual job title and current status.",
56
+ appointment: "A dated Appointment that names the holder, authority, and scope.",
57
+ team: "A Team record with its current members and chair.",
58
+ system: "A bounded System record with its services, data, owners, and exclusions.",
59
+ component: "A Component record linked to each System it supports, with a role and rationale.",
60
+ vendor: "A Vendor record for each material provider relationship.",
61
+ classification: "Reviewed handling categories used by the program's records.",
62
+ "information-type": "An Information Type with its default Classification and actual uses.",
63
+ framework: "The Framework and version selected for the Program.",
64
+ requirement: "A reviewed applicability decision on the Program for each selected Requirement.",
65
+ commitment: "A Commitment linked to the Systems and Controls that fulfill it.",
66
+ "requirement-mapping": "A reviewed mapping with its comparison, rationale, and source revisions.",
67
+ "reporting-route-set": "A proposed set of normal and fallback reporting routes with an owner and approval path.",
68
+ policy: "An independently approved Policy bound to its exact Markdown revision.",
69
+ document: "A governed Document with reviewed Markdown, an owner, and a separate approver.",
70
+ control: "An implemented Control with its actual procedure, scope, owner, source, and start date.",
71
+ "complementary-control": "A recorded customer or provider dependency, or a current review confirming none.",
72
+ evidence: "A verified Evidence Artifact linked to its source and supported work.",
73
+ "risk-assessment": "A completed, independently reviewed Risk Assessment with its conclusions.",
74
+ risk: "A rated Risk with an owner, response, and linked treatment Controls.",
75
+ obligation: "An enabled schedule or event rule with an owner, deadline, and required proof.",
76
+ "retention-schedule-item": "A reviewed retention row with its scope, cutoff, period, disposition, and sources.",
77
+ "obligation-event": "A dated Policy Event with its linked Work Queue actions.",
78
+ "policy-review": "A dated review with its decision, reviewer, and follow-up.",
79
+ meeting: "Meeting minutes with attendees, decisions, and assigned follow-up.",
80
+ exception: "A time-limited Exception with approval, scope, and expiry.",
81
+ asset: "An inventory record with the Asset's owner, custody, and status.",
82
+ "vendor-review": "A dated Vendor Review with a decision, evidence, and follow-up.",
83
+ "access-grant": "A current Access Grant showing approval, privilege, and lifecycle dates.",
84
+ "access-review": "A dated Access Review with decisions and assigned changes.",
85
+ "service-account": "A Service Account record with its owner, purpose, privilege, and expiry.",
86
+ training: "Approved Training content bound to its exact revision.",
87
+ attestation: "A person's dated acknowledgement bound to the exact content revision.",
88
+ "vulnerability-scan": "A dated scan record with scope, results, and linked evidence.",
89
+ vulnerability: "A tracked weakness with its owner, response, and closure proof.",
90
+ "penetration-test": "A test record with scope, provider, results, and follow-up.",
91
+ incident: "An Incident record with response decisions, dates, evidence, and follow-up.",
92
+ "backup-test": "A dated restore test with its result, evidence, and follow-up.",
93
+ exercise: "A dated exercise with participants, result, and follow-up.",
94
+ finding: "A confirmed Finding with an owner, due date, remediation, and verified closure.",
95
+ "action-item": "An assigned Action Item with a deadline and completion proof.",
96
+ audit: "An Audit record with the CPA-agreed type, scope, and period.",
97
+ "audit-request": "An owned Audit Request with a due date and approved response.",
98
+ "data-request": "A Data Request record with its scope, decision, and response.",
99
+ "control-test": "A reviewed management Control Test with results and linked evidence.",
100
+ "audit-population": "A reconciled Type 2 population linked to its fixed source export."
51
101
  };
52
102
 
53
103
  export const RESOURCE_PAGE_SUMMARIES = {
@@ -68,7 +118,7 @@ export const RESOURCE_PAGE_SUMMARIES = {
68
118
  "retention-schedule-item": "Review each structured retention rule.",
69
119
  policy: "Tailor the starter Policy and have someone other than its owner approve it.",
70
120
  document: "Adapt and approve plans.",
71
- control: "Describe each Control and its evidence source.",
121
+ control: "Put each Control in place and record how it works.",
72
122
  "complementary-control": "Record customer or provider responsibilities, or confirm there are none.",
73
123
  audit: "Record the CPA engagement and scope.",
74
124
  "audit-request": "Track fieldwork requests and responses.",
@@ -82,11 +132,11 @@ export const PROGRAM_PATH = [
82
132
  number: 1,
83
133
  title: "Define Scope",
84
134
  description: "Ownership, criteria, and service boundary",
85
- summary: "Name the owners, criteria, service, Systems, and providers in scope.",
135
+ summary: "A reviewed Program scope with owners, criteria, Systems, and material providers.",
86
136
  sections: [
87
- { id: "ownership", title: "Program Ownership", description: "Confirm who owns the program, plus the normal security reporting channel and its fallback.", steps: ["Confirm the initial program lead’s actual job title and the separate Policy Owner Appointment.", "Add the organization’s real appointments, reviewers, and operators.", "Review the starter Security and Risk Oversight team, its members, and its chair.", "Replace the starter reporting-channel placeholders, propose the set, and commit that proposal before Step 1 is complete. Approve it before the later implementation cutover.", "Add other teams only when the organization assigns shared responsibility to them."], types: ["person", "appointment", "team", "reporting-route-set"], defaultOpen: true },
88
- { id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Confirm the Program goal, owners, risk method, and candidate period.", "Review the included Security criteria references and record each applicability decision on the Program.", "Replace each planned service-commitment prompt with the actual promise or requirement. Use Requirement Mappings only when a supplemental source needs an explicit coverage comparison. Keep optional criteria out until the company chooses to add them."], types: ["program", "framework", "requirement", "commitment", "requirement-mapping"], defaultOpen: true },
89
- { id: "boundary", title: "System Boundary", description: "Start with the bounded System. Add Components that materially deliver the service, support Controls, produce authoritative Evidence, or support relevant operations. Keep Vendor relationships and specific Assets separate.", steps: ["Create the complete bounded System and select it on the Program.", "Add only relevant Components, with a role and rationale for each System use.", "Create Vendors for material external provider relationships and link supplied Components when factual.", "Normalize Information Types and Classifications used by the System, Components, Vendors, Risks, and Evidence Artifacts."], types: ["system", "component", "vendor", "classification", "information-type"], defaultOpen: false }
137
+ { id: "ownership", title: "Program Ownership", description: "Confirm who owns the program, plus the normal security reporting channel and its fallback.", steps: ["Confirm the program lead, policy owner, independent reviewer, and oversight team.", "Replace the reporting-channel placeholders with real normal and fallback routes, then commit the proposal."], types: ["person", "appointment", "team", "reporting-route-set"], defaultOpen: true },
138
+ { id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Set the Program goal, owners, risk method, and tentative timing.", "Review each selected criterion for this service and record its applicability decision.", "Replace the starter Commitment prompt with the real promise; add only needed supplemental mappings."], types: ["program", "framework", "requirement", "commitment", "requirement-mapping"], defaultOpen: true },
139
+ { id: "boundary", title: "System Boundary", description: "Start with the bounded System. Add Components that materially deliver the service, support Controls, produce authoritative Evidence, or support relevant operations. Keep Vendor relationships and specific Assets separate.", steps: ["Define the service and exclusions in a bounded System selected by the Program.", "Add material Components and Vendors with their actual System relationships.", "Record the Information Types and Classifications the service uses."], types: ["system", "component", "vendor", "classification", "information-type"], defaultOpen: false }
90
140
  ],
91
141
  resourceTypes: ["person", "appointment", "team", "reporting-route-set", "program", "framework", "requirement", "commitment", "requirement-mapping", "system", "component", "vendor", "classification", "information-type"],
92
142
  commands: [
@@ -108,10 +158,10 @@ export const PROGRAM_PATH = [
108
158
  number: 2,
109
159
  title: "Approve Policies",
110
160
  description: "Approve governed content and retention decisions",
111
- summary: "Review and independently approve Policies, program Documents, Training content, and the Data Retention Schedule.",
161
+ summary: "Approved Policies, program Documents, Training content, and retention decisions.",
112
162
  sections: [
113
- { id: "policy-content", title: "Policies", description: "Review every program Policy, Document, and Training record in one table, then bind independent approval to each exact revision.", steps: ["Review each governed Markdown artifact and replace every organization placeholder.", "Confirm the owner, separate approver, linked Controls, audience, and intended values that apply to each artifact.", "Record approval and its date against the exact revision. Leave approved content inactive until its Step 3 implementation cutover."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/policies" }], defaultOpen: true },
114
- { id: "retention", title: "Data Retention Schedule", description: "Complete each row proposal, then approve the document and all proposed rows once.", steps: ["Review the schedule policy, legal holds, exceptions, and disposal requirements.", "Have the owner finish each row for the Information Types and operational sources the organization actually uses.", "Trigger one independent approval only after the document and every row proposal are complete."], types: [], utility: "retention-schedule", defaultOpen: true }
163
+ { id: "policy-content", title: "Policies", description: "Approve the program's governing content.", instructions: "Open each Policy, program Document, and Training record. Replace placeholders, confirm the owner and intended values, then have a separate person approve the exact Markdown revision.", output: "Approved content bound to the reviewed revisions.", steps: ["Replace placeholders with the company's real rules, plans, and training material.", "Confirm the owner, separate approver, linked Controls, audience, and intended values.", "Record the independent approval and date against each exact Markdown revision."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/policies" }], defaultOpen: true },
164
+ { id: "retention", title: "Data Retention Schedule", description: "Approve the schedule document and its completed rows together.", steps: ["Review the governing schedule and any legal hold or exception rules.", "Complete a row for each distinct information use, cutoff, retention period, and disposition.", "Have a separate reviewer approve the complete document and row set."], types: [], utility: "retention-schedule", defaultOpen: true }
115
165
  ],
116
166
  resourceTypes: [],
117
167
  supportingResourceTypes: ["policy", "document", "training", "retention-schedule-item"],
@@ -132,12 +182,12 @@ export const PROGRAM_PATH = [
132
182
  number: 3,
133
183
  title: "Implement Controls",
134
184
  description: "Implement and prepare to operate",
135
- summary: "Implement Controls, configure their operating schedules and evidence sources, then activate the approved program.",
185
+ summary: "Implemented Controls with ready evidence sources and enabled work schedules.",
136
186
  sections: [
137
- { id: "controls", title: "Controls", description: "Implement the selected Controls and connect their scope, procedures, mappings, and operation patterns.", steps: ["Open every planned Control and confirm its mappings and operation pattern.", "Write the real procedure in Record Markdown, add bounded System scope, and map the operating and authoritative evidence-source Components."], types: ["control"], defaultOpen: true },
138
- { id: "complementary-controls", title: "Complementary Controls", description: "Record customer or provider responsibilities that form part of the implementation boundary, or confirm there are none.", steps: ["Review whether an in-scope Control depends on a customer or carved-out provider action."], types: ["complementary-control"], defaultOpen: true },
139
- { id: "evidence-sources", title: "Evidence Sources", description: "Check that every Control points to an authoritative source that can produce its expected evidence.", steps: ["Confirm each source Component is active, has the right evidence-source role, names current access owners, and includes repeatable retrieval instructions."], types: [], utility: "evidence-sources", defaultOpen: true },
140
- { id: "obligations", title: "Obligations", description: "Configure the calendar and event schedules that operate the Controls.", steps: ["Review and enable every required calendar and event schedule."], types: ["obligation"], defaultOpen: true }
187
+ { id: "controls", title: "Controls", description: "Put each selected Control into use and record what is actually in place.", steps: ["Open each Control for its specific setup action and current missing checks.", "Configure the activity and record its procedure, Systems, owner, and evidence sources.", "Mark it Implemented with its real start date after its sources and schedules are ready."], types: ["control"], defaultOpen: true },
188
+ { id: "complementary-controls", title: "Complementary Controls", description: "Record customer or provider responsibilities that form part of the implementation boundary, or confirm there are none.", steps: ["Identify any customer or carved-out provider action an in-scope Control depends on.", "Record each dependency, or confirm that the current scope has none."], types: ["complementary-control"], defaultOpen: true },
189
+ { id: "evidence-sources", title: "Evidence Sources", description: "Check that every Control points to an authoritative source that can produce its expected evidence.", steps: ["For each Control family, link an active Component with the required source kind, access owners, and retrieval instructions."], types: [], utility: "evidence-sources", defaultOpen: true },
190
+ { id: "obligations", title: "Obligations", description: "Configure the calendar and event schedules that operate the Controls.", steps: ["Check the owner, trigger or cadence, due window, and required proof for each Obligation.", "Enable the rules that will run the implemented Controls."], types: ["obligation"], defaultOpen: true }
141
191
  ],
142
192
  resourceTypes: ["control", "complementary-control", "obligation"],
143
193
  commands: [
@@ -160,17 +210,17 @@ export const PROGRAM_PATH = [
160
210
  number: 4,
161
211
  title: "Operate the Program",
162
212
  description: "Run the work and retain dated proof",
163
- summary: "Complete scheduled and event work. Keep dated proof.",
213
+ summary: "Dated operating records and linked proof for work actually performed.",
164
214
  sections: [
165
- { id: "risk", title: "Risk", description: "Maintain the program’s risk assessments and risk register as the service, threats, suppliers, and control needs change.", steps: ["Complete and approve risk assessments on schedule and after material changes.", "Record risks that need treatment, acceptance, or ongoing tracking.", "Add or update controls when the assessment identifies a new or changed response."], types: ["risk-assessment", "risk"], defaultOpen: true },
166
- { id: "queue", title: "Work Queue", description: "Complete recurring occurrences, Policy Event tasks, and assigned follow-up within their required windows.", steps: ["Complete due work within its allowed window and link dated proof.", "Start Policy Events when hiring, departures, incidents, or material changes occur.", "Resolve every other open Action Item from the same queue."], types: ["obligation-event", "data-request"], utility: "obligation-board", defaultOpen: true },
167
- { id: "evidence", title: "Evidence Artifacts", description: "Create records only for real exports, reports, screenshots, signed files, or approved external references collected during operation.", steps: ["Create an Evidence Artifact when the artifact exists or an operating record needs fixed supporting proof.", "Select the authoritative source Component, link the Controls and source operating record, and retain the fixed attachment or approved reference.", "Record the collector and Classification, then have another person verify the artifact before audit use."], types: ["evidence"], defaultOpen: true },
168
- { id: "governance", title: "Governance", description: "Record formal reviews, oversight meetings, and approved policy or control exceptions.", steps: ["Complete scheduled policy reviews and oversight meetings.", "Record decisions, attendees, follow-up work, and evidence.", "Approve time-bound exceptions before the departure begins."], types: ["policy-review", "meeting", "exception"], defaultOpen: false },
169
- { id: "inventories", title: "Assets and Vendors", description: "Maintain the asset inventory and recurring reviews of supplier relationships during operation.", steps: ["Keep ownership, custody, status, and lifecycle current for important assets.", "Perform vendor reviews on schedule and after material supplier changes.", "Link fixed reports and review evidence to the operating records."], types: ["asset", "vendor-review"], defaultOpen: false },
170
- { id: "access-training", title: "Access and Training Completion", description: "Inventory service accounts and retain access decisions, Training assignments, and acknowledgements produced during operation.", steps: ["Catalog service accounts that need separate tracking.", "Preserve access approvals and removals as they occur, then complete periodic access reviews and resolve exceptions.", "Retain each Training assignment and Attestation against the exact active content revision."], types: ["service-account", "access-grant", "access-review", "attestation"], defaultOpen: false },
171
- { id: "security", title: "Security Operations", description: "Record vulnerability work, applicable penetration testing, and incident response activity for the period.", steps: ["Retain scan scope, results, vulnerabilities, remediation, and exceptions.", "When the approved applicability review requires penetration testing, record the test and follow-up findings.", "Start the incident workflow when a qualifying event occurs."], types: ["vulnerability-scan", "vulnerability", "penetration-test", "incident"], defaultOpen: false },
172
- { id: "resilience", title: "Resilience", description: "Preserve proof that backups, restoration, continuity, and incident exercises work as designed.", steps: ["Record backup restoration tests and their results.", "Run continuity and incident exercises on schedule.", "Assign and close follow-up work from failed objectives or lessons learned."], types: ["backup-test", "exercise"], defaultOpen: false },
173
- { id: "issues", title: "Issues and Remediation", description: "Keep observations in the source report and track only confirmed gaps that need a separate remediation lifecycle.", steps: ["Create a Finding only when a confirmed gap needs its own owner, due date, status, or verified closure.", "Use the Finding itself for straightforward remediation; create Action Items only for separate assigned tasks.", "Work Action Items from Work Queue and close the Finding only after remediation is independently verified."], types: ["finding"], defaultOpen: false }
215
+ { id: "risk", title: "Risk", description: "Maintain the program’s risk assessments and risk register as the service, threats, suppliers, and control needs change.", steps: ["Complete and approve the required Risk Assessments.", "Record each risk that needs a response, owner, and treatment Controls."], types: ["risk-assessment", "risk"], defaultOpen: true },
216
+ { id: "queue", title: "Work Queue", description: "Complete recurring occurrences, Policy Event tasks, and assigned follow-up within their required windows.", steps: ["Complete due Work Queue occurrences with dated proof.", "Trigger a Policy Event when its real-world event occurs, then complete the linked Action Items."], types: ["obligation-event", "data-request"], utility: "obligation-board", defaultOpen: true },
217
+ { id: "evidence", title: "Evidence Artifacts", description: "Create records only for real exports, reports, screenshots, signed files, or approved external references collected during operation.", steps: ["Create an Evidence Artifact only when a real file or approved external reference exists.", "Link its source Component, supported Controls and work record, and fixed artifact or reference.", "Record the collector and Classification, then verify the artifact."], types: ["evidence"], defaultOpen: true },
218
+ { id: "governance", title: "Governance", description: "Record formal reviews, oversight meetings, and approved policy or control exceptions.", steps: ["Complete scheduled policy reviews and oversight meetings with decisions and evidence.", "Create separate Action Items only for follow-up with its own owner and deadline.", "Approve any time-limited Exception before the departure begins."], types: ["policy-review", "meeting", "exception"], defaultOpen: false },
219
+ { id: "inventories", title: "Assets and Vendors", description: "Maintain the asset inventory and recurring reviews of supplier relationships during operation.", steps: ["Keep important Assets and Vendor relationships current.", "Complete Vendor Reviews on schedule or after material changes, with a decision and evidence."], types: ["asset", "vendor-review"], defaultOpen: false },
220
+ { id: "access-training", title: "Access and Training Completion", description: "Inventory service accounts and retain access decisions, Training assignments, and acknowledgements produced during operation.", steps: ["Catalog Service Accounts that need separate tracking.", "Record access decisions and removals as they occur; complete scheduled Access Reviews.", "Keep Training assignments and Attestations tied to the exact active content revision."], types: ["service-account", "access-grant", "access-review", "attestation"], defaultOpen: false },
221
+ { id: "security", title: "Security Operations", description: "Record vulnerability work, applicable penetration testing, and incident response activity for the period.", steps: ["Record required scans and tests with their real scope, results, and evidence.", "Track confirmed weaknesses and their remediation.", "Start the Incident workflow when a qualifying event occurs."], types: ["vulnerability-scan", "vulnerability", "penetration-test", "incident"], defaultOpen: false },
222
+ { id: "resilience", title: "Resilience", description: "Preserve proof that backups, restoration, continuity, and incident exercises work as designed.", steps: ["Run scheduled restore tests and continuity or incident exercises.", "Record the result and evidence, then assign follow-up for failed objectives."], types: ["backup-test", "exercise"], defaultOpen: false },
223
+ { id: "issues", title: "Issues and Remediation", description: "Keep observations in the source report and track only confirmed gaps that need a separate remediation lifecycle.", steps: ["Create a Finding for a confirmed gap that needs a separate owner, deadline, or closure review.", "Use Action Items only for separately assigned tasks.", "Close the Finding after remediation is independently verified."], types: ["finding"], defaultOpen: false }
174
224
  ],
175
225
  resourceTypes: [
176
226
  "risk-assessment",
@@ -238,10 +288,10 @@ export const PROGRAM_PATH = [
238
288
  number: 5,
239
289
  title: "Audit",
240
290
  description: "Firm, formal period, fieldwork, and report",
241
- summary: "Track the CPA engagement, fieldwork, and evidence packet.",
291
+ summary: "A CPA engagement record and a reviewable, period-bound evidence packet.",
242
292
  sections: [
243
- { 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 },
244
- { id: "fieldwork", title: "Fieldwork", description: "Prepare Audit Documents, reconcile Type 2 populations, review both evidence paths, support testing, and build the indexed packet.", steps: ["Initialize and complete engagement-specific management Documents and populations.", "Approve and activate each Audit Document only when its engagement facts and timing are final.", "Review dated FileGRC operating records and verified Evidence Artifacts 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, Evidence Artifacts, attachments, indexes, history, and checksums."], types: ["audit-population", "control-test"], relatedLinks: [{ type: "document", label: "Audit Documents", href: "#/resources/document?stage=audit&documentScope=audit" }], utility: "audit-packet", defaultOpen: true }
293
+ { id: "engagement", title: "Engagement", description: "Record the actual CPA engagement, formal scope and dates, requests, and management responses.", steps: ["After engaging the CPA firm, create the Audit with its agreed type, scope, and date or period.", "Track firm requests with owners, due dates, and approved responses."], types: ["audit", "audit-request"], defaultOpen: true },
294
+ { id: "fieldwork", title: "Fieldwork", description: "Complete engagement documents, populations, requests, and the evidence packet.", steps: ["Complete engagement Documents and, for Type 2, reconcile each population to its fixed export.", "Review dated operating records and verified Evidence Artifacts for the agreed period; link samples and answer requests.", "Build and review the indexed packet from a clean Git revision."], types: ["audit-population", "control-test"], relatedLinks: [{ type: "document", label: "Audit Documents", href: "#/resources/document?stage=audit&documentScope=audit", summary: "Complete and approve engagement Documents, then link the required ones from each Audit.", instructions: "Complete each required engagement Document, link it from its Audit, obtain separate approval of the exact Markdown revision, then activate that approved revision for that Audit.", output: "Active Audit Documents linked to their Audits and bound to their approved revisions." }], utility: "audit-packet", defaultOpen: true }
245
295
  ],
246
296
  resourceTypes: ["audit", "audit-request", "audit-population", "control-test"],
247
297
  utilities: [
@@ -257,9 +307,12 @@ export const PROGRAM_PATH = [
257
307
  ],
258
308
  commands: [
259
309
  "filegrc guide audit --json",
310
+ "filegrc guide document --json",
311
+ "filegrc list document --json",
260
312
  "filegrc scaffold audit --title \"YEAR SOC 2 TYPE\"",
261
313
  "filegrc create AUDIT-MUTATION.json --json",
262
314
  "filegrc prepare-audit AUDIT_ID --json",
315
+ "filegrc activate-documents --scaffold --audit AUDIT_ID",
263
316
  "filegrc audit-readiness AUDIT_ID --json",
264
317
  "filegrc evidence-packet --audit AUDIT_ID --preview --json"
265
318
  ]
@@ -321,14 +374,27 @@ export function buildAgentProgramPath(model) {
321
374
  title: definition.pluralTitle,
322
375
  summary: RESOURCE_PAGE_SUMMARIES[type] || definition.description,
323
376
  instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
377
+ output: RESOURCE_OUTPUTS[type] || `A validated ${definition.title} record.`,
324
378
  use: definition.description,
325
379
  policyBasis: definition.guidance.policyBasis,
326
380
  guide: `npx filegrc guide ${type} --json`,
327
381
  list: `npx filegrc list ${type} --json`
328
382
  };
329
383
  });
384
+ const relatedPages = stage.sections.flatMap((section) => section.relatedLinks || [])
385
+ .filter((link) => link.instructions)
386
+ .map((link, index) => ({
387
+ order: `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + index)}`,
388
+ type: link.type,
389
+ title: link.label,
390
+ summary: link.summary,
391
+ instructions: link.instructions,
392
+ output: link.output,
393
+ guide: `npx filegrc program-path --json`,
394
+ list: `npx filegrc list ${link.type} --json`
395
+ }));
330
396
  const utilityPages = (stage.utilities || []).map((utility, index) => ({
331
- order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + index)}`,
397
+ order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + relatedPages.length + index)}`,
332
398
  utility: utility.id,
333
399
  title: utility.title,
334
400
  summary: utility.summary,
@@ -340,7 +406,7 @@ export function buildAgentProgramPath(model) {
340
406
  return {
341
407
  ...stage,
342
408
  commands: stage.commands.map(agentCommand),
343
- pages: stage.id === "run" ? utilityPages : [...resourcePages, ...utilityPages],
409
+ pages: stage.id === "run" ? utilityPages : [...resourcePages, ...relatedPages, ...utilityPages],
344
410
  ...(stage.id === "run" ? { operatingRecords: resourcePages } : {})
345
411
  };
346
412
  });
@@ -1508,10 +1508,12 @@ function legacyPolicyLibraryProposals(records) {
1508
1508
 
1509
1509
  async function controlsStage(scope, byId, readMarkdown, asOf, model) {
1510
1510
  const items = [];
1511
+ const families = selectedControlFamilies(scope.controls, model);
1511
1512
  if (!scope.controls.length) {
1512
1513
  items.push(item("control-scope", "action", "Select the program controls", "No controls are selected for the management program.", { type: "control" }));
1513
1514
  }
1514
1515
  for (const control of scope.controls) {
1516
+ const controlFamilies = families.filter((family) => family.controls.some(({ id }) => id === control.id));
1515
1517
  const source = await readMarkdown(control);
1516
1518
  const sourceSystems = (
1517
1519
  modelSupports(model, "component-sources")
@@ -1523,6 +1525,18 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
1523
1525
  && record.status !== "retired"
1524
1526
  && (record.controlIds || []).includes(control.id)
1525
1527
  ));
1528
+ const sourceDetails = await Promise.all(sourceSystems.map(async (sourceRecord) => ({
1529
+ record: sourceRecord,
1530
+ instructions: await readMarkdown(sourceRecord)
1531
+ })));
1532
+ const missingSourceFamilies = controlFamilies.filter((family) => !sourceDetails.some(({ record, instructions }) => (
1533
+ record.status === "active"
1534
+ && (record.evidenceSourceKinds || []).length > 0
1535
+ && (!family.sourceKinds.length || family.sourceKinds.some((kind) => (record.evidenceSourceKinds || []).includes(kind)))
1536
+ && (record.evidenceOwnerIds || []).length > 0
1537
+ && substantiveMarkdown(instructions)
1538
+ && openPlaceholderCount(instructions) === 0
1539
+ )));
1526
1540
  const checks = {
1527
1541
  ...(model.resources.control?.fields?.applicabilityReview ? {
1528
1542
  applicability: control.applicabilityReview?.decision === "applicable"
@@ -1534,6 +1548,9 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
1534
1548
  scope: (control.systemIds || []).some((id) => scope.systems.some((system) => system.id === id)),
1535
1549
  operationPattern: Boolean(control.operationPattern),
1536
1550
  evidenceSource: sourceSystems.length > 0,
1551
+ ...(modelSupports(model, "component-sources") ? {
1552
+ evidenceSourceReady: missingSourceFamilies.length === 0
1553
+ } : {}),
1537
1554
  implementationDate: Boolean(control.effectiveOn && control.effectiveOn <= asOf),
1538
1555
  ...(model.resources.control?.fields?.procedureRevision ? {
1539
1556
  procedureRevision: Boolean(control.procedureRevision),
@@ -1547,25 +1564,21 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
1547
1564
  } : {})
1548
1565
  };
1549
1566
  const missing = Object.entries(checks).filter(([, value]) => !value).map(([name]) => controlCheckLabel(name));
1567
+ const nextSteps = controlImplementationSteps(control, checks, {
1568
+ missingSourceFamilies
1569
+ });
1550
1570
  items.push(item(
1551
1571
  `control-${control.id}`,
1552
1572
  missing.length ? "action" : "complete",
1553
1573
  `${control.code ? `${control.code}: ` : ""}${control.title}`,
1554
1574
  missing.length
1555
- ? `Complete ${missing.length} ${missing.length === 1 ? "check" : "checks"} before implementation: ${missing.join(", ")}.`
1575
+ ? nextSteps[0] || `Complete the remaining Control checks: ${missing.join(", ")}.`
1556
1576
  : `Implemented ${control.effectiveOn}; owned, scoped, scheduled, documented, mapped, and tied to ${sourceSystems.length} authoritative ${sourceSystems.length === 1 ? "source" : "sources"}.`,
1557
1577
  control,
1558
1578
  {
1559
1579
  checks,
1560
- commands: [
1561
- ...(Object.hasOwn(checks, "applicability") && !checks.applicability ? [
1562
- "npx filegrc review-applicability --scaffold --type control > control-decisions.json",
1563
- "npx filegrc review-applicability control-decisions.json --preview --json",
1564
- "npx filegrc review-applicability control-decisions.json --yes --json"
1565
- ] : []),
1566
- "npx filegrc evidence-map --json",
1567
- `npx filegrc get ${shellArgument(control.id)} --mutation`
1568
- ],
1580
+ nextSteps,
1581
+ commands: [`npx filegrc get ${shellArgument(control.id)} --mutation`],
1569
1582
  workQueue: queueSchedules.length ? {
1570
1583
  enabled: queueSchedules.filter(obligationIsEnabled).length,
1571
1584
  running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model)).length,
@@ -1574,7 +1587,7 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
1574
1587
  }
1575
1588
  ));
1576
1589
  }
1577
- return stage("controls", "Implement Controls", "Each implemented Control needs an owner, actual procedure, scope, operation pattern, evidence source, mappings, an implementation date, and any required Obligations enabled. Scheduled work stays dormant until its governing Policy, program Documents, and Training are active and effective.", items);
1590
+ return stage("controls", "Implement Controls", "Put each selected Control in place, record how it works, and mark it Implemented when it is working.", items);
1578
1591
  }
1579
1592
 
1580
1593
  async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
@@ -1930,6 +1943,7 @@ function controlCheckLabel(name) {
1930
1943
  scope: "in-scope systems",
1931
1944
  operationPattern: "operation pattern",
1932
1945
  evidenceSource: "authoritative evidence source",
1946
+ evidenceSourceReady: "usable evidence source",
1933
1947
  implementationDate: "implementation date",
1934
1948
  procedureRevision: "effective procedure revision",
1935
1949
  procedureEffective: "procedure effective date",
@@ -1939,6 +1953,76 @@ function controlCheckLabel(name) {
1939
1953
  })[name] || name;
1940
1954
  }
1941
1955
 
1956
+ const starterControlSetup = {
1957
+ "control-security-governance": "Name a reviewer outside day-to-day security ownership and set up a quarterly security review.",
1958
+ "control-policy-management": "Assign someone to maintain the Control list and a separate approver for policies and plans. Set up their review path.",
1959
+ "control-security-communication": "Set up how the approved reporting routes, policy changes, and security notices reach staff and outside parties.",
1960
+ "control-workforce-expectations": "Set up the checks, agreements, and policy acknowledgements required before someone receives sensitive access.",
1961
+ "control-security-training": "Choose how new and existing workers receive security training, acknowledge the current material, and get reminders.",
1962
+ "control-risk-assessment": "Use the Program's risk method to assess the scoped service and record the results in FileGRC Risk Assessments and Risks.",
1963
+ "control-monitoring-remediation": "Define how Control problems are reported, reviewed, and verified closed. Use FileGRC Findings for gaps that need separate follow-up.",
1964
+ "control-access-authorization": "Configure unique, least-privilege accounts in the identity system. Restrict account changes to authorized admins.",
1965
+ "control-strong-authentication": "Turn on required MFA and secure sign-in settings for the in-scope Systems. Identify any gap that needs an approved Exception.",
1966
+ "control-access-review-offboarding": "Confirm the identity system can produce a complete access list and admins can promptly remove or change permissions.",
1967
+ "control-physical-workspace-security": "Decide how visitors, work areas, devices, and paper records are protected in the places your team actually works.",
1968
+ "control-data-classification-inventory": "Apply the approved Classifications to important data in the in-scope Systems and record its location in the inventory.",
1969
+ "control-encryption-transmission": "Configure encryption for in-scope data and devices, and assign who manages keys and approved transfer methods.",
1970
+ "control-data-retention-disposal": "Apply the approved retention schedule to live, backup, and vendor-held data. Define how each class is deleted or destroyed.",
1971
+ "control-inventory-configuration": "List important assets and owners, choose their secure settings, and decide how unsupported assets are handled.",
1972
+ "control-endpoint-protection": "Apply the required device settings, updates, encryption, screen lock, and malware protection to devices that access company Systems.",
1973
+ "control-network-security": "Restrict production network paths and remote access, and document the rules that allow traffic between environments.",
1974
+ "control-change-management": "Set up a change path with risk review, testing, approval, deployment, and rollback for software and infrastructure changes.",
1975
+ "control-vulnerability-management": "Choose what will be scanned, how often, who handles findings, and the target times for fixing them.",
1976
+ "control-penetration-testing": "Decide whether independent testing is needed for this service. If it is, set its scope, cadence, and owner.",
1977
+ "control-logging-monitoring": "Enable useful security logs and alerts for important Systems, protect them, and choose who responds to alerts.",
1978
+ "control-incident-response": "Set up a way to report, assign, escalate, contain, and close security incidents using the approved response plan.",
1979
+ "control-incident-exercise": "Choose a realistic incident scenario and an alert path to test, and assign the people who will run the exercise.",
1980
+ "control-backup-restoration": "Choose what needs backup or another recovery path, configure it, and decide how restores will be tested.",
1981
+ "control-continuity-exercise": "Set recovery priorities, contacts, and responsibilities, and choose how the continuity plan will be exercised.",
1982
+ "control-vendor-due-diligence": "Set a security review and contract check before a new Vendor gets sensitive data or becomes a material dependency.",
1983
+ "control-vendor-monitoring": "Choose which Vendors need ongoing review, who reviews them, and what information the review uses.",
1984
+ "control-security-exceptions": "Document how an Exception is reviewed and approved before a departure from policy begins. Record actual decisions in FileGRC Exceptions."
1985
+ };
1986
+
1987
+ export function controlImplementationSteps(control, checks, options = {}) {
1988
+ const steps = [];
1989
+ if (!checks.implemented) steps.push(starterControlSetup[control.id]
1990
+ || `Set up this Control: ${control.activity || control.statement}`);
1991
+ const record = [];
1992
+ if (!checks.owner) record.push("choose an owner");
1993
+ if (!checks.scope) record.push("select the Systems this covers");
1994
+ if (!checks.operationPattern) record.push("set when it runs");
1995
+ if (!checks.procedure) record.push("write who does what and when in Procedure");
1996
+ if (!checks.evidenceSource) record.push("link the tool or system that can show it happened");
1997
+ if (!checks.policyMapping && checks.policyMapping !== undefined) record.push("link the Policy it implements");
1998
+ if (!checks.criteriaMapping && checks.criteriaMapping !== undefined) record.push("link the criteria it covers");
1999
+ if (checks.implemented && !checks.implementationDate) record.push("enter the date it started working");
2000
+ if (checks.implemented && (checks.procedureRevision === false || checks.procedureEffective === false)) {
2001
+ record.push("record the Procedure revision and effective date");
2002
+ }
2003
+ if (record.length) steps.push(`In this Control, ${record.join("; ")}.`);
2004
+ const sourceFamilies = options.missingSourceFamilies || [];
2005
+ const sourceTargets = sourceFamilies.map((family) => `${family.title}${family.sourceKinds?.length ? ` (${family.sourceKinds.join(" or ")})` : ""}`);
2006
+ const sourceStep = checks.evidenceSourceReady === false
2007
+ ? `Cover the missing evidence families: ${sourceTargets.length ? sourceTargets.join(" and ") : "this Control"}. For each, link an active Component with the matching kind, evidence owners, and report retrieval instructions.`
2008
+ : null;
2009
+ const queueStep = checks.workQueue === false
2010
+ ? "Enable a linked Obligation with the owner and schedule for this Control's recurring or event work."
2011
+ : null;
2012
+ if (!checks.implemented) {
2013
+ if (sourceStep || queueStep) steps.push([sourceStep, queueStep].filter(Boolean).join(" "));
2014
+ else if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
2015
+ else steps.push(checks.procedureRevision === undefined
2016
+ ? "When it works, mark this Control Implemented and enter its start date."
2017
+ : "When it works, mark this Control Implemented. Enter its start date and the Procedure revision and effective date.");
2018
+ } else {
2019
+ if (sourceStep) steps.push(sourceStep);
2020
+ if (queueStep) steps.push(queueStep);
2021
+ if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
2022
+ }
2023
+ return steps;
2024
+ }
2025
+
1942
2026
  function assuranceGoalLabel(goal) {
1943
2027
  if (goal === "soc-2-type-1") return "SOC 2 Type 1";
1944
2028
  if (goal === "soc-2-type-2") return "SOC 2 Type 2";
package/src/state.js CHANGED
@@ -277,6 +277,7 @@ async function createAppStateUnlocked(input, options) {
277
277
  (audits.length ? audits : [null]).map(async (audit) => {
278
278
  const preparation = await assessAuditPreparation(loaded, {
279
279
  auditId: audit?.id,
280
+ programId: activeProgram.id,
280
281
  asOf,
281
282
  generatedAt,
282
283
  ...(audit ? {} : { programReadiness })
package/src/web.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  utcCalendarDate,
11
11
  validCalendarRecurrence
12
12
  } from "./recurrence.js";
13
- import { PROGRAM_PATH, programPathForModel, RESOURCE_INSTRUCTIONS, RESOURCE_PAGE_SUMMARIES } from "./program-path.js";
13
+ import { PROGRAM_PATH, programPathForModel, RESOURCE_INSTRUCTIONS, RESOURCE_OUTPUTS, RESOURCE_PAGE_SUMMARIES } from "./program-path.js";
14
14
  import { formatCalendarDate, formatLocalDateTime } from "./time.js";
15
15
 
16
16
  export function dashboardProgramReadiness(programReadiness = {}) {
@@ -96,6 +96,7 @@ let READINESS_STAGES = SHARED_PROGRAM_STAGES.map((stage) => ({
96
96
  number: String(stage.number)
97
97
  }));
98
98
  const RESOURCE_GUIDE_INSTRUCTIONS = ${JSON.stringify(RESOURCE_INSTRUCTIONS)};
99
+ const RESOURCE_GUIDE_OUTPUTS = ${JSON.stringify(RESOURCE_OUTPUTS)};
99
100
  const dashboardProgramReadiness = ${dashboardProgramReadiness.toString()};
100
101
  const workflowHrefBelongsToPage = ${workflowHrefBelongsToPage.toString()};
101
102
  const pageActionIdentity = ${pageActionIdentity.toString()};
@@ -246,6 +247,7 @@ function normalizeAppState(next) {
246
247
  function blockingStateSections(route) {
247
248
  if (route.name === "home") return ["program", "obligations", "workflow"];
248
249
  if (route.name === "repository") return ["repository"];
250
+ if (route.name === "list" && route.type === "control") return ["program"];
249
251
  if (route.name === "list" && route.type === "obligation") return ["obligations"];
250
252
  if (route.name === "obligations" || route.name === "stage" && route.stageId === "run") return ["program", "obligations"];
251
253
  if (route.name === "audit-packet") return ["repository", "program", "obligations", "audits"];
@@ -613,7 +615,7 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
613
615
  const progress = stageProgress(stage);
614
616
  main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
615
617
  '<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
616
- renderStagePageIndex(stage) + (stage.id === "controls" ? renderFinishStepThree() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
618
+ renderStageInstructions(stage) + renderStagePageIndex(stage) + (stage.id === "controls" ? renderFinishStepThree() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
617
619
  main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
618
620
  main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
619
621
  event.currentTarget.remove();
@@ -634,7 +636,8 @@ function renderPoliciesPage(main, params = new URLSearchParams()) {
634
636
  }
635
637
 
636
638
  function policyContentGuide() {
637
- return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use Policies" hidden><div><span>Instructions</span><p>Open each Policy, program Document, and Training record. Replace starter placeholders, review its Markdown, and confirm its owner, separate approver, relationships, and intended values.</p></div><div><span>Approval</span><p>Bind approval to the exact Markdown revision. Keep approved content inactive until Step 3 confirms implementation and records the activation cutover.</p></div><div><span>Included here</span><p>Use this page for governed program content. The Data Retention Schedule has its own Step 2 page because its document and structured rows are approved together.</p></div></section>';
639
+ const section = READINESS_STAGES.find(({ id }) => id === "policies")?.sections.find(({ id }) => id === "policy-content");
640
+ return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use Policies" hidden><div><span>Do now</span><p>' + esc(section?.instructions || "Review and approve the program's governing content.") + '</p></div><div><span>Output</span><p>' + esc(section?.output || "Approved content bound to the reviewed revisions.") + '</p></div></section>';
638
641
  }
639
642
 
640
643
  function renderRetentionSchedulePage(main, params = new URLSearchParams()) {
@@ -846,7 +849,7 @@ function renderPolicyApprovalGuidance() {
846
849
  : "";
847
850
  return '<article><strong>' + esc(proposal.title) + '</strong><p>' + esc(proposal.message) + '</p>' + review + '<div class="evidence-map-references">' + proposal.policyIds.map((id) => formatReference(id)).join("") + '</div></article>';
848
851
  }).join("");
849
- return '<section class="policy-lifecycle-note panel ' + (proposalRows ? "" : "single") + '"><div><p class="kicker">Step 2 approval</p><h3>Approve the governed content</h3><p>Approval means your company reviewed and accepted the requirements and intended values in each Policy, program Document, and Training record. It does not mean the linked Controls are implemented yet.</p><p>Bind each approval to the exact Markdown revision here. Implement Controls and configure Obligations in Step 3, then activate each unchanged approved revision at implementation cutover.</p></div>' + (proposalRows ? '<div class="policy-library-proposals">' + proposalRows + '</div>' : "") + '</section>';
852
+ return '<section class="policy-lifecycle-note panel ' + (proposalRows ? "" : "single") + '"><div><p class="kicker">Step 2 approval</p><h3>Approve the governed content</h3><p>Replace placeholders and confirm the intended values. Have a separate approver review the exact Markdown revision and record the approval date.</p></div>' + (proposalRows ? '<div class="policy-library-proposals">' + proposalRows + '</div>' : "") + '</section>';
850
853
  }
851
854
 
852
855
  function renderPoliciesTable(params = new URLSearchParams()) {
@@ -963,7 +966,8 @@ function renderDocumentActivationAssessments() {
963
966
  }
964
967
 
965
968
  function renderAuditDocumentActivationAssessments() {
966
- const audits = resourcesOfType("audit").map(({ record }) => record);
969
+ const audits = resourcesOfType("audit").map(({ record }) => record)
970
+ .filter((record) => !state.selectedProgramId || record.programId === state.selectedProgramId);
967
971
  const audit = selectDefaultAudit(audits, state.programReadiness?.asOf || currentDate());
968
972
  const preparation = audit ? state.auditPreparations?.[audit.id] : null;
969
973
  const assessments = preparation?.documentActivations || [];
@@ -1182,7 +1186,10 @@ function workflowGuidance(options = {}) {
1182
1186
  const visible = items.slice(0, 6);
1183
1187
  const renderRow = (item) => {
1184
1188
  const href = workflowItemHref(item);
1185
- const body = '<span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong><small>' + esc(item.message || workflowItemDetail(item)) + '</small></span>';
1189
+ const guidance = options.type === "control" && item.nextSteps?.length
1190
+ ? '<ol class="control-next-steps">' + item.nextSteps.map((step) => '<li>' + esc(step) + '</li>').join("") + '</ol>'
1191
+ : '<small>' + esc(item.message || workflowItemDetail(item)) + '</small>';
1192
+ const body = '<span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong>' + guidance + '</span>';
1186
1193
  return href ? '<a href="' + href + '">' + body + '</a>' : '<div>' + body + '</div>';
1187
1194
  };
1188
1195
  const rows = visible.map(renderRow).join("");
@@ -1750,6 +1757,12 @@ function renderStagePageIndex(stage) {
1750
1757
  return '<section class="stage-pages" aria-label="Step ' + esc(stage.number) + ' pages"><div class="stage-page-grid">' + cards + '</div></section>';
1751
1758
  }
1752
1759
 
1760
+ function renderStageInstructions(stage) {
1761
+ return '<details class="stage-instructions"><summary>How to complete Step ' + esc(stage.number) + '</summary><div class="stage-instructions-body">' + stage.sections.map((section) => (
1762
+ '<section><h3>' + esc(section.title) + '</h3><ol>' + section.steps.map((step) => '<li>' + esc(step) + '</li>').join("") + '</ol></section>'
1763
+ )).join("") + '</div></details>';
1764
+ }
1765
+
1753
1766
  function stagePageDestinations(stage) {
1754
1767
  return stage.sections.flatMap((section) => sectionDestinations(section)
1755
1768
  .filter((destination) => destination.utility !== "obligation-board")
@@ -1820,7 +1833,7 @@ function derivedStagePageState(stage, destination) {
1820
1833
  if (
1821
1834
  stage.id === "audit"
1822
1835
  && state.workflow?.assessments?.auditReadiness?.status === "not-started"
1823
- && (destination.type === "audit" || destination.utility === "audit-packet")
1836
+ && (destination.type === "audit" || destination.type === "document" && destination.href.includes("documentScope=audit") || destination.utility === "audit-packet")
1824
1837
  ) {
1825
1838
  return { complete: false, label: "No engagement" };
1826
1839
  }
@@ -1855,6 +1868,11 @@ function derivedStagePageState(stage, destination) {
1855
1868
  ? { complete: true, label: "Reviewed" }
1856
1869
  : { complete: false, label: "Review scope" };
1857
1870
  }
1871
+ if (stage.id === "audit" && destination.type === "document" && destination.href.includes("documentScope=audit")) {
1872
+ if (!resourcesOfType("document").some(({ record }) => auditSpecificDocument(record))) {
1873
+ return { complete: false, countsTowardProgress: false, label: "Only if needed" };
1874
+ }
1875
+ }
1858
1876
  if (destination.type && resourcesOfType(destination.type).length === 0) {
1859
1877
  return { complete: false, countsTowardProgress: false, label: "Only if needed" };
1860
1878
  }
@@ -2005,7 +2023,7 @@ function sectionDestinations(section) {
2005
2023
  destinations.push({ type, kind: "Record page", label: titleCase(definition.pluralTitle), href: "#/resources/" + encodeURIComponent(type), description: definition.description });
2006
2024
  }
2007
2025
  for (const link of section.relatedLinks || []) {
2008
- destinations.push({ type: link.type, kind: "Record page", label: link.label, href: link.href, description: section.description });
2026
+ destinations.push({ type: link.type, kind: "Record page", label: link.label, href: link.href, description: link.summary || section.description });
2009
2027
  }
2010
2028
  if (section.utility === "obligation-board") destinations.push({ utility: section.utility, kind: "Working page", label: "Work Queue", href: "#/stage/run", description: "Complete recurring work, Policy Event tasks, and assigned follow-up with its due windows and linked proof." });
2011
2029
  if (section.utility === "evidence-sources") destinations.push({ utility: section.utility, kind: "Coverage page", label: "Evidence Sources", href: "#/evidence-sources", description: section.description });
@@ -2083,6 +2101,7 @@ function renderObligations(main, params = new URLSearchParams()) {
2083
2101
  : "";
2084
2102
  main.innerHTML = '<div class="page obligation-board-page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
2085
2103
  '<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(stageProgress(stage)) + '</section>' +
2104
+ renderStageInstructions(stage) +
2086
2105
  feedback +
2087
2106
  operationGate +
2088
2107
  activeOperation +
@@ -2744,6 +2763,13 @@ function openNextAuditCycleDialog(prior) {
2744
2763
  });
2745
2764
  }
2746
2765
 
2766
+ function controlApplicabilityPending(record) {
2767
+ if (!record.applicabilityReview) return true;
2768
+ const item = state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items
2769
+ .find(({ resourceType, resourceId }) => resourceType === "control" && resourceId === record.id);
2770
+ return item?.checks?.applicability === false;
2771
+ }
2772
+
2747
2773
  function openApplicabilityReviewDialog(type, entries) {
2748
2774
  const definition = state.model.resources[type];
2749
2775
  const reviewPoints = definition?.guidance?.reviewPoints || [];
@@ -2752,8 +2778,9 @@ function openApplicabilityReviewDialog(type, entries) {
2752
2778
  const pending = entries.filter(({ record }) => (
2753
2779
  type === "requirement" && modelSupports("program-scope")
2754
2780
  ? !reviewedRequirements.has(record.id)
2755
- : !record.applicabilityReview
2756
- || type === "requirement" && record.applicability === "undetermined"
2781
+ : type === "control"
2782
+ ? controlApplicabilityPending(record)
2783
+ : !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
2757
2784
  ));
2758
2785
  const dialog = document.createElement("dialog");
2759
2786
  dialog.className = "commit-dialog applicability-dialog";
@@ -3102,7 +3129,9 @@ function renderList(main, type, params = new URLSearchParams()) {
3102
3129
  const hasPendingApplicability = entries.some(({ record }) => (
3103
3130
  type === "requirement" && modelSupports("program-scope")
3104
3131
  ? !reviewedRequirements.has(record.id)
3105
- : !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
3132
+ : type === "control"
3133
+ ? controlApplicabilityPending(record)
3134
+ : !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
3106
3135
  ));
3107
3136
  const applicabilityButton = !state.readOnly
3108
3137
  && ["requirement", "control", "commitment", "complementary-control"].includes(type)
@@ -3112,8 +3141,11 @@ function renderList(main, type, params = new URLSearchParams()) {
3112
3141
  const guideTrigger = '<button class="guide-trigger" id="resource-guide-trigger" type="button" aria-label="About ' + esc(definition.pluralTitle) + '" aria-haspopup="dialog" aria-controls="resource-guide" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button>';
3113
3142
  const listTools = '<div class="list-tools list-header-tools"><label><span class="sr-only">Filter list</span><input id="list-search" type="search" placeholder="Filter ' + esc(definition.pluralTitle.toLowerCase()) + '"></label>' +
3114
3143
  filters.map(({ name, label, values }) => '<select class="field-filter" data-field="' + esc(name) + '" aria-label="Filter by ' + esc(label.toLowerCase()) + '"><option value="">Any ' + esc(properCase(label)) + '</option>' + values.map((value) => '<option value="' + esc(value) + '">' + esc(filterOptionLabel(value)) + '</option>').join("") + '</select>').join("") + '<span id="result-count" aria-live="polite">' + entries.length + ' records</span>' + applicabilityButton + createButton + '</div>';
3115
- const pageSummary = STAGE_PAGE_SUMMARIES[type] || definition.description;
3116
- main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(listStage?.title || groupTitle(definition.group)) + '</p><div class="page-title-line"><h2>' + esc(titleCase(listTitle)) + '</h2>' + guideTrigger + '</div><p>' + esc(pageSummary) + '</p></div>' + listTools + '</div>' + resourceGuide(type) +
3144
+ const relatedGuidance = type === "document" && documentScope === "audit"
3145
+ ? listStage?.sections.flatMap((section) => section.relatedLinks || []).find((link) => link.type === "document" && link.instructions)
3146
+ : null;
3147
+ const pageSummary = relatedGuidance?.summary || STAGE_PAGE_SUMMARIES[type] || definition.description;
3148
+ main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(listStage?.title || groupTitle(definition.group)) + '</p><div class="page-title-line"><h2>' + esc(titleCase(listTitle)) + '</h2>' + guideTrigger + '</div><p>' + esc(pageSummary) + '</p></div>' + listTools + '</div>' + resourceGuide(type, relatedGuidance) +
3117
3149
  collectionReviewPanel(type) +
3118
3150
  '<section class="record-table-wrap"><table class="record-table"><thead><tr><th>' + esc(fieldLabel(type, "title")) + '</th>' + fields.map((name) => '<th>' + esc(fieldLabel(type, name)) + '</th>').join("") + '<th>Next action</th><th>Git file</th></tr></thead><tbody id="record-rows"></tbody></table></section>' +
3119
3151
  '<nav class="pagination list-pagination" aria-label="' + esc(definition.pluralTitle) + ' pages" hidden><button class="button" type="button" data-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-page="next">Next</button></nav></div>';
@@ -3316,7 +3348,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
3316
3348
  });
3317
3349
  const mayDelete = !state.readOnly && !routeSetLocked && !definition.singleton;
3318
3350
  const recordMenu = '<details class="record-overflow"><summary aria-label="More record actions" title="More record actions"><svg viewBox="0 0 18 4" aria-hidden="true"><circle cx="2" cy="2" r="2"></circle><circle cx="9" cy="2" r="2"></circle><circle cx="16" cy="2" r="2"></circle></svg></summary><div class="record-overflow-menu" aria-label="Record actions"><button type="button" data-record-modal="info">Record info</button><button type="button" data-record-modal="connections">Connections</button><button type="button" data-record-modal="history"' + (entry.historyLoaded === false ? ' disabled' : '') + '>File history' + (entry.historyLoaded === false ? ' (loading)' : '') + '</button>' + (mayDelete ? '<div class="record-overflow-separator"></div><button class="danger-text" type="button" data-delete-resource>Delete record</button>' : '') + '</div></details>';
3319
- main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="' + collectionHref + '">' + esc(collectionLabel) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence &amp; Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' : "") + recordMenu + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain + supportPanels + '</div></div>';
3351
+ main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="' + collectionHref + '">' + esc(collectionLabel) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence &amp; Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' : "") + recordMenu + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + (type === "control" ? " detail-control" : "") + '">' + detailMain + supportPanels + '</div></div>';
3320
3352
  main.querySelectorAll("[data-completion-requirements]").forEach((button) => button.addEventListener("click", () => openCompletionRequirementsDialog(button.dataset.completionRequirements)));
3321
3353
  main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
3322
3354
  main.querySelectorAll("[data-record-modal]").forEach((button) => button.addEventListener("click", () => {
@@ -4251,11 +4283,11 @@ function zonedTimestampFromLocal(value, timezone) {
4251
4283
  return value + offset;
4252
4284
  }
4253
4285
 
4254
- function resourceGuide(type) {
4286
+ function resourceGuide(type, pageGuidance = null) {
4255
4287
  const definition = state.model.resources[type];
4256
4288
  const guidance = definition?.guidance;
4257
4289
  if (!definition || !guidance) return "";
4258
- const instructions = RESOURCE_GUIDE_INSTRUCTIONS[type] || definition.description;
4290
+ const instructions = pageGuidance?.instructions || RESOURCE_GUIDE_INSTRUCTIONS[type] || definition.description;
4259
4291
  const sources = (guidance.sourceResourceIds || [])
4260
4292
  .map((id) => state.resources.find(({ record }) => record.id === id))
4261
4293
  .filter(Boolean);
@@ -4266,7 +4298,8 @@ function resourceGuide(type) {
4266
4298
  const reviewGuide = reviewPoints.length
4267
4299
  ? '<div class="guide-review"><span>When reviewing</span><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></div>'
4268
4300
  : "";
4269
- return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use ' + esc(definition.pluralTitle) + '" hidden><div><span>Instructions</span><p>' + esc(instructions) + '</p></div><div><span>Use</span><p>' + esc(definition.description) + '</p></div><div><span>Policy basis</span><p>' + esc(guidance.policyBasis) + '</p>' + sourceLinks + '</div>' + reviewGuide + '</section>';
4301
+ const output = pageGuidance?.output || RESOURCE_GUIDE_OUTPUTS[type] || 'A validated ' + definition.title + ' record.';
4302
+ return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use ' + esc(definition.pluralTitle) + '" hidden><div><span>Do now</span><p>' + esc(instructions) + '</p></div><div><span>Output</span><p>' + esc(output) + '</p></div><div><span>Policy basis</span><p>' + esc(guidance.policyBasis) + '</p>' + sourceLinks + '</div>' + reviewGuide + '</section>';
4270
4303
  }
4271
4304
 
4272
4305
  function setupResourceGuide(main) {
@@ -6486,6 +6519,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
6486
6519
  .brand{flex:0 0 auto;margin-bottom:18px}.sidebar-nav{--nav-control-width:14px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding-right:1px}.nav-home{margin-bottom:10px}.nav-stage>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0}.nav-stage>.nav-heading:hover{background:rgba(255,255,255,.08);color:#fff}.nav-stage-number{display:grid;place-items:center;width:22px;height:22px;border:1px solid rgba(255,255,255,.26);border-radius:50%;font-size:9.6px}.nav-stage-copy,.nav-stage-copy strong,.nav-stage-copy small{display:block;min-width:0}.nav-stage-copy strong{font-size:12px;line-height:1.25}.nav-stage-copy small{margin-top:2px;color:#aeb6d8;font-size:9.6px;line-height:1.25;font-weight:500}.nav-stage>.nav-items{margin:2px 0 8px 17px;padding:1px 0 5px 12px;border-left:1px solid rgba(255,255,255,.14)}.nav-subgroup>.nav-subheading,.nav-subgroup>.nav-items a{width:100%;display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding-right:6px}.nav-subgroup>.nav-subheading{border:0;background:none;padding-top:7px;padding-bottom:4px;padding-left:7px;color:#919bc4;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;cursor:pointer}.nav-control,.nav-control-slot{justify-self:end;width:var(--nav-control-width);text-align:right}.nav-subgroup>.nav-items{padding:1px 0 4px 3px}.nav-subgroup>.nav-items a{padding-top:6px;padding-bottom:6px;padding-left:8px;font-size:13.2px}.nav-group.open>.nav-heading>.chevron:before,.nav-group.open>.nav-subheading>.chevron:before{transform:rotate(45deg)}.nav-stage.open>.nav-items>.nav-subgroup:not(.open)>.nav-items{display:none}.sidebar-footer{flex:0 0 auto;margin:10px -18px -18px;padding:10px 18px 14px;background:#000024;border-top:1px solid rgba(255,255,255,.14)}.organization-nav{display:grid;grid-template-columns:32px minmax(0,1fr) 12px;gap:9px;align-items:center;padding:8px;border-radius:8px;color:#eef1ff;text-decoration:none}.organization-nav:hover,.organization-nav.current{background:rgba(255,255,255,.11)}.organization-mark{display:grid;place-items:center;width:32px;height:32px;border:1px solid rgba(255,255,255,.25);border-radius:50%;background:rgba(255,255,255,.08);font-size:13.2px;font-weight:800}.organization-nav strong,.organization-nav small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organization-nav strong{font-size:12px}.organization-nav small{margin-top:2px;color:#aeb6d8;font-size:9.6px}.organization-arrow{color:#aeb6d8;font-size:19.2px}.organization-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.organization-links{display:grid}.organization-links a{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid var(--line);text-decoration:none}.organization-links a:first-child{padding-top:0;border-top:0}.organization-links strong,.organization-links small{display:block}.organization-links strong{font-size:13.2px}.organization-links small{margin-top:3px;color:var(--muted);font-size:10.8px;line-height:1.4}.organization-links b{color:var(--accent);font-size:13.2px}.organization-links a:hover strong{color:var(--accent)}
6487
6520
  .nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:13.2px}
6488
6521
  .nav-heading-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:2px;align-items:stretch}.nav-heading-row>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr);gap:6px;align-items:center;width:100%;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0;text-decoration:none}.nav-heading-row>.nav-heading:hover,.nav-heading-row>.nav-heading.current{background:rgba(255,255,255,.1);color:#fff}.nav-toggle{display:grid;place-items:center;width:24px;height:auto;min-height:100%;padding:0;border:0;border-radius:6px;background:none;color:#aeb6d8;cursor:pointer}.nav-toggle:hover{background:rgba(255,255,255,.1);color:#fff}.nav-chevron{display:block;width:12px;height:12px;place-self:center;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}.nav-subheading-row{display:grid;grid-template-columns:minmax(0,1fr) 22px;gap:2px;align-items:center;width:100%;padding:0;border:0;border-radius:6px;background:none;color:#919bc4;cursor:pointer}.nav-subheading-row:hover{background:rgba(255,255,255,.1);color:#fff}.nav-subheading-row>.nav-subheading{display:flex;align-items:center;min-width:0;padding:7px;color:inherit;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780}.nav-group.open>.nav-heading-row .nav-chevron,.nav-group.open>.nav-subheading-row .nav-chevron{transform:rotate(90deg)}
6522
+ .stage-instructions{margin-top:14px;padding:0 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.stage-instructions>summary{padding:14px 0;cursor:pointer;font-size:12px;font-weight:700}.stage-instructions-body{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px;padding:2px 0 18px}.stage-instructions h3{margin:0 0 7px;font-size:12px}.stage-instructions ol{margin:0;padding-left:20px}.stage-instructions li{margin:0 0 6px;color:var(--muted);font-size:11px;line-height:1.5}
6489
6523
  .stage-overview-hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;align-items:center;padding:26px 28px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stage-overview-hero h2,.group-overview-head h2{font:500 37.2px Georgia,serif;margin:7px 0}.stage-overview-hero>div>p:not(.kicker),.group-overview-head>div>p:not(.kicker){max-width:760px;color:var(--muted);font-size:14.4px;line-height:1.55;margin:0}.stage-progress-card{padding:15px 17px;background:var(--paper);border:1px solid var(--line);border-radius:9px}.stage-progress-card>div:first-child{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}.stage-progress-card>div:first-child>strong{font:500 33.6px Georgia,serif}.stage-progress-card p{color:var(--muted);font-size:10.8px;line-height:1.45;margin:9px 0 0}.badge.neutral{background:#e5e8f2;color:#555e73}.stage-overview-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:15px;margin-top:15px;align-items:start}.stage-plan ol,.group-plan ol{display:grid;gap:12px;padding-left:20px;margin:0}.stage-plan li,.group-plan li{padding-left:4px;color:var(--ink);font-size:13.2px;line-height:1.5}.stage-groups{min-width:0}.stage-groups>.section-head{margin:4px 0 13px}.stage-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-group-card{position:relative;display:block;min-height:138px;padding:18px 38px 17px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.stage-group-card:hover{border-color:var(--accent-light)}.stage-group-card h3{font-size:15.6px;margin:0 0 7px}.stage-group-card p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.stage-group-card small{display:block;color:var(--accent);font-size:9.6px;font-weight:700;margin-top:12px}.stage-group-arrow{position:absolute;right:16px;top:16px;color:var(--accent);font-size:24px}.group-overview-head{display:flex;justify-content:space-between;align-items:end;gap:25px;margin-bottom:15px}.stage-status-link{display:grid;grid-template-columns:auto auto;align-items:center;gap:4px 12px;min-width:155px;padding:12px 14px;background:var(--panel);border:1px solid var(--line);border-radius:9px;text-decoration:none}.stage-status-link>strong{font:500 28.8px Georgia,serif;text-align:right}.stage-status-link>small{grid-column:1/-1;color:var(--muted);font-size:9.6px;text-align:right}.relationship-note{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;align-items:center;margin-bottom:15px;padding:17px 20px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.relationship-note h3{font-size:15.6px;margin:5px 0 0}.relationship-note>p{color:var(--muted);font-size:12px;line-height:1.55;margin:0}.relationship-note code{font-size:10.8px}.group-plan{margin-bottom:24px}.group-related-links{display:flex;align-items:center;gap:8px;margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.group-related-links>span{color:var(--muted);font-size:10.8px;margin-right:auto}.group-destinations>.section-head{margin-bottom:12px}.group-destination-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.group-destination-card{display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:16px;min-height:150px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.group-destination-card:hover{border-color:var(--accent-light)}.group-destination-card h3{font-size:15.6px;margin:5px 0 7px}.group-destination-card p:not(.kicker){color:var(--muted);font-size:10.8px;line-height:1.5;margin:0}.destination-rollup{align-self:center;text-align:right}.destination-rollup strong,.destination-rollup small{display:block}.destination-rollup strong{font:500 32.4px Georgia,serif}.destination-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:3px}
6490
6524
  .collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.workflow-guidance-more>summary{cursor:pointer;font-weight:750}.workflow-findings-more{margin-top:9px}.workflow-guidance-more>p{margin:8px 0 0}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.policy-lifecycle-note{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px;margin-top:18px}.policy-lifecycle-note h3{margin:5px 0 8px}.policy-lifecycle-note p:not(.kicker),.policy-library-proposals p{color:var(--muted);font-size:11px;line-height:1.55}.policy-library-proposals{display:grid;gap:8px}.policy-library-proposals article{padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.policy-library-proposals p{margin:5px 0 8px}.policy-activation{display:grid;gap:12px;margin-top:22px}.policy-activation-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.policy-activation-card{padding:17px 18px;border:1px solid #d8bd78;border-radius:10px;background:var(--panel)}.policy-activation-card.active-and-operating{border-color:#b9dac6}.policy-activation-card h3{margin:7px 0 0}.policy-activation-gaps{display:grid;gap:9px;margin-top:13px}.policy-activation-gaps>div{padding-top:8px;border-top:1px solid var(--line)}.policy-activation-gaps small{display:block;margin-bottom:6px;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.policy-activation-warning{margin:11px 0 0;padding:9px 10px;border-radius:7px;background:#f7e9cf;color:#6d4917;font-size:10.4px;line-height:1.45}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
6491
6525
  .stage-page-card-actions{display:flex;align-items:center;gap:10px}.stage-page-card-actions .stage-page-open{white-space:nowrap}.stage-page-open::after{content:"";position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-open:focus-visible::after{outline:2px solid var(--focus);outline-offset:2px}.stage-page-open>span{position:relative;z-index:2}
@@ -6669,6 +6703,7 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
6669
6703
  @media(max-width:760px){.record-table td:has(.relation){min-width:0}.record-table td[data-label]>.relation{grid-column:2}}
6670
6704
  @media(max-width:760px){.diagnostics>div{grid-template-columns:max-content minmax(0,1fr)}}
6671
6705
  .workflow-guidance .workflow-status{margin:4px 0 12px;color:var(--muted);font-size:10px}.workflow-completion-requirements{display:inline;appearance:none;-webkit-appearance:none;white-space:nowrap;padding:0;border:0;background:none;color:var(--accent);font:inherit;font-weight:750;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.workflow-completion-requirements:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.completion-requirements-dialog ul{margin:14px 0 0;padding-left:20px;color:var(--muted);font-size:12px;line-height:1.55}
6706
+ .control-next-steps{margin:8px 0 9px;padding-left:18px;font-size:12px;line-height:1.5}.control-next-steps li+li{margin-top:7px}.detail-control aside .detail-workflow-panel{max-height:none;overflow:visible}
6672
6707
  .record-completion-state{background:var(--surface-soft)}.record-completion-state h3{margin:4px 0 5px;font-size:16.8px}.record-completion-state p:last-child{margin:0;color:var(--muted);font-size:11px;line-height:1.45}
6673
6708
  `;
6674
6709
 
package/src/workflow.js CHANGED
@@ -1128,6 +1128,7 @@ function normalizeFinding(code, item, context) {
1128
1128
  severity: findingSeverity(state, context.assessment),
1129
1129
  title: item.title,
1130
1130
  message: item.message,
1131
+ ...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
1131
1132
  ...(item.resourceId ? { resourceId: item.resourceId } : {}),
1132
1133
  ...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
1133
1134
  ...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),