filegrc 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- 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 +49 -26
- 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";
|
|
@@ -95,6 +96,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
95
96
|
const result = await serveWorkspace(positionals[0] ?? root, {
|
|
96
97
|
host: flags.host ?? process.env.FILEGRC_HOST,
|
|
97
98
|
port: flags.port ?? process.env.FILEGRC_PORT,
|
|
99
|
+
fallbackToAvailablePort: true,
|
|
98
100
|
allowNonAuthoritativeWrites: flags["allow-non-authoritative-writes"] === true
|
|
99
101
|
});
|
|
100
102
|
const stopped = new Promise((resolvePromise) => {
|
|
@@ -106,6 +108,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
106
108
|
process.once("SIGINT", stop);
|
|
107
109
|
process.once("SIGTERM", stop);
|
|
108
110
|
});
|
|
111
|
+
if (result.usedFallbackPort) {
|
|
112
|
+
console.log(`Port ${result.requestedPort} is already in use. Using ${result.address.port} instead.`);
|
|
113
|
+
}
|
|
109
114
|
console.log(`filegrc workspace: ${result.url}`);
|
|
110
115
|
console.log(`Data: ${result.root}/data`);
|
|
111
116
|
printGithubStarMessage();
|
|
@@ -130,7 +135,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
130
135
|
const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
|
|
131
136
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
132
137
|
else if (flags.preview) {
|
|
133
|
-
console.log(`Setup preview: ${result.changes.system}
|
|
138
|
+
console.log(`Setup preview: ${result.changes.system} System ${result.system.id}; update the assurance target to ${result.target.assuranceGoal}.`);
|
|
134
139
|
console.log("No controls will be linked and no evidence records will be created.");
|
|
135
140
|
}
|
|
136
141
|
else {
|
|
@@ -139,8 +144,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
139
144
|
if (result.draft) {
|
|
140
145
|
console.log("Planned and in scope means selected for scope review, not approved or active.");
|
|
141
146
|
}
|
|
142
|
-
console.log(`Target: ${result.workspace.assuranceGoal}`);
|
|
143
|
-
console.log("Next: finish Step 1 by confirming people, criteria, commitments,
|
|
147
|
+
console.log(`Target: ${(result.program || result.workspace).assuranceGoal}`);
|
|
148
|
+
console.log("Next: finish Step 1 by confirming people, criteria, commitments, bounded Systems, Components, and Vendors. Run npx filegrc program-path --next --json.");
|
|
144
149
|
}
|
|
145
150
|
return output;
|
|
146
151
|
}
|
|
@@ -176,11 +181,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
176
181
|
}
|
|
177
182
|
if (command === "migrate") {
|
|
178
183
|
const targetModel = String(flags["to-model"] || "");
|
|
179
|
-
if (!["2", "3"].includes(targetModel)) throw new Error("Pass --to-model 2 or --to-model
|
|
184
|
+
if (!["2", "3", "4"].includes(targetModel)) throw new Error("Pass --to-model 2, --to-model 3, or --to-model 4.");
|
|
185
|
+
const systemDecisions = flags.decisions
|
|
186
|
+
? JSON.parse(await readFile(resolve(String(flags.decisions)), "utf8"))
|
|
187
|
+
: undefined;
|
|
180
188
|
const options = {
|
|
181
189
|
jobTitle: flags["job-title"],
|
|
182
190
|
startsOn: flags["starts-on"],
|
|
183
|
-
targetModelVersion: targetModel
|
|
191
|
+
targetModelVersion: targetModel,
|
|
192
|
+
systemDecisions: systemDecisions?.systemDecisions || systemDecisions
|
|
184
193
|
};
|
|
185
194
|
const plan = await planModelMigration(root, options);
|
|
186
195
|
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
@@ -228,15 +237,17 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
228
237
|
else printAgentOverview(result);
|
|
229
238
|
return result;
|
|
230
239
|
}
|
|
231
|
-
const result = buildAgentGuide(loaded, type, { id: flags.id });
|
|
240
|
+
const result = buildAgentGuide(loaded, type, { id: flags.id, programId: flags.program });
|
|
232
241
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
233
242
|
else printAgentGuide(result);
|
|
234
243
|
return result;
|
|
235
244
|
}
|
|
236
245
|
if (command === "program-path") {
|
|
237
246
|
const loaded = await loadWorkspace(root);
|
|
238
|
-
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
239
247
|
const auditId = positionals[0] || flags.audit;
|
|
248
|
+
const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
|
|
249
|
+
const programId = flags.program || audit?.programId;
|
|
250
|
+
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
|
|
240
251
|
const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
|
|
241
252
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
242
253
|
const output = selectProgramPathOutput(result, flags);
|
|
@@ -247,6 +258,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
247
258
|
if (command === "workflow") {
|
|
248
259
|
const result = await assessWorkflow(root, {
|
|
249
260
|
auditId: positionals[0] || flags.audit,
|
|
261
|
+
programId: flags.program,
|
|
250
262
|
asOf: flags["as-of"],
|
|
251
263
|
through: flags.through,
|
|
252
264
|
includeComplete: Boolean(flags.complete)
|
|
@@ -264,6 +276,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
264
276
|
: undefined;
|
|
265
277
|
const result = await assessWorkflow(root, {
|
|
266
278
|
auditId: positionals[0] || flags.audit,
|
|
279
|
+
programId: flags.program,
|
|
267
280
|
asOf: flags["as-of"],
|
|
268
281
|
through: flags.end || flags.through,
|
|
269
282
|
coverage
|
|
@@ -292,10 +305,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
292
305
|
}
|
|
293
306
|
if (command === "milestone-check") {
|
|
294
307
|
const loaded = await loadWorkspace(root);
|
|
295
|
-
const result = await assessWorkflow(loaded, { asOf: flags["as-of"] });
|
|
296
|
-
const
|
|
308
|
+
const result = await assessWorkflow(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
309
|
+
const program = resolveProgram(loaded, flags.program);
|
|
310
|
+
const target = program?.assuranceGoal === "none"
|
|
297
311
|
? "structuralValidity"
|
|
298
|
-
:
|
|
312
|
+
: program?.candidateCoverage
|
|
299
313
|
? "periodHealth"
|
|
300
314
|
: "evidenceReadiness";
|
|
301
315
|
const output = {
|
|
@@ -312,7 +326,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
312
326
|
if (command === "scaffold") {
|
|
313
327
|
const loaded = await loadWorkspace(root);
|
|
314
328
|
const type = positionals[0];
|
|
315
|
-
const result = scaffoldResourceMutation(loaded, type, flags.title, { id: flags.id });
|
|
329
|
+
const result = scaffoldResourceMutation(loaded, type, flags.title, { id: flags.id, programId: flags.program });
|
|
316
330
|
console.log(JSON.stringify(result, null, 2));
|
|
317
331
|
return result;
|
|
318
332
|
}
|
|
@@ -333,7 +347,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
333
347
|
const output = flags.workflow
|
|
334
348
|
? {
|
|
335
349
|
records,
|
|
336
|
-
workflow: await assessWorkflow(loaded, { asOf })
|
|
350
|
+
workflow: await assessWorkflow(loaded, { asOf, programId: flags.program })
|
|
337
351
|
}
|
|
338
352
|
: records;
|
|
339
353
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
@@ -394,7 +408,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
394
408
|
}
|
|
395
409
|
if (command === "program-readiness") {
|
|
396
410
|
const loaded = await loadWorkspace(root);
|
|
397
|
-
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
411
|
+
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
398
412
|
const output = flags.summary ? summarizeProgramReadiness(result) : result;
|
|
399
413
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
400
414
|
else if (flags.summary) {
|
|
@@ -427,7 +441,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
427
441
|
}
|
|
428
442
|
if (command === "evidence-map") {
|
|
429
443
|
const loaded = await loadWorkspace(root);
|
|
430
|
-
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"] });
|
|
444
|
+
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
431
445
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
432
446
|
else {
|
|
433
447
|
console.log(`${result.status.toUpperCase()}: ${result.counts.complete} mapped, ${result.counts.action} need action`);
|
|
@@ -435,11 +449,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
435
449
|
console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
436
450
|
if (item.status !== "action") continue;
|
|
437
451
|
if (item.sourceKinds?.length) console.log(` Source role: ${item.sourceKinds.join(" or ")}`);
|
|
438
|
-
for (const source of item.sourceSystemChecks || []) {
|
|
452
|
+
for (const source of item.sourceComponentChecks || item.sourceSystemChecks || []) {
|
|
439
453
|
const missing = Object.entries(source.checks)
|
|
440
454
|
.filter(([, passed]) => !passed)
|
|
441
455
|
.map(([name]) => evidenceSourceCheckName(name));
|
|
442
|
-
if (missing.length) console.log(` ${source.sourceSystemId}: ${missing.join(", ")}`);
|
|
456
|
+
if (missing.length) console.log(` ${source.sourceComponentId || source.sourceSystemId}: ${missing.join(", ")}`);
|
|
443
457
|
}
|
|
444
458
|
if (item.commands?.length) console.log(` Next: ${item.commands[0]}`);
|
|
445
459
|
}
|
|
@@ -989,15 +1003,15 @@ Usage:
|
|
|
989
1003
|
filegrc build [root] [--output .filegrc/site]
|
|
990
1004
|
filegrc validate [root] [--json]
|
|
991
1005
|
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]
|
|
1006
|
+
filegrc migrate --to-model <2|3|4> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
993
1007
|
filegrc describe <resource-type>
|
|
994
1008
|
filegrc types [--json]
|
|
995
|
-
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
1009
|
+
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
996
1010
|
filegrc program-path [audit-id] [--as-of YYYY-MM-DD] [--summary|--next|--current] [--json]
|
|
997
1011
|
filegrc workflow [audit-id] [--as-of YYYY-MM-DD] [--through YYYY-MM-DD] [--complete] [--require-ready] [--json]
|
|
998
1012
|
filegrc period-health [audit-id] [--start YYYY-MM-DD --end YYYY-MM-DD] [--as-of YYYY-MM-DD] [--require-healthy] [--json]
|
|
999
1013
|
filegrc milestone-check [--as-of YYYY-MM-DD] [--json]
|
|
1000
|
-
filegrc scaffold <resource-type> --title text [--id resource-id]
|
|
1014
|
+
filegrc scaffold <resource-type> --title text [--id resource-id] [--program program-id]
|
|
1001
1015
|
filegrc list [resource-type] [--workflow] [--json]
|
|
1002
1016
|
filegrc search <query> [--type resource-type] [--json]
|
|
1003
1017
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
@@ -1038,7 +1052,8 @@ function printCommandHelp(command) {
|
|
|
1038
1052
|
|
|
1039
1053
|
Options:
|
|
1040
1054
|
--host <address> Bind address. Defaults to FILEGRC_HOST or 127.0.0.1.
|
|
1041
|
-
--port <number>
|
|
1055
|
+
--port <number> Preferred port. Defaults to FILEGRC_PORT or 8787. If occupied,
|
|
1056
|
+
the server uses an available port. Use 0 to choose one directly.
|
|
1042
1057
|
--root <path> Workspace path when no positional root is given.
|
|
1043
1058
|
--allow-non-authoritative-writes
|
|
1044
1059
|
Allow local browser writes from a task checkout. This explicit
|
|
@@ -1076,15 +1091,19 @@ Options:
|
|
|
1076
1091
|
}
|
|
1077
1092
|
if (command === "migrate") {
|
|
1078
1093
|
console.log(`Usage:
|
|
1079
|
-
filegrc migrate --to-model <2|3> [options]
|
|
1094
|
+
filegrc migrate --to-model <2|3|4> [options]
|
|
1080
1095
|
|
|
1081
1096
|
Upgrade a workspace through an explicit, reviewable model boundary. Model v1
|
|
1082
|
-
workspaces migrate to v2 first. Model v2 workspaces migrate to v3
|
|
1097
|
+
workspaces migrate to v2 first. Model v2 workspaces migrate to v3, then v4. v4 separates
|
|
1098
|
+
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1099
|
+
specific Assets, Vendors, normalized information, and Evidence Artifacts. v3 migration
|
|
1100
|
+
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1083
1101
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1084
1102
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
1085
1103
|
|
|
1086
1104
|
Options:
|
|
1087
|
-
--to-model <version> Required target model;
|
|
1105
|
+
--to-model <version> Required target model; migrations must run in order through 2, 3, and 4
|
|
1106
|
+
--decisions <path> v4 JSON object keyed by old System ID with system/component decisions
|
|
1088
1107
|
--preview Show the complete atomic record plan without writing
|
|
1089
1108
|
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
1090
1109
|
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
@@ -1094,7 +1113,7 @@ Options:
|
|
|
1094
1113
|
--help Show this help
|
|
1095
1114
|
|
|
1096
1115
|
Start with:
|
|
1097
|
-
npx filegrc migrate --to-model
|
|
1116
|
+
npx filegrc migrate --to-model 4 --preview --json`);
|
|
1098
1117
|
return;
|
|
1099
1118
|
}
|
|
1100
1119
|
if (command === "program-readiness") {
|
|
@@ -1106,6 +1125,7 @@ controls, and mapped every selected control to a configured authoritative eviden
|
|
|
1106
1125
|
source. No audit ID or CPA firm is required.
|
|
1107
1126
|
|
|
1108
1127
|
Options:
|
|
1128
|
+
--program <id> Program to assess when more than one active Program exists
|
|
1109
1129
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
1110
1130
|
--require-ready Exit with code 2 unless the Evidence Ready gate passes
|
|
1111
1131
|
--summary Omit item details and print stage counts and next actions
|
|
@@ -1124,6 +1144,7 @@ deterministic Work Items, and one recommended next action.
|
|
|
1124
1144
|
|
|
1125
1145
|
Options:
|
|
1126
1146
|
--audit <id> Limit audit assessments to one engagement
|
|
1147
|
+
--program <id> Program to assess when no Audit supplies the Program
|
|
1127
1148
|
--as-of <date> Evaluate on YYYY-MM-DD
|
|
1128
1149
|
--through <date> Include scheduled work through YYYY-MM-DD
|
|
1129
1150
|
--complete Include completed Work Items
|
|
@@ -1143,6 +1164,7 @@ and current readiness state. Pass an audit ID to include Step 5 status.
|
|
|
1143
1164
|
|
|
1144
1165
|
Options:
|
|
1145
1166
|
--audit <id> Audit record to use for Step 5
|
|
1167
|
+
--program <id> Program to assess when no Audit supplies the Program
|
|
1146
1168
|
--as-of <date> Evaluate readiness on YYYY-MM-DD
|
|
1147
1169
|
--summary Print compact status and the first action for all five steps
|
|
1148
1170
|
--next Print only the current step and its first action
|
|
@@ -1157,10 +1179,11 @@ Options:
|
|
|
1157
1179
|
filegrc evidence-map [options]
|
|
1158
1180
|
|
|
1159
1181
|
Inspect the evidence-source checks included in Control implementation. Each item
|
|
1160
|
-
reports the required source roles, linked Controls, authoritative source
|
|
1182
|
+
reports the required source roles, linked Controls, authoritative source Components,
|
|
1161
1183
|
per-record checks, and exact edit commands. This diagnostic is read-only.
|
|
1162
1184
|
|
|
1163
1185
|
Options:
|
|
1186
|
+
--program <id> Program to assess when more than one active Program exists
|
|
1164
1187
|
--as-of <date> Evaluate the map on YYYY-MM-DD
|
|
1165
1188
|
--json Print the map as JSON
|
|
1166
1189
|
--root <path> Workspace path
|
|
@@ -1179,7 +1202,7 @@ function agentOverview(model) {
|
|
|
1179
1202
|
build: "filegrc build [root]",
|
|
1180
1203
|
validate: "filegrc validate [root] --json",
|
|
1181
1204
|
model: "filegrc model --json",
|
|
1182
|
-
migrate: "filegrc migrate --to-model
|
|
1205
|
+
migrate: "filegrc migrate --to-model 4 --preview --json",
|
|
1183
1206
|
describe: "filegrc describe <resource-type>",
|
|
1184
1207
|
types: "filegrc types --json",
|
|
1185
1208
|
guide: "filegrc guide [resource-type] --json",
|