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 +5 -5
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/cli.js +222 -55
- package/src/index.js +1 -1
- package/src/program-path.js +9 -4
- package/src/program-readiness.js +67 -5
- package/src/setup.js +111 -66
- package/src/web.js +4 -4
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
|
|
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
|
|
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`
|
|
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
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 =
|
|
87
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
754
|
-
already have a dedicated Step 5 operating record. Existing tests are
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
|
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";
|
package/src/program-path.js
CHANGED
|
@@ -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)
|
package/src/program-readiness.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
:
|
|
239
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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:
|
|
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}
|
|
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}}
|