filegrc 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ 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
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.
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.1",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
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";
@@ -83,15 +83,22 @@ export async function runCli(argv = process.argv.slice(2)) {
83
83
  ...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
84
84
  ...(flags.draft ? { draft: true } : {})
85
85
  });
86
- const result = await setupWorkspace(root, setupInput);
87
- if (flags.json) console.log(JSON.stringify(result, null, 2));
86
+ const result = flags.preview
87
+ ? await planWorkspaceSetup(root, setupInput)
88
+ : await setupWorkspace(root, setupInput);
89
+ const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
90
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
91
+ else if (flags.preview) {
92
+ console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
93
+ console.log("No controls will be linked and no evidence drafts will be created.");
94
+ }
88
95
  else {
89
96
  console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
90
97
  console.log(`System: ${result.system.id} (${result.system.status})`);
91
98
  console.log(`Target: ${result.workspace.assuranceGoal}`);
92
99
  console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run filegrc program-path for the full path.");
93
100
  }
94
- return result;
101
+ return output;
95
102
  }
96
103
  if (command === "build") {
97
104
  const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
@@ -253,6 +260,29 @@ export async function runCli(argv = process.argv.slice(2)) {
253
260
  return output;
254
261
  }
255
262
  if (command === "evidence-test-drafts") {
263
+ if (flags.preview) {
264
+ const loaded = await loadWorkspace(root);
265
+ const plan = planEvidenceTestDrafts(loaded);
266
+ const result = {
267
+ schemaVersion: 1,
268
+ preview: true,
269
+ total: plan.length,
270
+ create: plan.filter(({ existing }) => !existing).map((item) => ({
271
+ familyId: item.familyId,
272
+ title: item.title,
273
+ testEvidenceKind: item.testEvidenceKind,
274
+ controlIds: item.controlIds
275
+ })),
276
+ existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
277
+ id: existing.id,
278
+ title: existing.title,
279
+ status: existing.status
280
+ }))
281
+ };
282
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
283
+ else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
284
+ return result;
285
+ }
256
286
  const result = await ensureEvidenceTestDrafts(root);
257
287
  if (flags.json) console.log(JSON.stringify(result, null, 2));
258
288
  else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
@@ -641,7 +671,7 @@ function printHelp() {
641
671
 
642
672
  Usage:
643
673
  filegrc serve [root] [--host 127.0.0.1] [--port 8787]
644
- filegrc setup [setup.json|-] [setup options] [--draft] [--json]
674
+ filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
645
675
  filegrc build [root] [--output .filegrc/site]
646
676
  filegrc validate [root] [--json]
647
677
  filegrc model [--json|--write-docs|--check-docs]
@@ -654,7 +684,7 @@ Usage:
654
684
  filegrc search <query> [--type resource-type] [--json]
655
685
  filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
656
686
  filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
657
- filegrc evidence-test-drafts [--json]
687
+ filegrc evidence-test-drafts [--preview] [--json]
658
688
  filegrc audit-readiness [audit-id] [--require-ready] [--json]
659
689
  filegrc prepare-audit <audit-id> [--json]
660
690
  filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
@@ -707,6 +737,8 @@ Options:
707
737
  --internet-exposed <bool> true or false
708
738
  --program-goal <goal> none, readiness, type-1, or type-2
709
739
  --draft Save the service boundary as planned
740
+ --preview Validate and report planned writes without saving
741
+ --summary Omit full workspace relationship arrays
710
742
  --json Print the result as JSON
711
743
  --root <path> Workspace path
712
744
  --help Show this help`);
@@ -750,10 +782,12 @@ Options:
750
782
  console.log(`Usage:
751
783
  filegrc evidence-test-drafts [options]
752
784
 
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.
785
+ Preview or create missing draft External Evidence records for collection that
786
+ does not already have a dedicated Step 5 operating record. Existing tests are
787
+ preserved. Run after confirming applicable controls and source systems.
755
788
 
756
789
  Options:
790
+ --preview Report proposed drafts without creating them
757
791
  --json Print created and existing records as JSON
758
792
  --root <path> Workspace path
759
793
  --help Show this help`);
@@ -770,7 +804,7 @@ function agentOverview(model) {
770
804
  help: "filegrc help",
771
805
  version: "filegrc version",
772
806
  serve: "filegrc serve [root]",
773
- setup: "filegrc setup [setup.json|-] [--draft] [--json]",
807
+ setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
774
808
  build: "filegrc build [root]",
775
809
  validate: "filegrc validate [root] --json",
776
810
  model: "filegrc model --json",
@@ -783,7 +817,7 @@ function agentOverview(model) {
783
817
  search: "filegrc search <query> --json",
784
818
  obligations: "filegrc obligations --json",
785
819
  programReadiness: "filegrc program-readiness --json",
786
- evidenceTestDrafts: "filegrc evidence-test-drafts --json",
820
+ evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
787
821
  auditReadiness: "filegrc audit-readiness <audit-id> --json",
788
822
  prepareAudit: "filegrc prepare-audit <audit-id>",
789
823
  trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
@@ -924,13 +958,17 @@ function printProgramPath(result) {
924
958
  }
925
959
 
926
960
  function summarizeProgramReadiness(result) {
961
+ const ownership = result.stages
962
+ .flatMap((stage) => stage.items)
963
+ .find((item) => item.id === "program-ownership");
927
964
  const summarizeItem = (item) => item ? {
928
965
  id: item.id,
929
966
  status: item.status,
930
967
  title: item.title,
931
968
  message: item.message,
932
969
  ...(item.resourceType ? { resourceType: item.resourceType } : {}),
933
- ...(item.resourceId ? { resourceId: item.resourceId } : {})
970
+ ...(item.resourceId ? { resourceId: item.resourceId } : {}),
971
+ ...(item.unresolvedAssignments?.length ? { unresolvedAssignments: item.unresolvedAssignments } : {})
934
972
  } : null;
935
973
  return {
936
974
  schemaVersion: result.schemaVersion,
@@ -947,6 +985,7 @@ function summarizeProgramReadiness(result) {
947
985
  scopeCounts: Object.fromEntries(
948
986
  Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
949
987
  ),
988
+ unresolvedOwnership: ownership?.unresolvedAssignments || [],
950
989
  firstAction: summarizeItem(result.firstAction),
951
990
  stages: result.stages.map((stage) => ({
952
991
  id: stage.id,
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"
@@ -203,9 +203,21 @@ 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(
217
+ `${unresolved.length} ${unresolved.length === 1 ? "record has" : "records have"} no current person owner: ` +
218
+ `${unresolvedAssignments.map(({ title, resourceId }) => `${title} (${resourceId})`).join(", ")}.`
219
+ );
220
+ }
209
221
  if (!oversightComplete) detail.push("Finish and activate Security and Risk Oversight with a current chair who is separate from policy ownership.");
210
222
  return item(
211
223
  "program-ownership",
@@ -214,10 +226,29 @@ function programOwnershipItem(records, byId) {
214
226
  complete
215
227
  ? `${currentOwners.size} current ${currentOwners.size === 1 ? "person owns" : "people own"} the program records.${oversight ? " Security and Risk Oversight has a separate current chair." : ""}`
216
228
  : detail.join(" "),
217
- !oversightComplete ? oversight : unresolved[0] || { type: "person" }
229
+ !oversightComplete ? oversight : unresolved[0] || { type: "person" },
230
+ { unresolvedAssignments }
218
231
  );
219
232
  }
220
233
 
234
+ function ownershipResolutionReasons(ownerIds, byId) {
235
+ if (!ownerIds.length) return [{ ownerId: null, reason: "missing-owner" }];
236
+ return ownerIds.flatMap((ownerId) => {
237
+ const owner = byId.get(ownerId);
238
+ if (!owner) return [{ ownerId, reason: "missing-record" }];
239
+ if (owner.type === "person" && owner.status !== "active") {
240
+ return [{ ownerId, reason: "inactive-person" }];
241
+ }
242
+ if (owner.type === "team" && owner.status !== "active") {
243
+ return [{ ownerId, reason: "inactive-team" }];
244
+ }
245
+ if (currentPartyPeople([ownerId], byId).size === 0) {
246
+ return [{ ownerId, reason: owner.type === "team" ? "team-has-no-current-members" : "no-current-person" }];
247
+ }
248
+ return [];
249
+ });
250
+ }
251
+
221
252
  async function policiesStage(scope, byId, readMarkdown, asOf) {
222
253
  const linkedPolicyIds = new Set(scope.controls.flatMap((control) => control.policyIds || []));
223
254
  const policies = [...linkedPolicyIds].map((id) => byId.get(id)).filter((record) => (
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,61 @@ 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
+ linkedControlIds: [],
23
+ evidenceTestDraftIds: [],
24
+ onboardingComplete: !setup.draft
63
25
  };
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);
26
+ }
77
27
 
28
+ export async function planWorkspaceSetup(input = process.cwd(), payload = {}) {
29
+ const loaded = await loadWorkspace(input);
30
+ const setup = normalizeSetupPayload(payload);
31
+ validateSetup(loaded, setup);
32
+ const plan = buildSetupRecords(loaded, setup);
78
33
  return {
34
+ schemaVersion: 1,
35
+ preview: true,
79
36
  draft: setup.draft,
80
- system,
81
- workspace,
82
- linkedControlIds,
83
- evidenceTestDraftIds: evidenceTestDrafts.created.map(({ id }) => id),
37
+ changes: {
38
+ system: plan.existingSystem ? "update" : "create",
39
+ workspace: "update",
40
+ renderer: plan.renderer ? "update" : "unchanged",
41
+ controls: 0,
42
+ evidenceDrafts: 0
43
+ },
44
+ system: setupSystemSummary(plan.system),
45
+ target: setupTargetSummary(plan.workspace),
84
46
  onboardingComplete: !setup.draft
85
47
  };
86
48
  }
87
49
 
50
+ export function summarizeSetupResult(result) {
51
+ return {
52
+ schemaVersion: 1,
53
+ preview: false,
54
+ draft: result.draft,
55
+ changes: {
56
+ system: "saved",
57
+ workspace: "updated",
58
+ controls: result.linkedControlIds?.length || 0,
59
+ evidenceDrafts: result.evidenceTestDraftIds?.length || 0
60
+ },
61
+ system: setupSystemSummary(result.system),
62
+ target: setupTargetSummary(result.workspace),
63
+ onboardingComplete: result.onboardingComplete
64
+ };
65
+ }
66
+
88
67
  export function normalizeSetupPayload(payload = {}) {
89
68
  if (!payload || Array.isArray(payload) || typeof payload !== "object") {
90
69
  throw new Error("Setup input must be a JSON object.");
@@ -146,6 +125,44 @@ function findSetupSystem(resources, setup) {
146
125
  ));
147
126
  }
148
127
 
128
+ function buildSetupRecords(loaded, setup) {
129
+ const existingSystem = findSetupSystem(loaded.resources, setup);
130
+ const systemId = existingSystem?.id || createResourceId(
131
+ "system",
132
+ setup.serviceName,
133
+ loaded.resources.map(({ id }) => id)
134
+ );
135
+ const system = {
136
+ ...(existingSystem || {}),
137
+ schemaVersion: 1,
138
+ id: systemId,
139
+ type: "system",
140
+ title: setup.serviceName,
141
+ status: setup.draft
142
+ ? existingSystem?.status || "planned"
143
+ : existingSystem?.status === "planned"
144
+ ? "active"
145
+ : existingSystem?.status || "active",
146
+ criticality: setup.criticality,
147
+ ownerIds: [setup.ownerId],
148
+ description: setup.boundary,
149
+ systemKind: existingSystem?.systemKind || "service",
150
+ dataClassification: setup.dataClassification,
151
+ internetExposed: setup.internetExposed,
152
+ inScope: true
153
+ };
154
+ const existingWorkspace = loaded.resources.find(({ type }) => type === "workspace");
155
+ if (!existingWorkspace) throw new Error("The workspace settings record was not found.");
156
+ const workspace = {
157
+ ...existingWorkspace,
158
+ assuranceGoal: assuranceGoalFromSetup(setup.programGoal),
159
+ systemIds: [...new Set([...(existingWorkspace.systemIds || []), systemId])]
160
+ };
161
+ const existingRenderer = loaded.resources.find(({ type }) => type === "renderer-settings");
162
+ const renderer = existingRenderer ? { ...existingRenderer, showOnboarding: setup.draft } : null;
163
+ return { existingSystem, system, workspace, renderer };
164
+ }
165
+
149
166
  async function upsertResource(root, existing, record) {
150
167
  return existing
151
168
  ? updateResource(root, record.type, record.id, record)
@@ -159,6 +176,27 @@ function assuranceGoalFromSetup(goal) {
159
176
  return "none";
160
177
  }
161
178
 
179
+ function setupSystemSummary(system) {
180
+ return {
181
+ id: system.id,
182
+ title: system.title,
183
+ status: system.status,
184
+ inScope: system.inScope
185
+ };
186
+ }
187
+
188
+ function setupTargetSummary(workspace) {
189
+ return {
190
+ assuranceGoal: workspace.assuranceGoal,
191
+ scopeCounts: {
192
+ systems: workspace.systemIds?.length || 0,
193
+ frameworks: workspace.frameworkIds?.length || 0,
194
+ requirements: workspace.requirementIds?.length || 0,
195
+ controls: workspace.controlIds?.length || 0
196
+ }
197
+ };
198
+ }
199
+
162
200
  function booleanValue(value, name) {
163
201
  if (value === true || value === false) return value;
164
202
  if (value === "true") return true;