filegrc 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -20,13 +20,13 @@ npx filegrc serve
20
20
  npx filegrc setup --help
21
21
  npx filegrc build
22
22
  npx filegrc guide risk-assessment
23
- npx filegrc program-path --json
23
+ npx filegrc program-path --next --json
24
24
  npx filegrc scaffold risk-assessment --title "2026 Annual Risk Assessment"
25
25
  npx filegrc list risk --json
26
26
  npx filegrc references risk-example --json
27
27
  npx filegrc describe risk
28
28
  npx filegrc search "access review"
29
- npx filegrc evidence-test-drafts
29
+ npx filegrc evidence-test-drafts --preview --json
30
30
  npx filegrc program-readiness --summary --json
31
31
  npx filegrc program-readiness --require-ready
32
32
  npx filegrc audit-readiness audit-id
@@ -36,13 +36,13 @@ npx filegrc evidence-packet --start 2026-01-01 --end 2026-06-30 --audit audit-id
36
36
 
37
37
  `filegrc serve --help` prints bind, port, environment, and safety options without starting the server. The editable server defaults to `127.0.0.1:8787`; set `FILEGRC_HOST`, `FILEGRC_PORT`, or the matching flags when needed.
38
38
 
39
- `filegrc setup` provides the headless equivalent of browser onboarding. Run it without arguments for guided terminal setup, or pass all initial service-boundary fields and a management program goal as flags or a JSON payload. Selecting Type 1 or Type 2 updates the workspace goal and program scope. It does not create an audit record.
39
+ `filegrc setup` provides the headless equivalent of browser onboarding. Run it without arguments for guided terminal setup, or pass all initial service-boundary fields and a management program goal as flags or a JSON payload. Add `--preview` to validate and inspect the planned service and workspace writes without saving. Add `--summary --json` for compact agent output. Selecting Type 1 or Type 2 updates the workspace goal and selected systems. Setup does not select framework records, link controls, create evidence, or create an audit record.
40
40
 
41
- `filegrc program-path` gives agents the renderer’s six-step order, exact page Instructions, Use, Policy Basis, commands, current state, and next actions. `filegrc guide <type>` repeats the matching page guidance and adds fields, relationship candidates, Markdown slots, and timing for that record type.
41
+ `filegrc program-path --next --json` gives an agent the current step and first action without loading the full lifecycle. Use `--summary` for compact status across all six steps, `--current` for the full current-step guide, or no compact flag for every step. `filegrc guide <type>` repeats the matching page guidance and adds fields, relationship candidates, Markdown slots, and timing for that record type.
42
42
 
43
43
  `filegrc program-readiness` reports whether management can start a candidate Type 2 period. Add `--summary --json` for compact stage counts and next actions, or omit `--summary` for every readiness item. Use `--require-ready` in automation. The command does not require an audit ID or CPA firm.
44
44
 
45
- `filegrc evidence-test-drafts` creates missing draft tests for external evidence that does not already have a dedicated Step 5 record. When a Step 5 operating record exists, put the fixed artifact in an External Evidence record and link it from that operating record. Completing browser or CLI onboarding runs the same operation automatically.
45
+ `filegrc evidence-test-drafts --preview --json` reports the missing draft tests for external evidence without writing them. Run `filegrc evidence-test-drafts` after confirming applicable controls and authoritative source Systems. When a Step 5 operating record exists, put the fixed artifact in an External Evidence record and link it from that operating record.
46
46
 
47
47
  `filegrc obligations` shows recurring work and a task-level preview for each Policy Event, including owners, deadlines, and requested proof. `filegrc trigger` adds the event and all of its Action Items to the Work Queue atomically, then prints the created task IDs and deadlines.
48
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -93,7 +93,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
93
93
  "Inspect existing records and relation candidates before writing.",
94
94
  "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
95
95
  "Keep stable metadata in JSON and put the work performed, inputs, results, decisions, exceptions, and follow-up in the recommended Markdown companion.",
96
- "Run filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
96
+ "Run npx filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
97
97
  ],
98
98
  completionChecks: [
99
99
  "The record describes work that actually occurred; planned work is not marked complete.",
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldRes
6
6
  import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
7
7
  import { buildWorkspace } from "./build.js";
8
8
  import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
9
- import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
9
+ import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
10
10
  import {
11
11
  addEvidenceAttachment,
12
12
  createResource,
@@ -28,7 +28,7 @@ import { assessProgramReadiness } from "./program-readiness.js";
28
28
  import { markdownEntries } from "./resource-markdown.js";
29
29
  import { searchResources } from "./search.js";
30
30
  import { serveWorkspace } from "./server.js";
31
- import { setupWorkspace } from "./setup.js";
31
+ import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
32
32
  import { createAppState } from "./state.js";
33
33
  import { currentCalendarDate } from "./time.js";
34
34
  import { validateWorkspace } from "./validate.js";
@@ -37,10 +37,12 @@ import { loadWorkspace } from "./workspace.js";
37
37
  const BOOLEAN_FLAGS = new Set([
38
38
  "check-docs",
39
39
  "complete",
40
+ "current",
40
41
  "draft",
41
42
  "help",
42
43
  "json",
43
44
  "mutation",
45
+ "next",
44
46
  "preview",
45
47
  "require-ready",
46
48
  "summary",
@@ -83,15 +85,25 @@ export async function runCli(argv = process.argv.slice(2)) {
83
85
  ...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
84
86
  ...(flags.draft ? { draft: true } : {})
85
87
  });
86
- const result = await setupWorkspace(root, setupInput);
87
- if (flags.json) console.log(JSON.stringify(result, null, 2));
88
+ const result = flags.preview
89
+ ? await planWorkspaceSetup(root, setupInput)
90
+ : await setupWorkspace(root, setupInput);
91
+ const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
92
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
93
+ else if (flags.preview) {
94
+ console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
95
+ console.log("No controls will be linked and no evidence drafts will be created.");
96
+ }
88
97
  else {
89
98
  console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
90
99
  console.log(`System: ${result.system.id} (${result.system.status})`);
100
+ if (result.draft) {
101
+ console.log("Planned and in scope means selected for scope review, not approved or active.");
102
+ }
91
103
  console.log(`Target: ${result.workspace.assuranceGoal}`);
92
- console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run filegrc program-path for the full path.");
104
+ console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run npx filegrc program-path --next --json.");
93
105
  }
94
- return result;
106
+ return output;
95
107
  }
96
108
  if (command === "build") {
97
109
  const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
@@ -158,9 +170,10 @@ export async function runCli(argv = process.argv.slice(2)) {
158
170
  const auditId = positionals[0] || flags.audit;
159
171
  const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
160
172
  const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
161
- if (flags.json) console.log(JSON.stringify(result, null, 2));
162
- else printProgramPath(result);
163
- return result;
173
+ const output = selectProgramPathOutput(result, flags);
174
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
175
+ else printProgramPathOutput(output, flags);
176
+ return output;
164
177
  }
165
178
  if (command === "scaffold") {
166
179
  const loaded = await loadWorkspace(root);
@@ -253,6 +266,29 @@ export async function runCli(argv = process.argv.slice(2)) {
253
266
  return output;
254
267
  }
255
268
  if (command === "evidence-test-drafts") {
269
+ if (flags.preview) {
270
+ const loaded = await loadWorkspace(root);
271
+ const plan = planEvidenceTestDrafts(loaded);
272
+ const result = {
273
+ schemaVersion: 1,
274
+ preview: true,
275
+ total: plan.length,
276
+ create: plan.filter(({ existing }) => !existing).map((item) => ({
277
+ familyId: item.familyId,
278
+ title: item.title,
279
+ testEvidenceKind: item.testEvidenceKind,
280
+ controlIds: item.controlIds
281
+ })),
282
+ existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
283
+ id: existing.id,
284
+ title: existing.title,
285
+ status: existing.status
286
+ }))
287
+ };
288
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
289
+ else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
290
+ return result;
291
+ }
256
292
  const result = await ensureEvidenceTestDrafts(root);
257
293
  if (flags.json) console.log(JSON.stringify(result, null, 2));
258
294
  else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
@@ -641,20 +677,20 @@ function printHelp() {
641
677
 
642
678
  Usage:
643
679
  filegrc serve [root] [--host 127.0.0.1] [--port 8787]
644
- filegrc setup [setup.json|-] [setup options] [--draft] [--json]
680
+ filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
645
681
  filegrc build [root] [--output .filegrc/site]
646
682
  filegrc validate [root] [--json]
647
683
  filegrc model [--json|--write-docs|--check-docs]
648
684
  filegrc describe <resource-type>
649
685
  filegrc types [--json]
650
686
  filegrc guide [resource-type] [--id resource-id] [--json]
651
- filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--json]
687
+ filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--summary|--next|--current] [--json]
652
688
  filegrc scaffold <resource-type> --title text [--id resource-id]
653
689
  filegrc list [resource-type] [--json]
654
690
  filegrc search <query> [--type resource-type] [--json]
655
691
  filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
656
692
  filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
657
- filegrc evidence-test-drafts [--json]
693
+ filegrc evidence-test-drafts [--preview] [--json]
658
694
  filegrc audit-readiness [audit-id] [--require-ready] [--json]
659
695
  filegrc prepare-audit <audit-id> [--json]
660
696
  filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
@@ -707,6 +743,8 @@ Options:
707
743
  --internet-exposed <bool> true or false
708
744
  --program-goal <goal> none, readiness, type-1, or type-2
709
745
  --draft Save the service boundary as planned
746
+ --preview Validate and report planned writes without saving
747
+ --summary Omit full workspace relationship arrays
710
748
  --json Print the result as JSON
711
749
  --root <path> Workspace path
712
750
  --help Show this help`);
@@ -741,7 +779,10 @@ and current readiness state. Pass an audit ID to include Step 6 status.
741
779
  Options:
742
780
  --audit <id> Audit record to use for Step 6
743
781
  --as-of <date> Evaluate readiness on YYYY-MM-DD
744
- --json Print the full agent-oriented path as JSON
782
+ --summary Print compact status and the first action for all six steps
783
+ --next Print only the current step and its first action
784
+ --current Print the full guide for the current step only
785
+ --json Print the selected path view as JSON
745
786
  --root <path> Workspace path
746
787
  --help Show this help`);
747
788
  return;
@@ -750,10 +791,12 @@ Options:
750
791
  console.log(`Usage:
751
792
  filegrc evidence-test-drafts [options]
752
793
 
753
- Create missing draft External Evidence records for collection that does not
754
- already have a dedicated Step 5 operating record. Existing tests are preserved.
794
+ Preview or create missing draft External Evidence records for collection that
795
+ does not already have a dedicated Step 5 operating record. Existing tests are
796
+ preserved. Run after confirming applicable controls and source systems.
755
797
 
756
798
  Options:
799
+ --preview Report proposed drafts without creating them
757
800
  --json Print created and existing records as JSON
758
801
  --root <path> Workspace path
759
802
  --help Show this help`);
@@ -763,44 +806,48 @@ Options:
763
806
  }
764
807
 
765
808
  function agentOverview(model) {
809
+ const commands = {
810
+ help: "filegrc help",
811
+ version: "filegrc version",
812
+ serve: "filegrc serve [root]",
813
+ setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
814
+ build: "filegrc build [root]",
815
+ validate: "filegrc validate [root] --json",
816
+ model: "filegrc model --json",
817
+ describe: "filegrc describe <resource-type>",
818
+ types: "filegrc types --json",
819
+ guide: "filegrc guide [resource-type] --json",
820
+ programPath: "filegrc program-path [audit-id] --next --json",
821
+ scaffold: "filegrc scaffold <resource-type> --title <name>",
822
+ list: "filegrc list [resource-type] --json",
823
+ search: "filegrc search <query> --json",
824
+ obligations: "filegrc obligations --json",
825
+ programReadiness: "filegrc program-readiness --json",
826
+ evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
827
+ auditReadiness: "filegrc audit-readiness <audit-id> --json",
828
+ prepareAudit: "filegrc prepare-audit <audit-id>",
829
+ trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
830
+ evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
831
+ get: "filegrc get <resource-id> [--mutation]",
832
+ references: "filegrc references <resource-id> --json",
833
+ create: "filegrc create <record-or-mutation.json>",
834
+ complete: "filegrc complete <obligation-id> <completion-mutation.json>",
835
+ completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
836
+ completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
837
+ update: "filegrc update <resource-type> <id> <record-or-mutation.json>",
838
+ content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
839
+ attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
840
+ detach: "filegrc detach <evidence-id> <attachment-name> --yes",
841
+ delete: "filegrc delete <resource-type> <id> --yes",
842
+ commit: "git diff --check && git diff && git add <reviewed-paths> && git commit -m <reason>"
843
+ };
766
844
  return {
767
845
  rule: "Treat data/ as the source of truth. Run guide before creating an unfamiliar type, validate after every write, review the Git diff, then commit a focused change.",
768
846
  programPath: buildAgentProgramPath(model),
769
- actions: {
770
- help: "filegrc help",
771
- version: "filegrc version",
772
- serve: "filegrc serve [root]",
773
- setup: "filegrc setup [setup.json|-] [--draft] [--json]",
774
- build: "filegrc build [root]",
775
- validate: "filegrc validate [root] --json",
776
- model: "filegrc model --json",
777
- describe: "filegrc describe <resource-type>",
778
- types: "filegrc types --json",
779
- guide: "filegrc guide [resource-type] --json",
780
- programPath: "filegrc program-path [audit-id] --json",
781
- scaffold: "filegrc scaffold <resource-type> --title <name>",
782
- list: "filegrc list [resource-type] --json",
783
- search: "filegrc search <query> --json",
784
- obligations: "filegrc obligations --json",
785
- programReadiness: "filegrc program-readiness --json",
786
- evidenceTestDrafts: "filegrc evidence-test-drafts --json",
787
- auditReadiness: "filegrc audit-readiness <audit-id> --json",
788
- prepareAudit: "filegrc prepare-audit <audit-id>",
789
- trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
790
- evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
791
- get: "filegrc get <resource-id> [--mutation]",
792
- references: "filegrc references <resource-id> --json",
793
- create: "filegrc create <record-or-mutation.json>",
794
- complete: "filegrc complete <obligation-id> <completion-mutation.json>",
795
- completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
796
- completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
797
- update: "filegrc update <resource-type> <id> <record-or-mutation.json>",
798
- content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
799
- attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
800
- detach: "filegrc detach <evidence-id> <attachment-name> --yes",
801
- delete: "filegrc delete <resource-type> <id> --yes",
802
- commit: "git diff --check && git diff && git add <reviewed-paths> && git commit -m <reason>"
803
- },
847
+ actions: Object.fromEntries(Object.entries(commands).map(([name, command]) => [
848
+ name,
849
+ command.startsWith("filegrc ") ? `npx ${command}` : command
850
+ ])),
804
851
  resourceTypes: listResourceTypes(model).map(({ type, title, group }) => ({ type, title, group }))
805
852
  };
806
853
  }
@@ -851,12 +898,12 @@ function printAgentGuide(result) {
851
898
  for (const field of relationshipFields) {
852
899
  const hasCandidates = field.relation.candidates.length > 0;
853
900
  const suffix = field.relation.truncated && hasCandidates
854
- ? `, … (${field.relation.candidateCount} total; use filegrc list)`
901
+ ? `, … (${field.relation.candidateCount} total; use npx filegrc list)`
855
902
  : "";
856
903
  const candidates = hasCandidates
857
904
  ? field.relation.candidates.join(", ") + suffix
858
905
  : field.relation.candidateCount
859
- ? `use filegrc list (${field.relation.candidateCount} possible)`
906
+ ? `use npx filegrc list (${field.relation.candidateCount} possible)`
860
907
  : "none";
861
908
  console.log(`${field.name}\t${field.relation.types.join("|")}\t${candidates}`);
862
909
  }
@@ -923,15 +970,130 @@ function printProgramPath(result) {
923
970
  }
924
971
  }
925
972
 
973
+ function selectProgramPathOutput(result, flags) {
974
+ const modes = ["summary", "next", "current"].filter((name) => flags[name]);
975
+ if (modes.length > 1) throw new Error("Use only one of --summary, --next, or --current.");
976
+ if (flags.summary) return summarizeProgramPath(result);
977
+ if (flags.next) return nextProgramPath(result);
978
+ if (flags.current) {
979
+ const stage = result.stages.find(({ id }) => id === result.currentStep.id);
980
+ return { ...result, stages: stage ? [stage] : [] };
981
+ }
982
+ return result;
983
+ }
984
+
985
+ function summarizeProgramPath(result) {
986
+ return {
987
+ schemaVersion: result.schemaVersion,
988
+ asOf: result.asOf,
989
+ currentStep: result.currentStep,
990
+ evidenceReady: result.evidenceReady,
991
+ operating: result.operating,
992
+ stages: result.stages.map((stage) => ({
993
+ id: stage.id,
994
+ number: stage.number,
995
+ title: stage.title,
996
+ status: stage.status,
997
+ counts: stage.counts,
998
+ nextAction: summarizePathAction(stage.nextActions[0])
999
+ }))
1000
+ };
1001
+ }
1002
+
1003
+ function nextProgramPath(result) {
1004
+ const stage = result.stages.find(({ id }) => id === result.currentStep.id);
1005
+ const nextAction = stage?.nextActions[0];
1006
+ return {
1007
+ schemaVersion: result.schemaVersion,
1008
+ asOf: result.asOf,
1009
+ currentStep: result.currentStep,
1010
+ evidenceReady: result.evidenceReady,
1011
+ operating: result.operating,
1012
+ step: stage ? {
1013
+ id: stage.id,
1014
+ number: stage.number,
1015
+ title: stage.title,
1016
+ status: stage.status,
1017
+ summary: stage.summary,
1018
+ nextAction: summarizePathAction(nextAction),
1019
+ commands: nextActionCommands(stage, nextAction)
1020
+ } : null
1021
+ };
1022
+ }
1023
+
1024
+ function summarizePathAction(action) {
1025
+ if (!action) return null;
1026
+ return {
1027
+ id: action.id,
1028
+ status: action.status,
1029
+ title: action.title,
1030
+ message: action.message,
1031
+ ...(action.resourceType ? { resourceType: action.resourceType } : {}),
1032
+ ...(action.resourceId ? { resourceId: action.resourceId } : {})
1033
+ };
1034
+ }
1035
+
1036
+ function nextActionCommands(stage, action) {
1037
+ if (action?.commands?.length) return action.commands;
1038
+ if (!action?.resourceType) return stage.commands;
1039
+ const resourceType = shellArgument(action.resourceType);
1040
+ const commands = [`npx filegrc guide ${resourceType} --json`];
1041
+ if (action.resourceId) {
1042
+ const resourceId = shellArgument(action.resourceId);
1043
+ commands.push(`npx filegrc get ${resourceId} --mutation`);
1044
+ commands.push(`npx filegrc update ${resourceType} ${resourceId} MUTATION.json --json`);
1045
+ } else {
1046
+ commands.push(`npx filegrc list ${resourceType} --json`);
1047
+ commands.push(`npx filegrc scaffold ${resourceType} --title "NAME"`);
1048
+ commands.push("npx filegrc create MUTATION.json --json");
1049
+ }
1050
+ return commands;
1051
+ }
1052
+
1053
+ function shellArgument(value) {
1054
+ const text = String(value);
1055
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(text)
1056
+ ? text
1057
+ : `'${text.replaceAll("'", "'\\''")}'`;
1058
+ }
1059
+
1060
+ function printProgramPathOutput(result, flags) {
1061
+ if (flags.summary) {
1062
+ console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
1063
+ for (const stage of result.stages) {
1064
+ console.log(`${String(stage.status).toUpperCase()}\tStep ${stage.number}\t${stage.title}`);
1065
+ }
1066
+ const current = result.stages.find(({ id }) => id === result.currentStep.id);
1067
+ if (current?.nextAction) console.log(`Next: ${current.nextAction.title} · ${current.nextAction.message}`);
1068
+ return;
1069
+ }
1070
+ if (flags.next) {
1071
+ console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
1072
+ if (result.step?.nextAction) {
1073
+ console.log(`Next: ${result.step.nextAction.title} · ${result.step.nextAction.message}`);
1074
+ }
1075
+ for (const command of result.step?.commands || []) console.log(` ${command}`);
1076
+ return;
1077
+ }
1078
+ printProgramPath(result);
1079
+ }
1080
+
926
1081
  function summarizeProgramReadiness(result) {
927
- const summarizeItem = (item) => item ? {
1082
+ const ownership = result.stages
1083
+ .flatMap((stage) => stage.items)
1084
+ .find((item) => item.id === "program-ownership");
1085
+ const summarizeItem = (item, options = {}) => item ? {
928
1086
  id: item.id,
929
1087
  status: item.status,
930
1088
  title: item.title,
931
- message: item.message,
1089
+ ...(options.message === false ? {} : { message: item.message }),
932
1090
  ...(item.resourceType ? { resourceType: item.resourceType } : {}),
933
1091
  ...(item.resourceId ? { resourceId: item.resourceId } : {})
934
1092
  } : null;
1093
+ const unresolvedOwnership = ownership?.unresolvedAssignments || [];
1094
+ const ownershipReasons = unresolvedOwnership
1095
+ .flatMap((assignment) => assignment.reasons || [])
1096
+ .reduce((counts, { reason }) => ({ ...counts, [reason]: (counts[reason] || 0) + 1 }), {});
935
1097
  return {
936
1098
  schemaVersion: result.schemaVersion,
937
1099
  generatedAt: result.generatedAt,
@@ -947,13 +1109,18 @@ function summarizeProgramReadiness(result) {
947
1109
  scopeCounts: Object.fromEntries(
948
1110
  Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
949
1111
  ),
1112
+ unresolvedOwnership: {
1113
+ count: unresolvedOwnership.length,
1114
+ byReason: ownershipReasons,
1115
+ resourceIds: unresolvedOwnership.map(({ resourceId }) => resourceId)
1116
+ },
950
1117
  firstAction: summarizeItem(result.firstAction),
951
1118
  stages: result.stages.map((stage) => ({
952
1119
  id: stage.id,
953
1120
  title: stage.title,
954
1121
  status: stage.status,
955
1122
  counts: stage.counts,
956
- firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"))
1123
+ firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"), { message: false })
957
1124
  }))
958
1125
  };
959
1126
  }
package/src/index.js CHANGED
@@ -51,7 +51,7 @@ export {
51
51
  } from "./recurrence.js";
52
52
  export { searchResources, searchableValues } from "./search.js";
53
53
  export { createFilegrcServer, serveWorkspace } from "./server.js";
54
- export { normalizeSetupPayload, setupWorkspace } from "./setup.js";
54
+ export { normalizeSetupPayload, planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
55
55
  export { createAppState } from "./state.js";
56
56
  export { currentCalendarDate, formatCalendarDate, formatLocalDateTime } from "./time.js";
57
57
  export { validateWorkspace } from "./validate.js";
@@ -119,7 +119,7 @@ export const PROGRAM_PATH = [
119
119
  ],
120
120
  resourceTypes: ["evidence"],
121
121
  commands: [
122
- "filegrc evidence-test-drafts --json",
122
+ "filegrc evidence-test-drafts --preview --json",
123
123
  "filegrc guide evidence --json",
124
124
  "filegrc list evidence --json",
125
125
  "filegrc program-readiness --json"
@@ -235,8 +235,8 @@ export function buildAgentProgramPath(model) {
235
235
  instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
236
236
  use: definition.description,
237
237
  policyBasis: definition.guidance.policyBasis,
238
- guide: `filegrc guide ${type} --json`,
239
- list: `filegrc list ${type} --json`
238
+ guide: `npx filegrc guide ${type} --json`,
239
+ list: `npx filegrc list ${type} --json`
240
240
  };
241
241
  });
242
242
  const utilityPages = (stage.utilities || []).map((utility, index) => ({
@@ -246,16 +246,21 @@ export function buildAgentProgramPath(model) {
246
246
  instructions: utility.instructions,
247
247
  use: utility.use,
248
248
  policyBasis: utility.policyBasis,
249
- commands: utility.commands
249
+ commands: utility.commands.map(agentCommand)
250
250
  }));
251
251
  return {
252
252
  ...stage,
253
+ commands: stage.commands.map(agentCommand),
253
254
  pages: stage.id === "run" ? utilityPages : [...resourcePages, ...utilityPages],
254
255
  ...(stage.id === "run" ? { operatingRecords: resourcePages } : {})
255
256
  };
256
257
  });
257
258
  }
258
259
 
260
+ function agentCommand(command) {
261
+ return command.startsWith("filegrc ") ? `npx ${command}` : command;
262
+ }
263
+
259
264
  export function resourceProgramContext(type) {
260
265
  const stage = PROGRAM_PATH.find((candidate) => (
261
266
  candidate.resourceTypes.includes(type) || (candidate.supportingResourceTypes || []).includes(type)
@@ -203,10 +203,20 @@ function programOwnershipItem(records, byId) {
203
203
  && ![...oversightChairs].some((id) => policyOwnerIds.has(id))
204
204
  );
205
205
  const complete = currentOwners.size > 0 && unresolved.length === 0 && oversightComplete;
206
+ const unresolvedAssignments = unresolved.map((record) => ({
207
+ resourceType: record.type,
208
+ resourceId: record.id,
209
+ title: record.title,
210
+ ownerIds: record.ownerIds || [],
211
+ reasons: ownershipResolutionReasons(record.ownerIds || [], byId)
212
+ }));
206
213
  const detail = [];
207
214
  if (!currentOwners.size) detail.push("No current person owns the program records.");
208
- if (unresolved.length) detail.push(`${unresolved.length} ownership ${unresolved.length === 1 ? "assignment does" : "assignments do"} not resolve to a current person.`);
215
+ if (unresolved.length) {
216
+ detail.push(`${unresolved.length} ${unresolved.length === 1 ? "record has" : "records have"} no current person owner.`);
217
+ }
209
218
  if (!oversightComplete) detail.push("Finish and activate Security and Risk Oversight with a current chair who is separate from policy ownership.");
219
+ const oversightId = oversight?.id ? shellArgument(oversight.id) : null;
210
220
  return item(
211
221
  "program-ownership",
212
222
  complete ? "complete" : "action",
@@ -214,10 +224,40 @@ function programOwnershipItem(records, byId) {
214
224
  complete
215
225
  ? `${currentOwners.size} current ${currentOwners.size === 1 ? "person owns" : "people own"} the program records.${oversight ? " Security and Risk Oversight has a separate current chair." : ""}`
216
226
  : detail.join(" "),
217
- !oversightComplete ? oversight : unresolved[0] || { type: "person" }
227
+ !oversightComplete ? oversight : unresolved[0] || { type: "person" },
228
+ {
229
+ unresolvedAssignments,
230
+ ...(!oversightComplete && oversight ? {
231
+ commands: [
232
+ "npx filegrc guide person --json",
233
+ "npx filegrc list person --json",
234
+ 'npx filegrc scaffold person --title "REVIEWER NAME" | npx filegrc create - --json',
235
+ `npx filegrc get ${oversightId} --mutation`,
236
+ `npx filegrc update team ${oversightId} MUTATION.json --json`
237
+ ]
238
+ } : {})
239
+ }
218
240
  );
219
241
  }
220
242
 
243
+ function ownershipResolutionReasons(ownerIds, byId) {
244
+ if (!ownerIds.length) return [{ ownerId: null, reason: "missing-owner" }];
245
+ return ownerIds.flatMap((ownerId) => {
246
+ const owner = byId.get(ownerId);
247
+ if (!owner) return [{ ownerId, reason: "missing-record" }];
248
+ if (owner.type === "person" && owner.status !== "active") {
249
+ return [{ ownerId, reason: "inactive-person" }];
250
+ }
251
+ if (owner.type === "team" && owner.status !== "active") {
252
+ return [{ ownerId, reason: "inactive-team" }];
253
+ }
254
+ if (currentPartyPeople([ownerId], byId).size === 0) {
255
+ return [{ ownerId, reason: owner.type === "team" ? "team-has-no-current-members" : "no-current-person" }];
256
+ }
257
+ return [];
258
+ });
259
+ }
260
+
221
261
  async function policiesStage(scope, byId, readMarkdown, asOf) {
222
262
  const linkedPolicyIds = new Set(scope.controls.flatMap((control) => control.policyIds || []));
223
263
  const policies = [...linkedPolicyIds].map((id) => byId.get(id)).filter((record) => (
@@ -228,15 +268,30 @@ async function policiesStage(scope, byId, readMarkdown, asOf) {
228
268
  .flatMap((policy) => [...currentPartyPeople(policy.approverIds || [], byId)])
229
269
  .map((id) => byId.get(id))
230
270
  .find(Boolean);
271
+ const policyOwnerIds = new Set(policies.flatMap((policy) => (
272
+ [...partyPeople(policy.ownerIds || [], byId)]
273
+ )));
274
+ const oversight = byId.get("team-security-risk-oversight");
275
+ const availableReviewer = oversight?.type === "team" && oversight.status === "active"
276
+ ? [...currentPartyPeople(oversight.chairIds || [], byId)]
277
+ .filter((id) => !policyOwnerIds.has(id))
278
+ .map((id) => byId.get(id))
279
+ .find(Boolean)
280
+ : null;
281
+ const reviewerNeedsAssignment = !appointedReviewer && availableReviewer && policies.length;
231
282
  const items = [
232
283
  item(
233
284
  "independent-reviewer",
234
285
  appointedReviewer ? "complete" : "action",
235
- "Appoint the independent policy reviewer",
286
+ reviewerNeedsAssignment
287
+ ? "Assign the independent policy reviewer"
288
+ : "Appoint the independent policy reviewer",
236
289
  appointedReviewer
237
290
  ? `${appointedReviewer.title} is recorded as a reviewer separate from policy ownership.`
238
- : "Appoint a reviewer who is separate from the policy owner. The reviewer may be another person in the organization or an external person, and is separate from the CPA firm that may later perform the audit.",
239
- appointedReviewer || { type: "person" }
291
+ : reviewerNeedsAssignment
292
+ ? `${availableReviewer.title} chairs Security and Risk Oversight. Assign this person as approver on each policy after review.`
293
+ : "Appoint a reviewer who is separate from the policy owner. The reviewer may be another person in the organization or an external person, and is separate from the CPA firm that may later perform the audit.",
294
+ appointedReviewer || (reviewerNeedsAssignment ? policies[0] : { type: "person" })
240
295
  )
241
296
  ];
242
297
  if (!policies.length) {
@@ -639,6 +694,13 @@ function item(id, status, title, message, resource = {}, details = {}) {
639
694
  };
640
695
  }
641
696
 
697
+ function shellArgument(value) {
698
+ const text = String(value);
699
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(text)
700
+ ? text
701
+ : `'${text.replaceAll("'", "'\\''")}'`;
702
+ }
703
+
642
704
  function countStatuses(items) {
643
705
  const counts = { complete: 0, action: 0, later: 0, info: 0 };
644
706
  for (const current of items) counts[current.status] = (counts[current.status] || 0) + 1;
package/src/setup.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createResource, updateResource } from "./files.js";
2
- import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
3
2
  import { createResourceId } from "./id.js";
4
3
  import { loadWorkspace } from "./workspace.js";
5
4
 
@@ -10,81 +9,64 @@ export async function setupWorkspace(input = process.cwd(), payload = {}) {
10
9
  const loaded = await loadWorkspace(input);
11
10
  const setup = normalizeSetupPayload(payload);
12
11
  validateSetup(loaded, setup);
12
+ const plan = buildSetupRecords(loaded, setup);
13
13
 
14
- let current = loaded;
15
- let system = findSetupSystem(current.resources, setup);
16
- const systemId = system?.id || createResourceId("system", setup.serviceName, current.resources.map(({ id }) => id));
17
- system = {
18
- ...(system || {}),
19
- schemaVersion: 1,
20
- id: systemId,
21
- type: "system",
22
- title: setup.serviceName,
23
- status: setup.draft ? system?.status || "planned" : (system?.status === "planned" ? "active" : system?.status || "active"),
24
- criticality: setup.criticality,
25
- ownerIds: [setup.ownerId],
26
- description: setup.boundary,
27
- systemKind: system?.systemKind || "service",
28
- dataClassification: setup.dataClassification,
29
- internetExposed: setup.internetExposed,
30
- inScope: true
31
- };
32
- await upsertResource(current.root, current.resources.find(({ id }) => id === systemId), system);
33
-
34
- current = await loadWorkspace(current.root);
35
- const linkedControlIds = [];
36
- for (const control of current.resources.filter(({ type, status }) => (
37
- type === "control" && !["not-applicable", "retired"].includes(status)
38
- ))) {
39
- if ((control.systemIds || []).includes(systemId)) continue;
40
- await updateResource(current.root, "control", control.id, {
41
- ...control,
42
- systemIds: [...new Set([...(control.systemIds || []), systemId])]
43
- });
44
- linkedControlIds.push(control.id);
45
- }
14
+ await upsertResource(loaded.root, plan.existingSystem, plan.system);
15
+ await updateResource(loaded.root, "workspace", plan.workspace.id, plan.workspace);
16
+ if (plan.renderer) await updateResource(loaded.root, plan.renderer.type, plan.renderer.id, plan.renderer);
46
17
 
47
- current = await loadWorkspace(current.root);
48
- const existingWorkspace = current.resources.find(({ type }) => type === "workspace");
49
- if (!existingWorkspace) throw new Error("The workspace settings record was not found.");
50
- const workspace = {
51
- ...existingWorkspace,
52
- assuranceGoal: assuranceGoalFromSetup(setup.programGoal),
53
- frameworkIds: current.resources
54
- .filter(({ type, status }) => type === "framework" && status === "active")
55
- .map(({ id }) => id),
56
- requirementIds: current.resources
57
- .filter(({ type, applicability }) => type === "requirement" && applicability === "applicable")
58
- .map(({ id }) => id),
59
- controlIds: current.resources
60
- .filter(({ type, status }) => type === "control" && !["not-applicable", "retired"].includes(status))
61
- .map(({ id }) => id),
62
- systemIds: [...new Set([...(existingWorkspace.systemIds || []), systemId])]
18
+ return {
19
+ draft: setup.draft,
20
+ system: plan.system,
21
+ workspace: plan.workspace,
22
+ renderer: plan.renderer,
23
+ linkedControlIds: [],
24
+ evidenceTestDraftIds: [],
25
+ onboardingComplete: !setup.draft
63
26
  };
64
- await updateResource(current.root, "workspace", workspace.id, workspace);
65
-
66
- current = await loadWorkspace(current.root);
67
- const renderer = current.resources.find(({ type }) => type === "renderer-settings");
68
- if (renderer) {
69
- await updateResource(current.root, renderer.type, renderer.id, {
70
- ...renderer,
71
- showOnboarding: setup.draft
72
- });
73
- }
74
- const evidenceTestDrafts = setup.draft
75
- ? { created: [], existing: [], total: 0 }
76
- : await ensureEvidenceTestDrafts(current.root);
27
+ }
77
28
 
29
+ export async function planWorkspaceSetup(input = process.cwd(), payload = {}) {
30
+ const loaded = await loadWorkspace(input);
31
+ const setup = normalizeSetupPayload(payload);
32
+ validateSetup(loaded, setup);
33
+ const plan = buildSetupRecords(loaded, setup);
78
34
  return {
35
+ schemaVersion: 1,
36
+ preview: true,
79
37
  draft: setup.draft,
80
- system,
81
- workspace,
82
- linkedControlIds,
83
- evidenceTestDraftIds: evidenceTestDrafts.created.map(({ id }) => id),
38
+ changes: {
39
+ system: plan.existingSystem ? "update" : "create",
40
+ workspace: "update",
41
+ renderer: plan.renderer ? "update" : "unchanged",
42
+ controls: 0,
43
+ evidenceDrafts: 0
44
+ },
45
+ system: setupSystemSummary(plan.system),
46
+ target: setupTargetSummary(plan.workspace),
47
+ renderer: plan.renderer ? setupRendererSummary(plan.renderer) : null,
84
48
  onboardingComplete: !setup.draft
85
49
  };
86
50
  }
87
51
 
52
+ export function summarizeSetupResult(result) {
53
+ return {
54
+ schemaVersion: 1,
55
+ preview: false,
56
+ draft: result.draft,
57
+ changes: {
58
+ system: "saved",
59
+ workspace: "updated",
60
+ controls: result.linkedControlIds?.length || 0,
61
+ evidenceDrafts: result.evidenceTestDraftIds?.length || 0
62
+ },
63
+ system: setupSystemSummary(result.system),
64
+ target: setupTargetSummary(result.workspace),
65
+ renderer: result.renderer ? setupRendererSummary(result.renderer) : null,
66
+ onboardingComplete: result.onboardingComplete
67
+ };
68
+ }
69
+
88
70
  export function normalizeSetupPayload(payload = {}) {
89
71
  if (!payload || Array.isArray(payload) || typeof payload !== "object") {
90
72
  throw new Error("Setup input must be a JSON object.");
@@ -146,6 +128,44 @@ function findSetupSystem(resources, setup) {
146
128
  ));
147
129
  }
148
130
 
131
+ function buildSetupRecords(loaded, setup) {
132
+ const existingSystem = findSetupSystem(loaded.resources, setup);
133
+ const systemId = existingSystem?.id || createResourceId(
134
+ "system",
135
+ setup.serviceName,
136
+ loaded.resources.map(({ id }) => id)
137
+ );
138
+ const system = {
139
+ ...(existingSystem || {}),
140
+ schemaVersion: 1,
141
+ id: systemId,
142
+ type: "system",
143
+ title: setup.serviceName,
144
+ status: setup.draft
145
+ ? existingSystem?.status || "planned"
146
+ : existingSystem?.status === "planned"
147
+ ? "active"
148
+ : existingSystem?.status || "active",
149
+ criticality: setup.criticality,
150
+ ownerIds: [setup.ownerId],
151
+ description: setup.boundary,
152
+ systemKind: existingSystem?.systemKind || "service",
153
+ dataClassification: setup.dataClassification,
154
+ internetExposed: setup.internetExposed,
155
+ inScope: true
156
+ };
157
+ const existingWorkspace = loaded.resources.find(({ type }) => type === "workspace");
158
+ if (!existingWorkspace) throw new Error("The workspace settings record was not found.");
159
+ const workspace = {
160
+ ...existingWorkspace,
161
+ assuranceGoal: assuranceGoalFromSetup(setup.programGoal),
162
+ systemIds: [...new Set([...(existingWorkspace.systemIds || []), systemId])]
163
+ };
164
+ const existingRenderer = loaded.resources.find(({ type }) => type === "renderer-settings");
165
+ const renderer = existingRenderer ? { ...existingRenderer, showOnboarding: setup.draft } : null;
166
+ return { existingSystem, system, workspace, renderer };
167
+ }
168
+
149
169
  async function upsertResource(root, existing, record) {
150
170
  return existing
151
171
  ? updateResource(root, record.type, record.id, record)
@@ -159,6 +179,31 @@ function assuranceGoalFromSetup(goal) {
159
179
  return "none";
160
180
  }
161
181
 
182
+ function setupSystemSummary(system) {
183
+ return { ...system };
184
+ }
185
+
186
+ function setupTargetSummary(workspace) {
187
+ return {
188
+ assuranceGoal: workspace.assuranceGoal,
189
+ systemIds: [...(workspace.systemIds || [])],
190
+ scopeCounts: {
191
+ systems: workspace.systemIds?.length || 0,
192
+ frameworks: workspace.frameworkIds?.length || 0,
193
+ requirements: workspace.requirementIds?.length || 0,
194
+ controls: workspace.controlIds?.length || 0
195
+ }
196
+ };
197
+ }
198
+
199
+ function setupRendererSummary(renderer) {
200
+ return {
201
+ id: renderer.id,
202
+ type: renderer.type,
203
+ showOnboarding: renderer.showOnboarding
204
+ };
205
+ }
206
+
162
207
  function booleanValue(value, name) {
163
208
  if (value === true || value === false) return value;
164
209
  if (value === "true") return true;
package/src/web.js CHANGED
@@ -1519,7 +1519,7 @@ function renderOnboardingStep() {
1519
1519
  const finalActions = onboardingStep === steps.length - 1
1520
1520
  ? '<button class="button" type="button" data-onboarding="draft">Save draft</button><button class="button primary" type="button" data-onboarding="next">Complete setup</button>'
1521
1521
  : '<button class="button primary" type="button" data-onboarding="next">Next</button>';
1522
- onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
1522
+ onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-scroll"><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
1523
1523
  onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", cancelOnboarding);
1524
1524
  onboardingDialog.querySelector('[data-onboarding="back"]')?.addEventListener("click", () => {
1525
1525
  captureOnboardingForm();
@@ -1569,7 +1569,7 @@ function onboardingSetupForm() {
1569
1569
  : state.git.available
1570
1570
  ? '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git remote needed</strong><small>Saving and local commits still work. Add a remote before the browser can push.</small></span></div>'
1571
1571
  : '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Saving still works. Run <code>git init</code> at the workspace root before your first compliance commit.</small></span></div>';
1572
- return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft keeps the system planned. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
1572
+ return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft marks the service Planned and In scope. It is selected for scope review, but it is not approved or active. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
1573
1573
  }
1574
1574
 
1575
1575
  function captureOnboardingForm() {
@@ -2598,12 +2598,12 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
2598
2598
  .readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
2599
2599
  .page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:9.6px;font-weight:700}
2600
2600
  .operation-tracking{display:grid;gap:2px;min-width:0;text-decoration:none}.operation-tracking strong,.operation-tracking small{display:block;overflow-wrap:anywhere}.operation-tracking small{color:var(--muted);line-height:1.35}.operation-tracking.running strong{color:#176143}.operation-tracking.waiting strong,.operation-tracking.mixed strong{color:var(--amber)}.operation-tracking.paused strong{color:var(--red)}a.operation-tracking:hover strong{text-decoration:underline}
2601
- .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:auto}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{padding:12px 25px 23px}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 0}.onboarding-dialog>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2601
+ .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;flex:0 0 auto;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-scroll{min-height:0;overflow-y:auto}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{flex:0 0 auto;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2602
2602
  .page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:10.8px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
2603
2603
  @media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
2604
2604
  @media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
2605
2605
  @media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{min-width:0}.topbar-status{display:none}.search{display:flex;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
2606
- @media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid{grid-template-columns:1fr}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}.onboarding-actions{position:sticky;bottom:0;background:var(--panel);border-top:1px solid var(--line)}}
2606
+ @media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid{grid-template-columns:1fr}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}}
2607
2607
  @media(max-width:520px){.onboarding-form,.onboarding-sections,.setup-steps{grid-template-columns:1fr}.onboarding-form label.wide{grid-column:auto}.onboarding-actions{flex-wrap:wrap}.onboarding-skip{width:100%;order:3;margin:3px 0 0}.readiness-flow{grid-template-columns:1fr}.obligation-card-foot{align-items:flex-start;flex-direction:column}.obligation-action{align-self:flex-start}}
2608
2608
  @media(min-width:761px){.detail-grid{grid-template-columns:minmax(270px,1fr) minmax(0,2fr)}.detail-grid aside{grid-column:1;grid-row:1}.detail-main{grid-column:2;grid-row:1}}
2609
2609
  @media(min-width:761px){.detail-grid.detail-grid-structured{grid-template-columns:1fr}.detail-grid-structured aside{grid-column:1;grid-row:1;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.detail-grid-structured aside>.panel{align-self:start}}