filegrc 0.3.4 → 0.5.0

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/src/model-docs.js CHANGED
@@ -15,7 +15,7 @@ export function generateModelDocumentation(model) {
15
15
  "",
16
16
  "## Program path",
17
17
  "",
18
- "The renderer, CLI, generated agent instructions, and this reference use the same six-step lifecycle:",
18
+ "The renderer, CLI, generated agent instructions, and this reference use the same five-step lifecycle:",
19
19
  "",
20
20
  ...programPath.flatMap((stage) => [
21
21
  `### Step ${stage.number}. ${stage.title}`,
@@ -35,6 +35,62 @@ export function generateModelDocumentation(model) {
35
35
  ...stage.commands.map((command) => `- \`${command}\``),
36
36
  ""
37
37
  ]),
38
+ "## Relation groups",
39
+ "",
40
+ "Relationship fields use the named groups below. The registry expands each group to explicit resource types, so no relationship accepts an unrestricted wildcard.",
41
+ "",
42
+ "| Group | Resource types |",
43
+ "| --- | --- |",
44
+ ...Object.entries(model.relationGroups || {}).map(([name, types]) => (
45
+ `| \`${name}\` | ${types.map((type) => `\`${type}\``).join(", ")} |`
46
+ )),
47
+ "",
48
+ "## Collection review confirmations",
49
+ "",
50
+ "FileGRC derives record issues, but it cannot infer that management reviewed an apparently complete or empty collection. Each configured collection review records the conclusion, reviewer, date, current scope revision, and exact collection revision. A record or material scope change makes the confirmation stale.",
51
+ "",
52
+ "| Resource type | Review | Allowed conclusions | What to review |",
53
+ "| --- | --- | --- | --- |",
54
+ ...Object.entries(model.collectionReviews || {}).map(([type, review]) => (
55
+ `| \`${type}\` | ${escapeCell(review.title)} | ${review.decisions.map((decision) => `\`${decision}\``).join(", ")} | ${escapeCell(review.reviewPoints.join(" "))} |`
56
+ )),
57
+ "",
58
+ "## Relationship constraints",
59
+ "",
60
+ "Relationship constraints prevent cycles and duplicate active authority or access records.",
61
+ "",
62
+ ...((model.relationshipConstraints?.acyclic || []).map((constraint) => (
63
+ `- \`${constraint.resourceType}.${constraint.field}\` must be acyclic.`
64
+ ))),
65
+ ...((model.relationshipConstraints?.unique || []).map((constraint) => (
66
+ `- \`${constraint.resourceType}\` records in ${constraint.statuses.map((status) => `\`${status}\``).join(", ")} must be unique by ${constraint.fields.map((field) => `\`${field}\``).join(", ")}.`
67
+ ))),
68
+ "",
69
+ "## Obligation activities",
70
+ "",
71
+ "The model owns each activity name, allowed recurrence modes and scope types, its default completion record, and every record type that can prove completion.",
72
+ "",
73
+ "| Activity | Title | Recurrence | Scope resource types | Default completion | Accepted completion records |",
74
+ "| --- | --- | --- | --- | --- | --- |",
75
+ ...Object.entries(model.obligationActivities || {}).map(([name, activity]) => (
76
+ `| \`${name}\` | ${escapeCell(activity.title)} | ${activity.recurrenceModes.map((mode) => `\`${mode}\``).join(", ")} | ${activity.scopeResourceTypes.map((type) => `\`${type}\``).join(", ")} | \`${activity.completionType}\` | ${activity.completionResourceTypes.map((type) => `\`${type}\``).join(", ")} |`
77
+ )),
78
+ "",
79
+ "## Policy events",
80
+ "",
81
+ "The model owns each event title and the minimum and maximum count for each subject resource type.",
82
+ "",
83
+ "| Event | Title | Subject rules |",
84
+ "| --- | --- | --- |",
85
+ ...Object.entries(model.policyEvents || {}).map(([name, event]) => (
86
+ `| \`${name}\` | ${escapeCell(event.title)} | ${event.subjectRules.map(({ resourceType, minimum = 0, maximum }) => `\`${resourceType}\` ${minimum}..${Number.isInteger(maximum) ? maximum : "*"}`).join(", ")} |`
87
+ )),
88
+ "",
89
+ "## Nested object schemas",
90
+ "",
91
+ "Named object schemas reject unknown keys unless the schema explicitly allows a typed map or arbitrary JSON. Conditional properties are valid only for the selected discriminator.",
92
+ "",
93
+ ...Object.entries(model.objectTypes || {}).flatMap(([name, schema]) => objectTypeDocumentation(name, schema)),
38
94
  "## Common fields",
39
95
  "",
40
96
  "| Field | Type | Required | Meaning |",
@@ -53,7 +109,7 @@ export function generateModelDocumentation(model) {
53
109
  "",
54
110
  "## Program and audit readiness defaults",
55
111
  "",
56
- "Program Readiness checks management scope, policy adoption, control implementation, authoritative source configuration, and verified test captures without requiring an audit record. Audit Readiness starts after a CPA firm is engaged and uses the defaults below to prepare Type 1 and Type 2 fieldwork.",
112
+ "Program Readiness checks management scope, policy adoption, control implementation, and authoritative evidence mapping without requiring an audit record. Audit Readiness starts after a CPA firm is engaged and uses the defaults below to prepare Type 1 and Type 2 fieldwork.",
57
113
  "",
58
114
  "Management documents:",
59
115
  "",
@@ -66,9 +122,9 @@ export function generateModelDocumentation(model) {
66
122
  "Authoritative systems of record:",
67
123
  "",
68
124
  ...model.evidenceSourceFamilies.map((item) => (
69
- item.collectionTestRequired === false
70
- ? `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} filegrc operating records: ${item.operationRecordTypes.map((type) => `\`${type}\``).join(", ")}. No separate collection test is required. ${item.timing}`
71
- : `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} Test external collection: ${item.testPrompt} ${item.timing}`
125
+ item.filegrcManaged === true
126
+ ? `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} FileGRC operating records: ${item.operationRecordTypes.map((type) => `\`${type}\``).join(", ")}. Expected evidence: ${item.evidencePrompt} ${item.timing}`
127
+ : `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} Expected evidence: ${item.evidencePrompt} ${item.timing}`
72
128
  )),
73
129
  "",
74
130
  "## Resource groups",
@@ -84,6 +140,11 @@ export function generateModelDocumentation(model) {
84
140
  lines.push(`Instructions: ${RESOURCE_INSTRUCTIONS[type] || resource.description}`, "");
85
141
  lines.push(`Policy basis: ${resource.guidance.policyBasis}`, "");
86
142
  lines.push(`Timing: ${resource.guidance.cadence}`, "");
143
+ if (resource.guidance.reviewPoints?.length) {
144
+ lines.push("When reviewing:", "");
145
+ for (const point of resource.guidance.reviewPoints) lines.push(`- ${point}`);
146
+ lines.push("");
147
+ }
87
148
  if (resource.guidance.sourceResourceIds?.length) {
88
149
  lines.push(`Default sources: ${resource.guidance.sourceResourceIds.map((id) => `\`${id}\``).join(", ")}`, "");
89
150
  }
@@ -143,7 +204,12 @@ function choiceLabel(name) {
143
204
  }
144
205
 
145
206
  function fieldType(field) {
146
- if (field.type === "array") return `array of ${field.items ?? "values"}`;
207
+ if (field.type === "array") {
208
+ return field.itemObjectType
209
+ ? `array of object (\`${field.itemObjectType}\`)`
210
+ : `array of ${field.items ?? "values"}`;
211
+ }
212
+ if (field.type === "object" && field.objectType) return `object (\`${field.objectType}\`)`;
147
213
  return field.format && field.format !== field.type ? `${field.type} (${field.format})` : field.type;
148
214
  }
149
215
 
@@ -151,14 +217,44 @@ function fieldNotes(field) {
151
217
  return [
152
218
  field.label,
153
219
  field.values ? `Values: ${field.values.map((item) => `\`${item}\``).join(", ")}` : "",
220
+ field.registry ? `Values come from the \`${field.registry}\` registry.` : "",
221
+ field.relationGroup ? `Relation group: \`${field.relationGroup}\`.` : "",
154
222
  field.relation ? `References: ${field.relation.map((item) => `\`${item}\``).join(", ")}` : "",
155
223
  field.minimum !== undefined ? `Minimum: \`${field.minimum}\`.` : "",
156
224
  field.maximum !== undefined ? `Maximum: \`${field.maximum}\`.` : "",
225
+ field.managed ? "Managed by filegrc." : "",
157
226
  field.disjointFrom ? `Must not overlap \`${field.disjointFrom}\`.` : "",
158
- field.requiredWhen ? `Required when ${Object.entries(field.requiredWhen).map(([key, value]) => `\`${key}\` is \`${value}\``).join(" and ")}` : ""
227
+ field.requiredWhen ? `Required when ${conditionText(field.requiredWhen)}.` : "",
228
+ field.allowedWhen ? `Allowed when ${conditionText(field.allowedWhen)}.` : ""
159
229
  ].filter(Boolean).join(" ");
160
230
  }
161
231
 
232
+ function objectTypeDocumentation(name, schema) {
233
+ const lines = [`### \`${name}\``, ""];
234
+ const properties = Object.entries(schema.properties || {});
235
+ if (properties.length) {
236
+ const required = new Set(schema.required || []);
237
+ lines.push("| Property | Type | Required | Notes |", "| --- | --- | --- | --- |");
238
+ for (const [propertyName, property] of properties) {
239
+ const requiredLabel = required.has(propertyName)
240
+ ? "Yes"
241
+ : property.requiredWhen
242
+ ? "Conditional"
243
+ : "No";
244
+ lines.push(`| \`${propertyName}\` | ${fieldType(property)} | ${requiredLabel} | ${escapeCell(fieldNotes(property))} |`);
245
+ }
246
+ } else if (schema.additionalProperties === true) {
247
+ lines.push("Allows arbitrary JSON properties.");
248
+ } else if (schema.additionalProperties) {
249
+ lines.push(`Allows dynamic keys whose values are ${fieldType(schema.additionalProperties)}.`);
250
+ } else {
251
+ lines.push("Does not allow properties.");
252
+ }
253
+ if (schema.keyFormat) lines.push("", `Key format: \`${schema.keyFormat}\`.`);
254
+ lines.push("");
255
+ return lines;
256
+ }
257
+
162
258
  function escapeCell(value) {
163
259
  return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
164
260
  }