filegrc 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ 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
@@ -38,7 +38,7 @@ npx filegrc evidence-packet --start 2026-01-01 --end 2026-06-30 --audit audit-id
38
38
 
39
39
  `filegrc setup` provides the headless equivalent of browser onboarding. Run it without arguments for guided terminal setup, or pass all initial service-boundary fields and a management program goal as flags or a JSON payload. Add `--preview` to validate and inspect the planned service and workspace writes without saving. Add `--summary --json` for compact agent output. Selecting Type 1 or Type 2 updates the workspace goal and selected systems. Setup does not select framework records, link controls, create evidence, or create an audit record.
40
40
 
41
- `filegrc program-path` gives agents the renderer’s six-step order, exact page Instructions, Use, Policy Basis, commands, current state, and next actions. `filegrc guide <type>` repeats the matching page guidance and adds fields, relationship candidates, Markdown slots, and timing for that record type.
41
+ `filegrc program-path --next --json` gives an agent the current step and first action without loading the full lifecycle. Use `--summary` for compact status across all six steps, `--current` for the full current-step guide, or no compact flag for every step. `filegrc guide <type>` repeats the matching page guidance and adds fields, relationship candidates, Markdown slots, and timing for that record type.
42
42
 
43
43
  `filegrc program-readiness` reports whether management can start a candidate Type 2 period. Add `--summary --json` for compact stage counts and next actions, or omit `--summary` for every readiness item. Use `--require-ready` in automation. The command does not require an audit ID or CPA firm.
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -93,7 +93,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
93
93
  "Inspect existing records and relation candidates before writing.",
94
94
  "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
95
95
  "Keep stable metadata in JSON and put the work performed, inputs, results, decisions, exceptions, and follow-up in the recommended Markdown companion.",
96
- "Run filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
96
+ "Run npx filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
97
97
  ],
98
98
  completionChecks: [
99
99
  "The record describes work that actually occurred; planned work is not marked complete.",
package/src/build.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { FAVICON_PNG } from "./favicon.js";
3
+ import { FAVICON_PNG, LOGO_MARK_PNG } from "./favicon.js";
4
4
  import { resolveWorkspacePath, resolveWorkspaceRoot } from "./paths.js";
5
5
  import { createAppState } from "./state.js";
6
6
  import { APP_SCRIPT, APP_STYLES, renderIndex } from "./web.js";
@@ -12,6 +12,7 @@ export async function buildWorkspace(input = process.cwd(), options = {}) {
12
12
  const paths = {
13
13
  html: resolveWorkspacePath(root, join(outputOption, "index.html")),
14
14
  favicon: resolveWorkspacePath(root, join(outputOption, "favicon.png")),
15
+ logoMark: resolveWorkspacePath(root, join(outputOption, "logo-mark-white.png")),
15
16
  script: resolveWorkspacePath(root, join(outputOption, "filegrc-app.js")),
16
17
  styles: resolveWorkspacePath(root, join(outputOption, "filegrc.css"))
17
18
  };
@@ -20,6 +21,7 @@ export async function buildWorkspace(input = process.cwd(), options = {}) {
20
21
  await Promise.all([
21
22
  writeFile(paths.html, renderIndex(state), "utf8"),
22
23
  writeFile(paths.favicon, FAVICON_PNG),
24
+ writeFile(paths.logoMark, LOGO_MARK_PNG),
23
25
  writeFile(paths.script, APP_SCRIPT, "utf8"),
24
26
  writeFile(paths.styles, APP_STYLES, "utf8")
25
27
  ]);
package/src/cli.js CHANGED
@@ -29,6 +29,7 @@ import { markdownEntries } from "./resource-markdown.js";
29
29
  import { searchResources } from "./search.js";
30
30
  import { serveWorkspace } from "./server.js";
31
31
  import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
32
+ import { printGithubStarMessage } from "./startup.js";
32
33
  import { createAppState } from "./state.js";
33
34
  import { currentCalendarDate } from "./time.js";
34
35
  import { validateWorkspace } from "./validate.js";
@@ -37,10 +38,12 @@ import { loadWorkspace } from "./workspace.js";
37
38
  const BOOLEAN_FLAGS = new Set([
38
39
  "check-docs",
39
40
  "complete",
41
+ "current",
40
42
  "draft",
41
43
  "help",
42
44
  "json",
43
45
  "mutation",
46
+ "next",
44
47
  "preview",
45
48
  "require-ready",
46
49
  "summary",
@@ -62,13 +65,19 @@ export async function runCli(argv = process.argv.slice(2)) {
62
65
  host: flags.host ?? process.env.FILEGRC_HOST,
63
66
  port: flags.port ?? process.env.FILEGRC_PORT
64
67
  });
65
- console.log(`filegrc workspace: ${result.url}`);
66
- console.log(`Data: ${result.root}/data`);
67
- return await new Promise((resolvePromise) => {
68
- const stop = () => result.server.close(resolvePromise);
68
+ const stopped = new Promise((resolvePromise) => {
69
+ const stop = () => {
70
+ process.removeListener("SIGINT", stop);
71
+ process.removeListener("SIGTERM", stop);
72
+ result.server.close(resolvePromise);
73
+ };
69
74
  process.once("SIGINT", stop);
70
75
  process.once("SIGTERM", stop);
71
76
  });
77
+ console.log(`filegrc workspace: ${result.url}`);
78
+ console.log(`Data: ${result.root}/data`);
79
+ printGithubStarMessage();
80
+ return await stopped;
72
81
  }
73
82
  if (command === "setup") {
74
83
  const payload = positionals[0] ? await readSetupPayload(positionals[0]) : {};
@@ -95,8 +104,11 @@ export async function runCli(argv = process.argv.slice(2)) {
95
104
  else {
96
105
  console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
97
106
  console.log(`System: ${result.system.id} (${result.system.status})`);
107
+ if (result.draft) {
108
+ console.log("Planned and in scope means selected for scope review, not approved or active.");
109
+ }
98
110
  console.log(`Target: ${result.workspace.assuranceGoal}`);
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.");
111
+ console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run npx filegrc program-path --next --json.");
100
112
  }
101
113
  return output;
102
114
  }
@@ -165,9 +177,10 @@ export async function runCli(argv = process.argv.slice(2)) {
165
177
  const auditId = positionals[0] || flags.audit;
166
178
  const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
167
179
  const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
168
- if (flags.json) console.log(JSON.stringify(result, null, 2));
169
- else printProgramPath(result);
170
- return result;
180
+ const output = selectProgramPathOutput(result, flags);
181
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
182
+ else printProgramPathOutput(output, flags);
183
+ return output;
171
184
  }
172
185
  if (command === "scaffold") {
173
186
  const loaded = await loadWorkspace(root);
@@ -678,7 +691,7 @@ Usage:
678
691
  filegrc describe <resource-type>
679
692
  filegrc types [--json]
680
693
  filegrc guide [resource-type] [--id resource-id] [--json]
681
- filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--json]
694
+ filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--summary|--next|--current] [--json]
682
695
  filegrc scaffold <resource-type> --title text [--id resource-id]
683
696
  filegrc list [resource-type] [--json]
684
697
  filegrc search <query> [--type resource-type] [--json]
@@ -773,7 +786,10 @@ and current readiness state. Pass an audit ID to include Step 6 status.
773
786
  Options:
774
787
  --audit <id> Audit record to use for Step 6
775
788
  --as-of <date> Evaluate readiness on YYYY-MM-DD
776
- --json Print the full agent-oriented path as JSON
789
+ --summary Print compact status and the first action for all six steps
790
+ --next Print only the current step and its first action
791
+ --current Print the full guide for the current step only
792
+ --json Print the selected path view as JSON
777
793
  --root <path> Workspace path
778
794
  --help Show this help`);
779
795
  return;
@@ -797,44 +813,48 @@ Options:
797
813
  }
798
814
 
799
815
  function agentOverview(model) {
816
+ const commands = {
817
+ help: "filegrc help",
818
+ version: "filegrc version",
819
+ serve: "filegrc serve [root]",
820
+ setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
821
+ build: "filegrc build [root]",
822
+ validate: "filegrc validate [root] --json",
823
+ model: "filegrc model --json",
824
+ describe: "filegrc describe <resource-type>",
825
+ types: "filegrc types --json",
826
+ guide: "filegrc guide [resource-type] --json",
827
+ programPath: "filegrc program-path [audit-id] --next --json",
828
+ scaffold: "filegrc scaffold <resource-type> --title <name>",
829
+ list: "filegrc list [resource-type] --json",
830
+ search: "filegrc search <query> --json",
831
+ obligations: "filegrc obligations --json",
832
+ programReadiness: "filegrc program-readiness --json",
833
+ evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
834
+ auditReadiness: "filegrc audit-readiness <audit-id> --json",
835
+ prepareAudit: "filegrc prepare-audit <audit-id>",
836
+ trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
837
+ evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
838
+ get: "filegrc get <resource-id> [--mutation]",
839
+ references: "filegrc references <resource-id> --json",
840
+ create: "filegrc create <record-or-mutation.json>",
841
+ complete: "filegrc complete <obligation-id> <completion-mutation.json>",
842
+ completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
843
+ completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
844
+ update: "filegrc update <resource-type> <id> <record-or-mutation.json>",
845
+ content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
846
+ attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
847
+ detach: "filegrc detach <evidence-id> <attachment-name> --yes",
848
+ delete: "filegrc delete <resource-type> <id> --yes",
849
+ commit: "git diff --check && git diff && git add <reviewed-paths> && git commit -m <reason>"
850
+ };
800
851
  return {
801
852
  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.",
802
853
  programPath: buildAgentProgramPath(model),
803
- actions: {
804
- help: "filegrc help",
805
- version: "filegrc version",
806
- serve: "filegrc serve [root]",
807
- setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
808
- build: "filegrc build [root]",
809
- validate: "filegrc validate [root] --json",
810
- model: "filegrc model --json",
811
- describe: "filegrc describe <resource-type>",
812
- types: "filegrc types --json",
813
- guide: "filegrc guide [resource-type] --json",
814
- programPath: "filegrc program-path [audit-id] --json",
815
- scaffold: "filegrc scaffold <resource-type> --title <name>",
816
- list: "filegrc list [resource-type] --json",
817
- search: "filegrc search <query> --json",
818
- obligations: "filegrc obligations --json",
819
- programReadiness: "filegrc program-readiness --json",
820
- evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
821
- auditReadiness: "filegrc audit-readiness <audit-id> --json",
822
- prepareAudit: "filegrc prepare-audit <audit-id>",
823
- trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
824
- evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
825
- get: "filegrc get <resource-id> [--mutation]",
826
- references: "filegrc references <resource-id> --json",
827
- create: "filegrc create <record-or-mutation.json>",
828
- complete: "filegrc complete <obligation-id> <completion-mutation.json>",
829
- completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
830
- completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
831
- update: "filegrc update <resource-type> <id> <record-or-mutation.json>",
832
- content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
833
- attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
834
- detach: "filegrc detach <evidence-id> <attachment-name> --yes",
835
- delete: "filegrc delete <resource-type> <id> --yes",
836
- commit: "git diff --check && git diff && git add <reviewed-paths> && git commit -m <reason>"
837
- },
854
+ actions: Object.fromEntries(Object.entries(commands).map(([name, command]) => [
855
+ name,
856
+ command.startsWith("filegrc ") ? `npx ${command}` : command
857
+ ])),
838
858
  resourceTypes: listResourceTypes(model).map(({ type, title, group }) => ({ type, title, group }))
839
859
  };
840
860
  }
@@ -885,12 +905,12 @@ function printAgentGuide(result) {
885
905
  for (const field of relationshipFields) {
886
906
  const hasCandidates = field.relation.candidates.length > 0;
887
907
  const suffix = field.relation.truncated && hasCandidates
888
- ? `, … (${field.relation.candidateCount} total; use filegrc list)`
908
+ ? `, … (${field.relation.candidateCount} total; use npx filegrc list)`
889
909
  : "";
890
910
  const candidates = hasCandidates
891
911
  ? field.relation.candidates.join(", ") + suffix
892
912
  : field.relation.candidateCount
893
- ? `use filegrc list (${field.relation.candidateCount} possible)`
913
+ ? `use npx filegrc list (${field.relation.candidateCount} possible)`
894
914
  : "none";
895
915
  console.log(`${field.name}\t${field.relation.types.join("|")}\t${candidates}`);
896
916
  }
@@ -957,19 +977,130 @@ function printProgramPath(result) {
957
977
  }
958
978
  }
959
979
 
980
+ function selectProgramPathOutput(result, flags) {
981
+ const modes = ["summary", "next", "current"].filter((name) => flags[name]);
982
+ if (modes.length > 1) throw new Error("Use only one of --summary, --next, or --current.");
983
+ if (flags.summary) return summarizeProgramPath(result);
984
+ if (flags.next) return nextProgramPath(result);
985
+ if (flags.current) {
986
+ const stage = result.stages.find(({ id }) => id === result.currentStep.id);
987
+ return { ...result, stages: stage ? [stage] : [] };
988
+ }
989
+ return result;
990
+ }
991
+
992
+ function summarizeProgramPath(result) {
993
+ return {
994
+ schemaVersion: result.schemaVersion,
995
+ asOf: result.asOf,
996
+ currentStep: result.currentStep,
997
+ evidenceReady: result.evidenceReady,
998
+ operating: result.operating,
999
+ stages: result.stages.map((stage) => ({
1000
+ id: stage.id,
1001
+ number: stage.number,
1002
+ title: stage.title,
1003
+ status: stage.status,
1004
+ counts: stage.counts,
1005
+ nextAction: summarizePathAction(stage.nextActions[0])
1006
+ }))
1007
+ };
1008
+ }
1009
+
1010
+ function nextProgramPath(result) {
1011
+ const stage = result.stages.find(({ id }) => id === result.currentStep.id);
1012
+ const nextAction = stage?.nextActions[0];
1013
+ return {
1014
+ schemaVersion: result.schemaVersion,
1015
+ asOf: result.asOf,
1016
+ currentStep: result.currentStep,
1017
+ evidenceReady: result.evidenceReady,
1018
+ operating: result.operating,
1019
+ step: stage ? {
1020
+ id: stage.id,
1021
+ number: stage.number,
1022
+ title: stage.title,
1023
+ status: stage.status,
1024
+ summary: stage.summary,
1025
+ nextAction: summarizePathAction(nextAction),
1026
+ commands: nextActionCommands(stage, nextAction)
1027
+ } : null
1028
+ };
1029
+ }
1030
+
1031
+ function summarizePathAction(action) {
1032
+ if (!action) return null;
1033
+ return {
1034
+ id: action.id,
1035
+ status: action.status,
1036
+ title: action.title,
1037
+ message: action.message,
1038
+ ...(action.resourceType ? { resourceType: action.resourceType } : {}),
1039
+ ...(action.resourceId ? { resourceId: action.resourceId } : {})
1040
+ };
1041
+ }
1042
+
1043
+ function nextActionCommands(stage, action) {
1044
+ if (action?.commands?.length) return action.commands;
1045
+ if (!action?.resourceType) return stage.commands;
1046
+ const resourceType = shellArgument(action.resourceType);
1047
+ const commands = [`npx filegrc guide ${resourceType} --json`];
1048
+ if (action.resourceId) {
1049
+ const resourceId = shellArgument(action.resourceId);
1050
+ commands.push(`npx filegrc get ${resourceId} --mutation`);
1051
+ commands.push(`npx filegrc update ${resourceType} ${resourceId} MUTATION.json --json`);
1052
+ } else {
1053
+ commands.push(`npx filegrc list ${resourceType} --json`);
1054
+ commands.push(`npx filegrc scaffold ${resourceType} --title "NAME"`);
1055
+ commands.push("npx filegrc create MUTATION.json --json");
1056
+ }
1057
+ return commands;
1058
+ }
1059
+
1060
+ function shellArgument(value) {
1061
+ const text = String(value);
1062
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(text)
1063
+ ? text
1064
+ : `'${text.replaceAll("'", "'\\''")}'`;
1065
+ }
1066
+
1067
+ function printProgramPathOutput(result, flags) {
1068
+ if (flags.summary) {
1069
+ console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
1070
+ for (const stage of result.stages) {
1071
+ console.log(`${String(stage.status).toUpperCase()}\tStep ${stage.number}\t${stage.title}`);
1072
+ }
1073
+ const current = result.stages.find(({ id }) => id === result.currentStep.id);
1074
+ if (current?.nextAction) console.log(`Next: ${current.nextAction.title} · ${current.nextAction.message}`);
1075
+ return;
1076
+ }
1077
+ if (flags.next) {
1078
+ console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
1079
+ if (result.step?.nextAction) {
1080
+ console.log(`Next: ${result.step.nextAction.title} · ${result.step.nextAction.message}`);
1081
+ }
1082
+ for (const command of result.step?.commands || []) console.log(` ${command}`);
1083
+ return;
1084
+ }
1085
+ printProgramPath(result);
1086
+ }
1087
+
960
1088
  function summarizeProgramReadiness(result) {
961
1089
  const ownership = result.stages
962
1090
  .flatMap((stage) => stage.items)
963
1091
  .find((item) => item.id === "program-ownership");
964
- const summarizeItem = (item) => item ? {
1092
+ const summarizeItem = (item, options = {}) => item ? {
965
1093
  id: item.id,
966
1094
  status: item.status,
967
1095
  title: item.title,
968
- message: item.message,
1096
+ ...(options.message === false ? {} : { message: item.message }),
969
1097
  ...(item.resourceType ? { resourceType: item.resourceType } : {}),
970
- ...(item.resourceId ? { resourceId: item.resourceId } : {}),
971
- ...(item.unresolvedAssignments?.length ? { unresolvedAssignments: item.unresolvedAssignments } : {})
1098
+ ...(item.resourceId ? { resourceId: item.resourceId } : {})
972
1099
  } : null;
1100
+ const unresolvedOwnership = ownership?.unresolvedAssignments || [];
1101
+ const ownershipReasons = unresolvedOwnership
1102
+ .flatMap((assignment) => assignment.reasons || [])
1103
+ .reduce((counts, { reason }) => ({ ...counts, [reason]: (counts[reason] || 0) + 1 }), {});
973
1104
  return {
974
1105
  schemaVersion: result.schemaVersion,
975
1106
  generatedAt: result.generatedAt,
@@ -985,14 +1116,18 @@ function summarizeProgramReadiness(result) {
985
1116
  scopeCounts: Object.fromEntries(
986
1117
  Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
987
1118
  ),
988
- unresolvedOwnership: ownership?.unresolvedAssignments || [],
1119
+ unresolvedOwnership: {
1120
+ count: unresolvedOwnership.length,
1121
+ byReason: ownershipReasons,
1122
+ resourceIds: unresolvedOwnership.map(({ resourceId }) => resourceId)
1123
+ },
989
1124
  firstAction: summarizeItem(result.firstAction),
990
1125
  stages: result.stages.map((stage) => ({
991
1126
  id: stage.id,
992
1127
  title: stage.title,
993
1128
  status: stage.status,
994
1129
  counts: stage.counts,
995
- firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"))
1130
+ firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"), { message: false })
996
1131
  }))
997
1132
  };
998
1133
  }
package/src/favicon.js CHANGED
@@ -1,109 +1,4 @@
1
- import { deflateSync } from "node:zlib";
1
+ import { readFileSync } from "node:fs";
2
2
 
3
- const SIZE = 64;
4
- const WHITE = [248, 249, 255, 255];
5
-
6
- export const FAVICON_PNG = createFavicon();
7
-
8
- function createFavicon() {
9
- const pixels = Buffer.alloc(SIZE * SIZE * 4);
10
-
11
- for (let y = 0; y < SIZE; y += 1) {
12
- for (let x = 0; x < SIZE; x += 1) {
13
- if (!insideRoundedSquare(x, y, 11)) continue;
14
- setPixel(pixels, x, y, backgroundColor(x, y));
15
- }
16
- }
17
-
18
- drawStroke(pixels, [
19
- [18, 8],
20
- [39, 8],
21
- [51, 20],
22
- [51, 50],
23
- [50, 53],
24
- [47, 55],
25
- [17, 55],
26
- [14, 54],
27
- [12, 51],
28
- [12, 13],
29
- [14, 10],
30
- [18, 8]
31
- ], 2.3, WHITE);
32
- drawStroke(pixels, [[39, 8], [39, 20], [51, 20]], 2.3, WHITE);
33
-
34
- const rows = Buffer.alloc((SIZE * 4 + 1) * SIZE);
35
- for (let y = 0; y < SIZE; y += 1) {
36
- const rowOffset = y * (SIZE * 4 + 1);
37
- rows[rowOffset] = 0;
38
- pixels.copy(rows, rowOffset + 1, y * SIZE * 4, (y + 1) * SIZE * 4);
39
- }
40
-
41
- const header = Buffer.alloc(13);
42
- header.writeUInt32BE(SIZE, 0);
43
- header.writeUInt32BE(SIZE, 4);
44
- header.set([8, 6, 0, 0, 0], 8);
45
-
46
- return Buffer.concat([
47
- Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
48
- pngChunk("IHDR", header),
49
- pngChunk("IDAT", deflateSync(rows)),
50
- pngChunk("IEND", Buffer.alloc(0))
51
- ]);
52
- }
53
-
54
- function insideRoundedSquare(x, y, radius) {
55
- const cornerX = x < radius ? radius - 1 : x >= SIZE - radius ? SIZE - radius : x;
56
- const cornerY = y < radius ? radius - 1 : y >= SIZE - radius ? SIZE - radius : y;
57
- return Math.hypot(x - cornerX, y - cornerY) <= radius;
58
- }
59
-
60
- function backgroundColor(x, y) {
61
- const progress = Math.min(1, (x + y) / ((SIZE - 1) * 1.2));
62
- return [0, 0, Math.round(112 + (53 - 112) * progress), 255];
63
- }
64
-
65
- function drawStroke(pixels, points, radius, color) {
66
- for (let y = 0; y < SIZE; y += 1) {
67
- for (let x = 0; x < SIZE; x += 1) {
68
- const onStroke = points.slice(1).some((point, index) => (
69
- distanceToSegment(x, y, points[index], point) <= radius
70
- ));
71
- if (onStroke) setPixel(pixels, x, y, color);
72
- }
73
- }
74
- }
75
-
76
- function distanceToSegment(x, y, start, end) {
77
- const dx = end[0] - start[0];
78
- const dy = end[1] - start[1];
79
- const lengthSquared = dx * dx + dy * dy;
80
- const progress = lengthSquared
81
- ? Math.max(0, Math.min(1, ((x - start[0]) * dx + (y - start[1]) * dy) / lengthSquared))
82
- : 0;
83
- return Math.hypot(x - (start[0] + progress * dx), y - (start[1] + progress * dy));
84
- }
85
-
86
- function setPixel(pixels, x, y, color) {
87
- pixels.set(color, (y * SIZE + x) * 4);
88
- }
89
-
90
- function pngChunk(type, data) {
91
- const typeBuffer = Buffer.from(type, "ascii");
92
- const chunk = Buffer.alloc(data.length + 12);
93
- chunk.writeUInt32BE(data.length, 0);
94
- typeBuffer.copy(chunk, 4);
95
- data.copy(chunk, 8);
96
- chunk.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), data.length + 8);
97
- return chunk;
98
- }
99
-
100
- function crc32(buffer) {
101
- let value = 0xffffffff;
102
- for (const byte of buffer) {
103
- value ^= byte;
104
- for (let bit = 0; bit < 8; bit += 1) {
105
- value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0);
106
- }
107
- }
108
- return (value ^ 0xffffffff) >>> 0;
109
- }
3
+ export const FAVICON_PNG = readFileSync(new URL("./favicon.png", import.meta.url));
4
+ export const LOGO_MARK_PNG = readFileSync(new URL("./logo-mark-white.png", import.meta.url));
Binary file
Binary file
@@ -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)
@@ -213,12 +213,10 @@ function programOwnershipItem(records, byId) {
213
213
  const detail = [];
214
214
  if (!currentOwners.size) detail.push("No current person owns the program records.");
215
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
- );
216
+ detail.push(`${unresolved.length} ${unresolved.length === 1 ? "record has" : "records have"} no current person owner.`);
220
217
  }
221
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;
222
220
  return item(
223
221
  "program-ownership",
224
222
  complete ? "complete" : "action",
@@ -227,7 +225,18 @@ function programOwnershipItem(records, byId) {
227
225
  ? `${currentOwners.size} current ${currentOwners.size === 1 ? "person owns" : "people own"} the program records.${oversight ? " Security and Risk Oversight has a separate current chair." : ""}`
228
226
  : detail.join(" "),
229
227
  !oversightComplete ? oversight : unresolved[0] || { type: "person" },
230
- { unresolvedAssignments }
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
+ }
231
240
  );
232
241
  }
233
242
 
@@ -259,15 +268,30 @@ async function policiesStage(scope, byId, readMarkdown, asOf) {
259
268
  .flatMap((policy) => [...currentPartyPeople(policy.approverIds || [], byId)])
260
269
  .map((id) => byId.get(id))
261
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;
262
282
  const items = [
263
283
  item(
264
284
  "independent-reviewer",
265
285
  appointedReviewer ? "complete" : "action",
266
- "Appoint the independent policy reviewer",
286
+ reviewerNeedsAssignment
287
+ ? "Assign the independent policy reviewer"
288
+ : "Appoint the independent policy reviewer",
267
289
  appointedReviewer
268
290
  ? `${appointedReviewer.title} is recorded as a reviewer separate from policy ownership.`
269
- : "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.",
270
- appointedReviewer || { type: "person" }
291
+ : reviewerNeedsAssignment
292
+ ? `${availableReviewer.title} chairs Security and Risk Oversight. Assign this person as approver on each policy after review.`
293
+ : "Appoint a reviewer who is separate from the policy owner. The reviewer may be another person in the organization or an external person, and is separate from the CPA firm that may later perform the audit.",
294
+ appointedReviewer || (reviewerNeedsAssignment ? policies[0] : { type: "person" })
271
295
  )
272
296
  ];
273
297
  if (!policies.length) {
@@ -670,6 +694,13 @@ function item(id, status, title, message, resource = {}, details = {}) {
670
694
  };
671
695
  }
672
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
+
673
704
  function countStatuses(items) {
674
705
  const counts = { complete: 0, action: 0, later: 0, info: 0 };
675
706
  for (const current of items) counts[current.status] = (counts[current.status] || 0) + 1;
package/src/server.js CHANGED
@@ -5,7 +5,7 @@ import { getResourceDefinition } from "../model/index.js";
5
5
  import { prepareAuditWorkspace } from "./audit-preparation.js";
6
6
  import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
7
7
  import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
8
- import { FAVICON_PNG } from "./favicon.js";
8
+ import { FAVICON_PNG, LOGO_MARK_PNG } from "./favicon.js";
9
9
  import { createResource, deleteResource, updateContent, updateResource } from "./files.js";
10
10
  import { commitAndPushWorkspace, getFileHistory, pullWorkspace, pushWorkspace } from "./git.js";
11
11
  import { completeObligationOccurrence, createObligationEvent, planObligations } from "./obligations.js";
@@ -137,6 +137,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
137
137
  }
138
138
  }
139
139
  if (request.method === "GET" && url.pathname === "/favicon.png") return text(response, 200, FAVICON_PNG, "image/png");
140
+ if (request.method === "GET" && url.pathname === "/logo-mark-white.png") return text(response, 200, LOGO_MARK_PNG, "image/png");
140
141
  if (request.method === "GET" && url.pathname === "/filegrc-app.js") return text(response, 200, APP_SCRIPT, "text/javascript; charset=utf-8");
141
142
  if (request.method === "GET" && url.pathname === "/filegrc.css") return text(response, 200, APP_STYLES, "text/css; charset=utf-8");
142
143
  if (request.method === "GET" && url.pathname.startsWith("/packet/")) {
package/src/setup.js CHANGED
@@ -19,6 +19,7 @@ export async function setupWorkspace(input = process.cwd(), payload = {}) {
19
19
  draft: setup.draft,
20
20
  system: plan.system,
21
21
  workspace: plan.workspace,
22
+ renderer: plan.renderer,
22
23
  linkedControlIds: [],
23
24
  evidenceTestDraftIds: [],
24
25
  onboardingComplete: !setup.draft
@@ -43,6 +44,7 @@ export async function planWorkspaceSetup(input = process.cwd(), payload = {}) {
43
44
  },
44
45
  system: setupSystemSummary(plan.system),
45
46
  target: setupTargetSummary(plan.workspace),
47
+ renderer: plan.renderer ? setupRendererSummary(plan.renderer) : null,
46
48
  onboardingComplete: !setup.draft
47
49
  };
48
50
  }
@@ -60,6 +62,7 @@ export function summarizeSetupResult(result) {
60
62
  },
61
63
  system: setupSystemSummary(result.system),
62
64
  target: setupTargetSummary(result.workspace),
65
+ renderer: result.renderer ? setupRendererSummary(result.renderer) : null,
63
66
  onboardingComplete: result.onboardingComplete
64
67
  };
65
68
  }
@@ -177,17 +180,13 @@ function assuranceGoalFromSetup(goal) {
177
180
  }
178
181
 
179
182
  function setupSystemSummary(system) {
180
- return {
181
- id: system.id,
182
- title: system.title,
183
- status: system.status,
184
- inScope: system.inScope
185
- };
183
+ return { ...system };
186
184
  }
187
185
 
188
186
  function setupTargetSummary(workspace) {
189
187
  return {
190
188
  assuranceGoal: workspace.assuranceGoal,
189
+ systemIds: [...(workspace.systemIds || [])],
191
190
  scopeCounts: {
192
191
  systems: workspace.systemIds?.length || 0,
193
192
  frameworks: workspace.frameworkIds?.length || 0,
@@ -197,6 +196,14 @@ function setupTargetSummary(workspace) {
197
196
  };
198
197
  }
199
198
 
199
+ function setupRendererSummary(renderer) {
200
+ return {
201
+ id: renderer.id,
202
+ type: renderer.type,
203
+ showOnboarding: renderer.showOnboarding
204
+ };
205
+ }
206
+
200
207
  function booleanValue(value, name) {
201
208
  if (value === true || value === false) return value;
202
209
  if (value === "true") return true;
package/src/startup.js ADDED
@@ -0,0 +1,5 @@
1
+ const GITHUB_STAR_MESSAGE = "\n\x1b[38;2;255;184;0m⭐️ → ❤️ https://github.com/Sunpeak-AI/filegrc\x1b[0m\n";
2
+
3
+ export function printGithubStarMessage() {
4
+ console.log(GITHUB_STAR_MESSAGE);
5
+ }
package/src/web.js CHANGED
@@ -174,7 +174,7 @@ function buildNavigation(route) {
174
174
  const organizationCurrent = route.name === "organization" || route.name === "repository" || ["workspace", "renderer-settings"].includes(route.type);
175
175
  const organizationName = state.workspace.organizationName || "Organization";
176
176
  const initial = organizationName.trim().charAt(0).toUpperCase() || "O";
177
- return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./favicon.png" alt="" width="39" height="39"><span><strong>filegrc</strong><small>SOC 2 workspace</small></span></a><nav class="sidebar-nav">' + stages + '</nav><div class="sidebar-footer"><a class="organization-nav ' + (organizationCurrent ? "current" : "") + '" href="#/organization"><span class="organization-mark">' + esc(initial) + '</span><span><strong>' + esc(organizationName) + '</strong><small>Organization</small></span><span class="organization-arrow">›</span></a></div></aside><button class="nav-scrim" type="button" aria-label="Close navigation"></button>';
177
+ return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./logo-mark-white.png" alt="" width="39" height="39"><span><strong>filegrc</strong><small>SOC 2 workspace</small></span></a><nav class="sidebar-nav">' + stages + '</nav><div class="sidebar-footer"><a class="organization-nav ' + (organizationCurrent ? "current" : "") + '" href="#/organization"><span class="organization-mark">' + esc(initial) + '</span><span><strong>' + esc(organizationName) + '</strong><small>Organization</small></span><span class="organization-arrow">›</span></a></div></aside><button class="nav-scrim" type="button" aria-label="Close navigation"></button>';
178
178
  }
179
179
 
180
180
  function readinessStageForRoute(route) {
@@ -1519,7 +1519,7 @@ function renderOnboardingStep() {
1519
1519
  const finalActions = onboardingStep === steps.length - 1
1520
1520
  ? '<button class="button" type="button" data-onboarding="draft">Save draft</button><button class="button primary" type="button" data-onboarding="next">Complete setup</button>'
1521
1521
  : '<button class="button primary" type="button" data-onboarding="next">Next</button>';
1522
- onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
1522
+ onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-scroll"><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
1523
1523
  onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", cancelOnboarding);
1524
1524
  onboardingDialog.querySelector('[data-onboarding="back"]')?.addEventListener("click", () => {
1525
1525
  captureOnboardingForm();
@@ -1569,7 +1569,7 @@ function onboardingSetupForm() {
1569
1569
  : state.git.available
1570
1570
  ? '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git remote needed</strong><small>Saving and local commits still work. Add a remote before the browser can push.</small></span></div>'
1571
1571
  : '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Saving still works. Run <code>git init</code> at the workspace root before your first compliance commit.</small></span></div>';
1572
- return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft keeps the system planned. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
1572
+ return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft marks the service Planned and In scope. It is selected for scope review, but it is not approved or active. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
1573
1573
  }
1574
1574
 
1575
1575
  function captureOnboardingForm() {
@@ -2598,12 +2598,12 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
2598
2598
  .readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
2599
2599
  .page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:9.6px;font-weight:700}
2600
2600
  .operation-tracking{display:grid;gap:2px;min-width:0;text-decoration:none}.operation-tracking strong,.operation-tracking small{display:block;overflow-wrap:anywhere}.operation-tracking small{color:var(--muted);line-height:1.35}.operation-tracking.running strong{color:#176143}.operation-tracking.waiting strong,.operation-tracking.mixed strong{color:var(--amber)}.operation-tracking.paused strong{color:var(--red)}a.operation-tracking:hover strong{text-decoration:underline}
2601
- .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:auto}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{padding:12px 25px 23px}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 0}.onboarding-dialog>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2601
+ .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;flex:0 0 auto;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-scroll{min-height:0;overflow-y:auto}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{flex:0 0 auto;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2602
2602
  .page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:10.8px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
2603
2603
  @media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
2604
2604
  @media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
2605
2605
  @media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{min-width:0}.topbar-status{display:none}.search{display:flex;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
2606
- @media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid{grid-template-columns:1fr}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}.onboarding-actions{position:sticky;bottom:0;background:var(--panel);border-top:1px solid var(--line)}}
2606
+ @media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid{grid-template-columns:1fr}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}}
2607
2607
  @media(max-width:520px){.onboarding-form,.onboarding-sections,.setup-steps{grid-template-columns:1fr}.onboarding-form label.wide{grid-column:auto}.onboarding-actions{flex-wrap:wrap}.onboarding-skip{width:100%;order:3;margin:3px 0 0}.readiness-flow{grid-template-columns:1fr}.obligation-card-foot{align-items:flex-start;flex-direction:column}.obligation-action{align-self:flex-start}}
2608
2608
  @media(min-width:761px){.detail-grid{grid-template-columns:minmax(270px,1fr) minmax(0,2fr)}.detail-grid aside{grid-column:1;grid-row:1}.detail-main{grid-column:2;grid-row:1}}
2609
2609
  @media(min-width:761px){.detail-grid.detail-grid-structured{grid-template-columns:1fr}.detail-grid-structured aside{grid-column:1;grid-row:1;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.detail-grid-structured aside>.panel{align-self:start}}