filegrc 0.5.1 → 0.6.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/README.md +14 -6
- package/model/index.js +4 -4
- package/model/v4.json +10052 -0
- package/package.json +1 -1
- package/src/agent.js +17 -5
- package/src/audit-preparation.js +17 -13
- package/src/audit-transition.js +7 -4
- package/src/batch-review.js +31 -8
- package/src/cli.js +43 -25
- package/src/collection-review.js +71 -25
- package/src/evidence-packet.js +76 -43
- package/src/external-reviewer.js +4 -4
- package/src/files.js +70 -6
- package/src/git.js +70 -13
- package/src/index.js +1 -0
- package/src/model-migration.js +631 -14
- package/src/obligations.js +14 -7
- package/src/program-path.js +49 -38
- package/src/program-readiness.js +94 -54
- package/src/program.js +52 -0
- package/src/reconciliation.js +2 -2
- package/src/server.js +50 -8
- package/src/setup.js +56 -21
- package/src/source-coverage.js +13 -9
- package/src/state.js +17 -14
- package/src/timing.js +6 -0
- package/src/validate.js +100 -9
- package/src/web.js +690 -139
- package/src/workflow.js +47 -25
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createResourceId } from "./id.js";
|
|
|
2
2
|
import { markdownEntries } from "./resource-markdown.js";
|
|
3
3
|
import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
|
|
4
4
|
import { assessCollectionReview } from "./collection-review.js";
|
|
5
|
+
import { resolveProgram } from "./program.js";
|
|
5
6
|
|
|
6
7
|
const STARTING_STATUS_ORDER = [
|
|
7
8
|
"draft",
|
|
@@ -29,7 +30,7 @@ export function listResourceTypes(model) {
|
|
|
29
30
|
export function buildAgentGuide(loaded, type, options = {}) {
|
|
30
31
|
const definition = loaded.model.resources[type];
|
|
31
32
|
if (!definition) throw new Error(`Unknown resource type "${type}".`);
|
|
32
|
-
const collectionReview = assessCollectionReview(loaded, type);
|
|
33
|
+
const collectionReview = assessCollectionReview(loaded, type, { programId: options.programId });
|
|
33
34
|
const fields = { ...loaded.model.commonFields, ...definition.fields };
|
|
34
35
|
const required = new Set([
|
|
35
36
|
...Object.entries(loaded.model.commonFields)
|
|
@@ -115,12 +116,18 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
115
116
|
recommendedMarkdown.length
|
|
116
117
|
? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
|
|
117
118
|
: "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
|
|
119
|
+
...(type === "program" && String(loaded.model.modelVersion) === "4"
|
|
120
|
+
? ["Review Requirement applicability with npx filegrc review-applicability --type requirement --scaffold, then preview and apply the reviewed decisions as one validated batch."]
|
|
121
|
+
: []),
|
|
118
122
|
"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."
|
|
119
123
|
],
|
|
120
124
|
completionChecks: [
|
|
121
125
|
"Required and status-dependent fields are complete, and the lifecycle status matches the facts.",
|
|
122
126
|
"Every relationship resolves to the intended existing record.",
|
|
123
127
|
"Dates describe the business event in the workspace time zone, not the file edit time.",
|
|
128
|
+
...(type === "program" && String(loaded.model.modelVersion) === "4"
|
|
129
|
+
? ["Every selected Requirement has an applicable or not-applicable decision reviewed against the current Program scope."]
|
|
130
|
+
: []),
|
|
124
131
|
...(recommendedMarkdown.length
|
|
125
132
|
? ["Required or recommended Markdown explains the work, decisions, results, exceptions, and follow-up that apply."]
|
|
126
133
|
: ["The structured fields state the current fact clearly; optional Record Markdown is added only when needed."]),
|
|
@@ -172,7 +179,7 @@ export function scaffoldResourceMutation(loaded, type, title, options = {}) {
|
|
|
172
179
|
record[name] = scaffoldValue(name, field, loaded.model);
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
|
-
applyModelScaffoldDefaults(record, loaded);
|
|
182
|
+
applyModelScaffoldDefaults(record, loaded, options);
|
|
176
183
|
|
|
177
184
|
const slots = markdownEntries(loaded.model, record).filter((slot) => (
|
|
178
185
|
slot.required
|
|
@@ -189,7 +196,7 @@ export function scaffoldResourceMutation(loaded, type, title, options = {}) {
|
|
|
189
196
|
};
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
function applyModelScaffoldDefaults(record, loaded) {
|
|
199
|
+
function applyModelScaffoldDefaults(record, loaded, options = {}) {
|
|
193
200
|
if (record.type === "appointment") {
|
|
194
201
|
const normalizedTitle = record.title.toLowerCase();
|
|
195
202
|
const match = Object.entries(loaded.model.appointmentTemplates || {}).find(([kind, template]) => (
|
|
@@ -203,14 +210,18 @@ function applyModelScaffoldDefaults(record, loaded) {
|
|
|
203
210
|
return;
|
|
204
211
|
}
|
|
205
212
|
if (record.type === "audit") {
|
|
213
|
+
const program = resolveProgram(loaded, options.programId);
|
|
206
214
|
const kind = {
|
|
207
215
|
"soc-2-type-1": "soc-2-type-1",
|
|
208
216
|
"soc-2-type-2": "soc-2-type-2"
|
|
209
|
-
}[
|
|
217
|
+
}[program?.assuranceGoal];
|
|
210
218
|
if (kind) record.auditKind = kind;
|
|
211
219
|
for (const field of ["frameworkIds", "systemIds", "requirementIds", "controlIds"]) {
|
|
212
|
-
if (loaded.
|
|
220
|
+
if (field === "requirementIds" && String(loaded.model.modelVersion) === "4") {
|
|
221
|
+
record[field] = (program.requirementApplicability || []).filter(({ decision }) => decision === "applicable").map(({ requirementId }) => requirementId);
|
|
222
|
+
} else if (program?.[field]?.length) record[field] = [...program[field]];
|
|
213
223
|
}
|
|
224
|
+
if (program.type === "program") record.programId = program.id;
|
|
214
225
|
const programOwner = loaded.resources.find((candidate) => (
|
|
215
226
|
candidate.type === "appointment"
|
|
216
227
|
&& candidate.appointmentKind === "program-lead"
|
|
@@ -280,6 +291,7 @@ function allowedValues(model, field) {
|
|
|
280
291
|
|
|
281
292
|
function scaffoldValue(name, field = {}, model) {
|
|
282
293
|
if (field.const !== undefined) return field.const;
|
|
294
|
+
if (field.default !== undefined) return structuredClone(field.default);
|
|
283
295
|
if (name === "status" && field.values) {
|
|
284
296
|
return STARTING_STATUS_ORDER.find((value) => field.values.includes(value)) ?? field.values[0] ?? null;
|
|
285
297
|
}
|
package/src/audit-preparation.js
CHANGED
|
@@ -55,7 +55,8 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
55
55
|
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
56
56
|
|
|
57
57
|
const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
|
|
58
|
-
generatedAt: options.generatedAt
|
|
58
|
+
generatedAt: options.generatedAt,
|
|
59
|
+
programId: audit?.programId
|
|
59
60
|
});
|
|
60
61
|
const stages = [
|
|
61
62
|
programFoundationStage(programReadiness, loaded.workspace),
|
|
@@ -157,7 +158,8 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
157
158
|
const selectedControls = (audit.controlIds || [])
|
|
158
159
|
.map((id) => loaded.resources.find((record) => record.id === id))
|
|
159
160
|
.filter(Boolean);
|
|
160
|
-
const
|
|
161
|
+
const v4 = String(loaded.model.modelVersion) === "4";
|
|
162
|
+
const sourceSystems = loaded.resources.filter((record) => record.type === (v4 ? "component" : "system"));
|
|
161
163
|
const populations = (audit.auditKind === "soc-2-type-2" ? model.populationTemplates || [] : [])
|
|
162
164
|
.filter((template) => !existingKinds.has(template.kind))
|
|
163
165
|
.map((template) => {
|
|
@@ -183,7 +185,7 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
183
185
|
coverage: structuredClone(audit.coverage),
|
|
184
186
|
ownerIds: [...audit.ownerIds],
|
|
185
187
|
...(controlIds.length ? { controlIds } : {}),
|
|
186
|
-
...(matchingSources.length === 1 ? { sourceSystemId: matchingSources[0].id } : {}),
|
|
188
|
+
...(matchingSources.length === 1 ? { [v4 ? "sourceComponentId" : "sourceSystemId"]: matchingSources[0].id } : {}),
|
|
187
189
|
reconciliationSummary: `Authoritative source to confirm: ${template.sourcePrompt}. ${template.timing || ""}`.trim()
|
|
188
190
|
};
|
|
189
191
|
});
|
|
@@ -608,14 +610,16 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
608
610
|
item(
|
|
609
611
|
"external-evidence",
|
|
610
612
|
externalControls.length && controlsWithExternalEvidence.length === externalControls.length ? "complete" : externalControls.length ? "action" : "info",
|
|
611
|
-
"Review
|
|
613
|
+
"Review Evidence Artifacts",
|
|
612
614
|
externalControls.length
|
|
613
|
-
? `${controlsWithExternalEvidence.length} of ${externalControls.length} selected controls that rely on external
|
|
614
|
-
: "No selected controls require a separate
|
|
615
|
+
? `${controlsWithExternalEvidence.length} of ${externalControls.length} selected controls that rely on external Components have verified Evidence Artifacts for the formal period. Confirm the source Component, date or period, control links, collector, verifier, and retained artifact or approved external reference.`
|
|
616
|
+
: "No selected controls require a separate Evidence Artifact.",
|
|
615
617
|
externalEvidence[0] || { type: "evidence" }
|
|
616
618
|
)
|
|
617
619
|
];
|
|
618
|
-
const
|
|
620
|
+
const v4 = String(model.modelVersion) === "4";
|
|
621
|
+
const systems = records.filter((record) => record.type === (v4 ? "component" : "system") && record.status === "active");
|
|
622
|
+
const sourceId = (record) => v4 ? record.sourceComponentId : record.sourceSystemId;
|
|
619
623
|
for (const source of model.evidenceSourceFamilies || []) {
|
|
620
624
|
const relevantControls = controls.filter((control) => (source.controlCodes || []).includes(control.code));
|
|
621
625
|
if (!relevantControls.length) {
|
|
@@ -652,7 +656,7 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
652
656
|
(system.evidenceSourceKinds || []).some((kind) => (source.sourceKinds || []).includes(kind))
|
|
653
657
|
));
|
|
654
658
|
const coveredControls = relevantControls.filter((control) => externalEvidence.some((record) => (
|
|
655
|
-
sourceSystems.some((system) => system.id === record
|
|
659
|
+
sourceSystems.some((system) => system.id === sourceId(record))
|
|
656
660
|
&& controlIdsForRecord(record, byId).has(control.id)
|
|
657
661
|
)));
|
|
658
662
|
const status = sourceSystems.length && coveredControls.length === relevantControls.length ? "complete" : "action";
|
|
@@ -666,12 +670,12 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
666
670
|
status,
|
|
667
671
|
source.title,
|
|
668
672
|
message,
|
|
669
|
-
externalEvidence.find((record) => sourceSystems.some((system) => system.id === record
|
|
673
|
+
externalEvidence.find((record) => sourceSystems.some((system) => system.id === sourceId(record)))
|
|
670
674
|
|| sourceSystems[0]
|
|
671
|
-
|| { type: "system" }
|
|
675
|
+
|| { type: v4 ? "component" : "system" }
|
|
672
676
|
));
|
|
673
677
|
}
|
|
674
|
-
return stage("evidence", "Audit Evidence",
|
|
678
|
+
return stage("evidence", "Audit Evidence", `Review both evidence paths: dated filegrc operating records and verified ${v4 ? "Evidence Artifacts from authoritative Components" : "External Evidence from authoritative systems"}. filegrc includes both in the audit packet.`, items);
|
|
675
679
|
}
|
|
676
680
|
|
|
677
681
|
function populationsStage(audit, records, byId, model) {
|
|
@@ -748,8 +752,8 @@ function populationResult(population, audit, byId) {
|
|
|
748
752
|
&& evidence.type === "evidence"
|
|
749
753
|
&& evidence.artifactKind === "population-export"
|
|
750
754
|
&& evidence.status === "verified"
|
|
751
|
-
&& population.sourceSystemId
|
|
752
|
-
&& evidence.sourceSystemId === population.sourceSystemId
|
|
755
|
+
&& (population.sourceComponentId || population.sourceSystemId)
|
|
756
|
+
&& (evidence.sourceComponentId || evidence.sourceSystemId) === (population.sourceComponentId || population.sourceSystemId)
|
|
753
757
|
&& coverageMatches(
|
|
754
758
|
evidence.coverage,
|
|
755
759
|
coverageStart(audit.coverage),
|
package/src/audit-transition.js
CHANGED
|
@@ -4,8 +4,8 @@ import { loadWorkspace } from "./workspace.js";
|
|
|
4
4
|
|
|
5
5
|
export async function planNextAuditCycle(input = process.cwd(), options = {}) {
|
|
6
6
|
const loaded = await loadWorkspace(input);
|
|
7
|
-
if (String(loaded.model.modelVersion)
|
|
8
|
-
throw new Error("Audit-cycle carry-forward requires a model v3 workspace.");
|
|
7
|
+
if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
|
|
8
|
+
throw new Error("Audit-cycle carry-forward requires a model v3 or v4 workspace.");
|
|
9
9
|
}
|
|
10
10
|
const prior = loaded.resources.find((record) => (
|
|
11
11
|
record.type === "audit" && record.id === options.priorAuditId
|
|
@@ -32,8 +32,11 @@ export async function planNextAuditCycle(input = process.cwd(), options = {}) {
|
|
|
32
32
|
requirementIds: [...(prior.requirementIds || [])],
|
|
33
33
|
controlIds: [...(prior.controlIds || [])],
|
|
34
34
|
complementaryControlIds: [...(prior.complementaryControlIds || [])],
|
|
35
|
-
|
|
36
|
-
...(prior.
|
|
35
|
+
...(prior.programId ? { programId: prior.programId } : {}),
|
|
36
|
+
...(prior.subserviceTreatments ? { subserviceTreatments: structuredClone(prior.subserviceTreatments) } : {
|
|
37
|
+
subserviceVendorIds: [...(prior.subserviceVendorIds || [])],
|
|
38
|
+
...(prior.subserviceMethod ? { subserviceMethod: prior.subserviceMethod } : {})
|
|
39
|
+
}),
|
|
37
40
|
...(prior.complementaryControlsConclusion
|
|
38
41
|
? { complementaryControlsConclusion: prior.complementaryControlsConclusion }
|
|
39
42
|
: {}),
|
package/src/batch-review.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { applyResourceBatch } from "./files.js";
|
|
2
2
|
import { getGitSummary } from "./git.js";
|
|
3
3
|
import { loadWorkspace } from "./workspace.js";
|
|
4
|
+
import { resolveProgram } from "./program.js";
|
|
4
5
|
|
|
5
6
|
const REVIEWABLE_TYPES = new Set([
|
|
6
7
|
"requirement",
|
|
@@ -11,17 +12,23 @@ const REVIEWABLE_TYPES = new Set([
|
|
|
11
12
|
|
|
12
13
|
export async function scaffoldApplicabilityReview(input = process.cwd(), options = {}) {
|
|
13
14
|
const loaded = await loadWorkspace(input);
|
|
14
|
-
if (String(loaded.model.modelVersion)
|
|
15
|
-
throw new Error("Batch applicability review requires a model v3 workspace.");
|
|
15
|
+
if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
|
|
16
|
+
throw new Error("Batch applicability review requires a model v3 or v4 workspace.");
|
|
16
17
|
}
|
|
17
18
|
const requestedType = options.type ? String(options.type) : null;
|
|
18
19
|
if (requestedType && !REVIEWABLE_TYPES.has(requestedType)) {
|
|
19
20
|
throw new Error(`Applicability review type must be one of ${[...REVIEWABLE_TYPES].join(", ")}.`);
|
|
20
21
|
}
|
|
22
|
+
const program = resolveProgram(loaded, options.programId);
|
|
23
|
+
const reviewedRequirementIds = new Set((program.requirementApplicability || [])
|
|
24
|
+
.filter(({ decision }) => ["applicable", "not-applicable"].includes(decision))
|
|
25
|
+
.map(({ requirementId }) => requirementId));
|
|
21
26
|
const records = loaded.resources.filter((record) => (
|
|
22
27
|
REVIEWABLE_TYPES.has(record.type)
|
|
23
28
|
&& (!requestedType || record.type === requestedType)
|
|
24
|
-
&&
|
|
29
|
+
&& (record.type === "requirement" && String(loaded.model.modelVersion) === "4"
|
|
30
|
+
? !reviewedRequirementIds.has(record.id)
|
|
31
|
+
: !record.applicabilityReview)
|
|
25
32
|
&& !["retired", "superseded"].includes(record.status)
|
|
26
33
|
));
|
|
27
34
|
return {
|
|
@@ -39,14 +46,16 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
|
|
|
39
46
|
|
|
40
47
|
export async function planApplicabilityReview(input = process.cwd(), options = {}) {
|
|
41
48
|
const loaded = await loadWorkspace(input);
|
|
42
|
-
if (String(loaded.model.modelVersion)
|
|
43
|
-
throw new Error("Batch applicability review requires a model v3 workspace.");
|
|
49
|
+
if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
|
|
50
|
+
throw new Error("Batch applicability review requires a model v3 or v4 workspace.");
|
|
44
51
|
}
|
|
45
52
|
if (!Array.isArray(options.decisions) || !options.decisions.length) {
|
|
46
53
|
throw new Error("Applicability review needs at least one decision.");
|
|
47
54
|
}
|
|
48
55
|
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
49
|
-
const
|
|
56
|
+
const program = resolveProgram(loaded, options.programId);
|
|
57
|
+
const v4RequirementDecisions = [];
|
|
58
|
+
const update = options.decisions.flatMap((decision) => {
|
|
50
59
|
const record = byId.get(decision.id);
|
|
51
60
|
if (!record || !REVIEWABLE_TYPES.has(record.type)) {
|
|
52
61
|
throw new Error(`Resource "${decision.id}" is not an applicability-review record.`);
|
|
@@ -81,16 +90,30 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
|
|
|
81
90
|
if (!["applicable", "not-applicable"].includes(result)) {
|
|
82
91
|
throw new Error(`Requirement "${record.id}" must be applicable or not-applicable.`);
|
|
83
92
|
}
|
|
93
|
+
if (String(loaded.model.modelVersion) === "4") {
|
|
94
|
+
v4RequirementDecisions.push({ requirementId: record.id, decision: result, rationale, reviewedByIds, reviewedOn, scopeRevision });
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
84
97
|
next.applicability = result;
|
|
85
98
|
next.applicabilityRationale = rationale;
|
|
86
99
|
}
|
|
87
100
|
if (record.type === "control" && result === "not-applicable") next.status = "not-applicable";
|
|
88
101
|
if (record.type === "control" && result === "applicable" && record.status === "not-applicable") next.status = "planned";
|
|
89
|
-
return next;
|
|
102
|
+
return [next];
|
|
90
103
|
});
|
|
104
|
+
if (v4RequirementDecisions.length) {
|
|
105
|
+
const replaced = new Set(v4RequirementDecisions.map(({ requirementId }) => requirementId));
|
|
106
|
+
update.push({
|
|
107
|
+
...program,
|
|
108
|
+
requirementApplicability: [
|
|
109
|
+
...(program.requirementApplicability || []).filter(({ requirementId }) => !replaced.has(requirementId)),
|
|
110
|
+
...v4RequirementDecisions
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
91
114
|
return {
|
|
92
115
|
operation: "applicability-review",
|
|
93
|
-
reviewedIds:
|
|
116
|
+
reviewedIds: options.decisions.map(({ id }) => id),
|
|
94
117
|
changes: {
|
|
95
118
|
update,
|
|
96
119
|
expectedRevisions: options.expectedRevisions || {},
|
package/src/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import { relativeToWorkspace, resolveDataPath } from "./paths.js";
|
|
44
44
|
import { buildAgentProgramPath } from "./program-path.js";
|
|
45
45
|
import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
46
|
+
import { resolveProgram } from "./program.js";
|
|
46
47
|
import { applyReconciliation, planReconciliation } from "./reconciliation.js";
|
|
47
48
|
import { markdownEntries } from "./resource-markdown.js";
|
|
48
49
|
import { effectiveResourceStatus } from "./resource-status.js";
|
|
@@ -130,7 +131,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
130
131
|
const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
|
|
131
132
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
132
133
|
else if (flags.preview) {
|
|
133
|
-
console.log(`Setup preview: ${result.changes.system}
|
|
134
|
+
console.log(`Setup preview: ${result.changes.system} System ${result.system.id}; update the assurance target to ${result.target.assuranceGoal}.`);
|
|
134
135
|
console.log("No controls will be linked and no evidence records will be created.");
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
@@ -139,8 +140,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
139
140
|
if (result.draft) {
|
|
140
141
|
console.log("Planned and in scope means selected for scope review, not approved or active.");
|
|
141
142
|
}
|
|
142
|
-
console.log(`Target: ${result.workspace.assuranceGoal}`);
|
|
143
|
-
console.log("Next: finish Step 1 by confirming people, criteria, commitments,
|
|
143
|
+
console.log(`Target: ${(result.program || result.workspace).assuranceGoal}`);
|
|
144
|
+
console.log("Next: finish Step 1 by confirming people, criteria, commitments, bounded Systems, Components, and Vendors. Run npx filegrc program-path --next --json.");
|
|
144
145
|
}
|
|
145
146
|
return output;
|
|
146
147
|
}
|
|
@@ -176,11 +177,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
176
177
|
}
|
|
177
178
|
if (command === "migrate") {
|
|
178
179
|
const targetModel = String(flags["to-model"] || "");
|
|
179
|
-
if (!["2", "3"].includes(targetModel)) throw new Error("Pass --to-model 2 or --to-model
|
|
180
|
+
if (!["2", "3", "4"].includes(targetModel)) throw new Error("Pass --to-model 2, --to-model 3, or --to-model 4.");
|
|
181
|
+
const systemDecisions = flags.decisions
|
|
182
|
+
? JSON.parse(await readFile(resolve(String(flags.decisions)), "utf8"))
|
|
183
|
+
: undefined;
|
|
180
184
|
const options = {
|
|
181
185
|
jobTitle: flags["job-title"],
|
|
182
186
|
startsOn: flags["starts-on"],
|
|
183
|
-
targetModelVersion: targetModel
|
|
187
|
+
targetModelVersion: targetModel,
|
|
188
|
+
systemDecisions: systemDecisions?.systemDecisions || systemDecisions
|
|
184
189
|
};
|
|
185
190
|
const plan = await planModelMigration(root, options);
|
|
186
191
|
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
@@ -228,15 +233,17 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
228
233
|
else printAgentOverview(result);
|
|
229
234
|
return result;
|
|
230
235
|
}
|
|
231
|
-
const result = buildAgentGuide(loaded, type, { id: flags.id });
|
|
236
|
+
const result = buildAgentGuide(loaded, type, { id: flags.id, programId: flags.program });
|
|
232
237
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
233
238
|
else printAgentGuide(result);
|
|
234
239
|
return result;
|
|
235
240
|
}
|
|
236
241
|
if (command === "program-path") {
|
|
237
242
|
const loaded = await loadWorkspace(root);
|
|
238
|
-
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
239
243
|
const auditId = positionals[0] || flags.audit;
|
|
244
|
+
const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
|
|
245
|
+
const programId = flags.program || audit?.programId;
|
|
246
|
+
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
|
|
240
247
|
const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
|
|
241
248
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
242
249
|
const output = selectProgramPathOutput(result, flags);
|
|
@@ -247,6 +254,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
247
254
|
if (command === "workflow") {
|
|
248
255
|
const result = await assessWorkflow(root, {
|
|
249
256
|
auditId: positionals[0] || flags.audit,
|
|
257
|
+
programId: flags.program,
|
|
250
258
|
asOf: flags["as-of"],
|
|
251
259
|
through: flags.through,
|
|
252
260
|
includeComplete: Boolean(flags.complete)
|
|
@@ -264,6 +272,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
264
272
|
: undefined;
|
|
265
273
|
const result = await assessWorkflow(root, {
|
|
266
274
|
auditId: positionals[0] || flags.audit,
|
|
275
|
+
programId: flags.program,
|
|
267
276
|
asOf: flags["as-of"],
|
|
268
277
|
through: flags.end || flags.through,
|
|
269
278
|
coverage
|
|
@@ -292,10 +301,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
292
301
|
}
|
|
293
302
|
if (command === "milestone-check") {
|
|
294
303
|
const loaded = await loadWorkspace(root);
|
|
295
|
-
const result = await assessWorkflow(loaded, { asOf: flags["as-of"] });
|
|
296
|
-
const
|
|
304
|
+
const result = await assessWorkflow(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
305
|
+
const program = resolveProgram(loaded, flags.program);
|
|
306
|
+
const target = program?.assuranceGoal === "none"
|
|
297
307
|
? "structuralValidity"
|
|
298
|
-
:
|
|
308
|
+
: program?.candidateCoverage
|
|
299
309
|
? "periodHealth"
|
|
300
310
|
: "evidenceReadiness";
|
|
301
311
|
const output = {
|
|
@@ -312,7 +322,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
312
322
|
if (command === "scaffold") {
|
|
313
323
|
const loaded = await loadWorkspace(root);
|
|
314
324
|
const type = positionals[0];
|
|
315
|
-
const result = scaffoldResourceMutation(loaded, type, flags.title, { id: flags.id });
|
|
325
|
+
const result = scaffoldResourceMutation(loaded, type, flags.title, { id: flags.id, programId: flags.program });
|
|
316
326
|
console.log(JSON.stringify(result, null, 2));
|
|
317
327
|
return result;
|
|
318
328
|
}
|
|
@@ -333,7 +343,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
333
343
|
const output = flags.workflow
|
|
334
344
|
? {
|
|
335
345
|
records,
|
|
336
|
-
workflow: await assessWorkflow(loaded, { asOf })
|
|
346
|
+
workflow: await assessWorkflow(loaded, { asOf, programId: flags.program })
|
|
337
347
|
}
|
|
338
348
|
: records;
|
|
339
349
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
@@ -394,7 +404,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
394
404
|
}
|
|
395
405
|
if (command === "program-readiness") {
|
|
396
406
|
const loaded = await loadWorkspace(root);
|
|
397
|
-
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
407
|
+
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
398
408
|
const output = flags.summary ? summarizeProgramReadiness(result) : result;
|
|
399
409
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
400
410
|
else if (flags.summary) {
|
|
@@ -427,7 +437,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
427
437
|
}
|
|
428
438
|
if (command === "evidence-map") {
|
|
429
439
|
const loaded = await loadWorkspace(root);
|
|
430
|
-
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"] });
|
|
440
|
+
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
431
441
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
432
442
|
else {
|
|
433
443
|
console.log(`${result.status.toUpperCase()}: ${result.counts.complete} mapped, ${result.counts.action} need action`);
|
|
@@ -435,11 +445,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
435
445
|
console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
436
446
|
if (item.status !== "action") continue;
|
|
437
447
|
if (item.sourceKinds?.length) console.log(` Source role: ${item.sourceKinds.join(" or ")}`);
|
|
438
|
-
for (const source of item.sourceSystemChecks || []) {
|
|
448
|
+
for (const source of item.sourceComponentChecks || item.sourceSystemChecks || []) {
|
|
439
449
|
const missing = Object.entries(source.checks)
|
|
440
450
|
.filter(([, passed]) => !passed)
|
|
441
451
|
.map(([name]) => evidenceSourceCheckName(name));
|
|
442
|
-
if (missing.length) console.log(` ${source.sourceSystemId}: ${missing.join(", ")}`);
|
|
452
|
+
if (missing.length) console.log(` ${source.sourceComponentId || source.sourceSystemId}: ${missing.join(", ")}`);
|
|
443
453
|
}
|
|
444
454
|
if (item.commands?.length) console.log(` Next: ${item.commands[0]}`);
|
|
445
455
|
}
|
|
@@ -989,15 +999,15 @@ Usage:
|
|
|
989
999
|
filegrc build [root] [--output .filegrc/site]
|
|
990
1000
|
filegrc validate [root] [--json]
|
|
991
1001
|
filegrc model [--json|--write-docs|--check-docs]
|
|
992
|
-
filegrc migrate --to-model <2|3> [--preview] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
1002
|
+
filegrc migrate --to-model <2|3|4> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
993
1003
|
filegrc describe <resource-type>
|
|
994
1004
|
filegrc types [--json]
|
|
995
|
-
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
1005
|
+
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
996
1006
|
filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--summary|--next|--current] [--json]
|
|
997
1007
|
filegrc workflow [audit-id] [--as-of YYYY-MM-DD] [--through YYYY-MM-DD] [--complete] [--require-ready] [--json]
|
|
998
1008
|
filegrc period-health [audit-id] [--start YYYY-MM-DD --end YYYY-MM-DD] [--as-of YYYY-MM-DD] [--require-healthy] [--json]
|
|
999
1009
|
filegrc milestone-check [--as-of YYYY-MM-DD] [--json]
|
|
1000
|
-
filegrc scaffold <resource-type> --title text [--id resource-id]
|
|
1010
|
+
filegrc scaffold <resource-type> --title text [--id resource-id] [--program program-id]
|
|
1001
1011
|
filegrc list [resource-type] [--workflow] [--json]
|
|
1002
1012
|
filegrc search <query> [--type resource-type] [--json]
|
|
1003
1013
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
@@ -1076,15 +1086,19 @@ Options:
|
|
|
1076
1086
|
}
|
|
1077
1087
|
if (command === "migrate") {
|
|
1078
1088
|
console.log(`Usage:
|
|
1079
|
-
filegrc migrate --to-model <2|3> [options]
|
|
1089
|
+
filegrc migrate --to-model <2|3|4> [options]
|
|
1080
1090
|
|
|
1081
1091
|
Upgrade a workspace through an explicit, reviewable model boundary. Model v1
|
|
1082
|
-
workspaces migrate to v2 first. Model v2 workspaces migrate to v3
|
|
1092
|
+
workspaces migrate to v2 first. Model v2 workspaces migrate to v3, then v4. v4 separates
|
|
1093
|
+
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1094
|
+
specific Assets, Vendors, normalized information, and Evidence Artifacts. v3 migration
|
|
1095
|
+
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1083
1096
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1084
1097
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
1085
1098
|
|
|
1086
1099
|
Options:
|
|
1087
|
-
--to-model <version> Required target model;
|
|
1100
|
+
--to-model <version> Required target model; migrations must run in order through 2, 3, and 4
|
|
1101
|
+
--decisions <path> v4 JSON object keyed by old System ID with system/component decisions
|
|
1088
1102
|
--preview Show the complete atomic record plan without writing
|
|
1089
1103
|
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
1090
1104
|
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
@@ -1094,7 +1108,7 @@ Options:
|
|
|
1094
1108
|
--help Show this help
|
|
1095
1109
|
|
|
1096
1110
|
Start with:
|
|
1097
|
-
npx filegrc migrate --to-model
|
|
1111
|
+
npx filegrc migrate --to-model 4 --preview --json`);
|
|
1098
1112
|
return;
|
|
1099
1113
|
}
|
|
1100
1114
|
if (command === "program-readiness") {
|
|
@@ -1106,6 +1120,7 @@ controls, and mapped every selected control to a configured authoritative eviden
|
|
|
1106
1120
|
source. No audit ID or CPA firm is required.
|
|
1107
1121
|
|
|
1108
1122
|
Options:
|
|
1123
|
+
--program <id> Program to assess when more than one active Program exists
|
|
1109
1124
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
1110
1125
|
--require-ready Exit with code 2 unless the Evidence Ready gate passes
|
|
1111
1126
|
--summary Omit item details and print stage counts and next actions
|
|
@@ -1124,6 +1139,7 @@ deterministic Work Items, and one recommended next action.
|
|
|
1124
1139
|
|
|
1125
1140
|
Options:
|
|
1126
1141
|
--audit <id> Limit audit assessments to one engagement
|
|
1142
|
+
--program <id> Program to assess when no Audit supplies the Program
|
|
1127
1143
|
--as-of <date> Evaluate on YYYY-MM-DD
|
|
1128
1144
|
--through <date> Include scheduled work through YYYY-MM-DD
|
|
1129
1145
|
--complete Include completed Work Items
|
|
@@ -1143,6 +1159,7 @@ and current readiness state. Pass an audit ID to include Step 5 status.
|
|
|
1143
1159
|
|
|
1144
1160
|
Options:
|
|
1145
1161
|
--audit <id> Audit record to use for Step 5
|
|
1162
|
+
--program <id> Program to assess when no Audit supplies the Program
|
|
1146
1163
|
--as-of <date> Evaluate readiness on YYYY-MM-DD
|
|
1147
1164
|
--summary Print compact status and the first action for all five steps
|
|
1148
1165
|
--next Print only the current step and its first action
|
|
@@ -1157,10 +1174,11 @@ Options:
|
|
|
1157
1174
|
filegrc evidence-map [options]
|
|
1158
1175
|
|
|
1159
1176
|
Inspect the evidence-source checks included in Control implementation. Each item
|
|
1160
|
-
reports the required source roles, linked Controls, authoritative source
|
|
1177
|
+
reports the required source roles, linked Controls, authoritative source Components,
|
|
1161
1178
|
per-record checks, and exact edit commands. This diagnostic is read-only.
|
|
1162
1179
|
|
|
1163
1180
|
Options:
|
|
1181
|
+
--program <id> Program to assess when more than one active Program exists
|
|
1164
1182
|
--as-of <date> Evaluate the map on YYYY-MM-DD
|
|
1165
1183
|
--json Print the map as JSON
|
|
1166
1184
|
--root <path> Workspace path
|
|
@@ -1179,7 +1197,7 @@ function agentOverview(model) {
|
|
|
1179
1197
|
build: "filegrc build [root]",
|
|
1180
1198
|
validate: "filegrc validate [root] --json",
|
|
1181
1199
|
model: "filegrc model --json",
|
|
1182
|
-
migrate: "filegrc migrate --to-model
|
|
1200
|
+
migrate: "filegrc migrate --to-model 4 --preview --json",
|
|
1183
1201
|
describe: "filegrc describe <resource-type>",
|
|
1184
1202
|
types: "filegrc types --json",
|
|
1185
1203
|
guide: "filegrc guide [resource-type] --json",
|