filegrc 0.5.0 → 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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/Sunpeak-AI/filegrc.git"
8
+ "url": "git+https://github.com/Alignbase/filegrc.git"
9
9
  },
10
10
  "type": "module",
11
11
  "bin": {
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
- }[loaded.workspace?.assuranceGoal];
217
+ }[program?.assuranceGoal];
210
218
  if (kind) record.auditKind = kind;
211
219
  for (const field of ["frameworkIds", "systemIds", "requirementIds", "controlIds"]) {
212
- if (loaded.workspace?.[field]?.length) record[field] = [...loaded.workspace[field]];
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
  }
@@ -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 sourceSystems = loaded.resources.filter((record) => record.type === "system");
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 External Evidence",
613
+ "Review Evidence Artifacts",
612
614
  externalControls.length
613
- ? `${controlsWithExternalEvidence.length} of ${externalControls.length} selected controls that rely on external systems have verified External Evidence for the formal period. Confirm the source System, date or period, control links, collector, verifier, and retained artifact or approved external reference.`
614
- : "No selected controls require a separate External Evidence record.",
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 systems = records.filter((record) => record.type === "system" && record.status === "active");
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.sourceSystemId)
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.sourceSystemId))
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", "Review both evidence paths: dated filegrc operating records and verified External Evidence from authoritative systems. filegrc includes both in the audit packet.", items);
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),
@@ -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) !== "3") {
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
- subserviceVendorIds: [...(prior.subserviceVendorIds || [])],
36
- ...(prior.subserviceMethod ? { subserviceMethod: prior.subserviceMethod } : {}),
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
  : {}),
@@ -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) !== "3") {
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
- && !record.applicabilityReview
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) !== "3") {
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 update = options.decisions.map((decision) => {
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: update.map(({ id }) => id),
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} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
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, vendors, and in-scope systems. Run npx filegrc program-path --next --json.");
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 3.");
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 target = loaded.workspace?.assuranceGoal === "none"
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
- : loaded.workspace?.candidateCoverage
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 with planned
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; 2 for legacy v1 migration, 3 for the active 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 3 --preview --json`);
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 Systems,
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 3 --preview --json",
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",