filegrc 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -53,7 +53,9 @@ import { activatePolicies, planPolicyActivation, scaffoldPolicyActivation } from
53
53
  import { applyPolicyLibraryUpgrade, assessPolicyLibraryUpgrades } from "./policy-library.js";
54
54
  import { buildAgentProgramPath } from "./program-path.js";
55
55
  import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
56
+ import { planProgramAmendment } from "./program-amendment.js";
56
57
  import { resolveProgram } from "./program.js";
58
+ import { resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
57
59
  import { applyReconciliation, planReconciliation } from "./reconciliation.js";
58
60
  import { markdownEntries } from "./resource-markdown.js";
59
61
  import { effectiveResourceStatus } from "./resource-status.js";
@@ -223,6 +225,7 @@ export async function runCli(argv = process.argv.slice(2)) {
223
225
  : `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
224
226
  } else {
225
227
  console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
228
+ if (result.migrationReportPath) console.log(`Migration report: ${result.migrationReportPath}`);
226
229
  }
227
230
  return result;
228
231
  }
@@ -261,7 +264,10 @@ export async function runCli(argv = process.argv.slice(2)) {
261
264
  const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
262
265
  const programId = flags.program || audit?.programId;
263
266
  const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
264
- const auditReadiness = auditId ? await assessAuditPreparation(loaded, { auditId }) : null;
267
+ const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
268
+ auditId,
269
+ asOf: flags["as-of"]
270
+ }) : null;
265
271
  const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
266
272
  const output = selectProgramPathOutput(result, flags);
267
273
  if (flags.json) console.log(JSON.stringify(output, null, 2));
@@ -458,6 +464,46 @@ export async function runCli(argv = process.argv.slice(2)) {
458
464
  if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
459
465
  return output;
460
466
  }
467
+ if (command === "program-amendment") {
468
+ const sourceResourceId = flags.source || positionals[0];
469
+ if (!sourceResourceId) throw new Error("Pass a source resource ID or --source resource-id.");
470
+ const result = await planProgramAmendment(root, { sourceResourceId });
471
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
472
+ else {
473
+ console.log(`Program amendment review for ${result.source.title}`);
474
+ for (const [type, ids] of Object.entries(result.byResourceType)) console.log(`${type}\t${ids.join(",")}`);
475
+ for (const work of result.reviewWork) {
476
+ console.log(`REVIEW\t${work.resourceType}\t${work.resourceIds?.length ? work.resourceIds.join(",") + "\t" : ""}${work.message}`);
477
+ }
478
+ }
479
+ return result;
480
+ }
481
+ if (command === "review-bindings") {
482
+ const resourceId = positionals[0];
483
+ if (!resourceId) throw new Error("Pass a Retention Schedule Item or Requirement Mapping ID.");
484
+ const loaded = await loadWorkspace(root);
485
+ const record = loaded.resources.find(({ id }) => id === resourceId);
486
+ if (!record) throw new Error(`Resource "${resourceId}" was not found.`);
487
+ const dependencyIds = record.type === "retention-schedule-item"
488
+ ? retentionReviewResourceIds(record, loaded)
489
+ : record.type === "requirement-mapping"
490
+ ? [...new Set([...(record.sourceResourceIds || []), ...(record.targetResourceIds || [])])]
491
+ : null;
492
+ if (!dependencyIds) throw new Error("Review bindings are available for Retention Schedule Items and Requirement Mappings.");
493
+ const revisions = await resourceReviewRevisions(loaded, dependencyIds);
494
+ const result = {
495
+ resource: { type: record.type, id: record.id, title: record.title },
496
+ dependencyIds,
497
+ reviewedSourceRevisions: Object.fromEntries(revisions),
498
+ missingResourceIds: dependencyIds.filter((id) => !revisions.has(id))
499
+ };
500
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
501
+ else {
502
+ console.log(`Review bindings for ${record.title}`);
503
+ for (const id of dependencyIds) console.log(`${revisions.has(id) ? "READY" : "MISSING"}\t${id}\t${revisions.get(id) || ""}`);
504
+ }
505
+ return result;
506
+ }
461
507
  if (command === "evidence-map") {
462
508
  const loaded = await loadWorkspace(root);
463
509
  const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"], programId: flags.program });
@@ -481,7 +527,10 @@ export async function runCli(argv = process.argv.slice(2)) {
481
527
  }
482
528
  if (command === "audit-readiness") {
483
529
  const loaded = await loadWorkspace(root);
484
- const result = await assessAuditPreparation(loaded, { auditId: positionals[0] || flags.audit });
530
+ const result = await assessAuditPreparation(loaded, {
531
+ auditId: positionals[0] || flags.audit,
532
+ asOf: flags["as-of"]
533
+ });
485
534
  if (flags.json) console.log(JSON.stringify(result, null, 2));
486
535
  else {
487
536
  console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} management items complete`);
@@ -1115,7 +1164,7 @@ Usage:
1115
1164
  filegrc build [root] [--output .filegrc/site]
1116
1165
  filegrc validate [root] [--json]
1117
1166
  filegrc model [--json|--write-docs|--check-docs]
1118
- filegrc migrate --to-model <2|3|4|5|6> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
1167
+ filegrc migrate --to-model <${SUPPORTED_MODEL_VERSIONS.join("|")}> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
1119
1168
  filegrc describe <resource-type>
1120
1169
  filegrc types [--json]
1121
1170
  filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
@@ -1128,8 +1177,10 @@ Usage:
1128
1177
  filegrc search <query> [--type resource-type] [--json]
1129
1178
  filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
1130
1179
  filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
1180
+ filegrc program-amendment <source-resource-id> [--json]
1181
+ filegrc review-bindings <retention-or-mapping-id> [--json]
1131
1182
  filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
1132
- filegrc audit-readiness [audit-id] [--require-ready] [--json]
1183
+ filegrc audit-readiness [audit-id] [--as-of YYYY-MM-DD] [--require-ready] [--json]
1133
1184
  filegrc prepare-audit <audit-id> [--json]
1134
1185
  filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes] [--risk-level normal|high] [--json]
1135
1186
  filegrc external-reviewer-setup --scaffold
@@ -1214,7 +1265,9 @@ workspaces migrate to v2 first. Continue one version at a time through v${ACTIVE
1214
1265
  the repository Workspace, management Program, bounded Systems, operational Components,
1215
1266
  specific Assets, Vendors, normalized information, and Evidence Artifacts. Model v5 separates
1216
1267
  Document approval from activation and records program-versus-engagement scope. Model v6 gives
1217
- Training the same approval and activation split and moves its schedule into Obligations. v3 migration
1268
+ Training the same approval and activation split and moves its schedule into Obligations. Model v7
1269
+ keeps issued historical Documents neutral and requires a current activation for legacy Training. Model v8
1270
+ adds structured retention schedule items, reviewed requirement mappings, source-linked Commitments, and custom obligations. v3 migration
1218
1271
  previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
1219
1272
  core Appointments, removal of obsolete manual page state, classified review work,
1220
1273
  and dataModelVersion changed last. The command writes no Git commit.
@@ -1231,7 +1284,8 @@ Options:
1231
1284
  --help Show this help
1232
1285
 
1233
1286
  Start with:
1234
- npx filegrc migrate --to-model ${ACTIVE_MODEL_VERSION} --preview --json`);
1287
+ npx filegrc guide --json
1288
+ # Use the next model version shown by the guide.`);
1235
1289
  return;
1236
1290
  }
1237
1291
  if (command === "program-readiness") {
@@ -1252,6 +1306,24 @@ Options:
1252
1306
  --help Show this help`);
1253
1307
  return;
1254
1308
  }
1309
+ if (command === "program-amendment") {
1310
+ console.log(`Usage:
1311
+ filegrc program-amendment <source-resource-id> [--json]
1312
+
1313
+ Trace a Policy, Document, Framework, Requirement, or Commitment through its
1314
+ Commitments, mappings, Controls, Obligations, and retention rules. The command
1315
+ only reports review work. It does not infer or write management decisions.`);
1316
+ return;
1317
+ }
1318
+ if (command === "review-bindings") {
1319
+ console.log(`Usage:
1320
+ filegrc review-bindings <retention-or-mapping-id> [--json]
1321
+
1322
+ Calculate the exact current source revisions required by an active Retention
1323
+ Schedule Item or Requirement Mapping. Copy reviewedSourceRevisions into the
1324
+ record only after management completes the review.`);
1325
+ return;
1326
+ }
1255
1327
  if (command === "workflow") {
1256
1328
  console.log(`Usage:
1257
1329
  filegrc workflow [audit-id] [options]
@@ -1398,6 +1470,10 @@ Options:
1398
1470
  }
1399
1471
 
1400
1472
  function agentOverview(model) {
1473
+ const currentModelIndex = SUPPORTED_MODEL_VERSIONS.indexOf(String(model.modelVersion));
1474
+ const nextModelVersion = currentModelIndex >= 0 && currentModelIndex < SUPPORTED_MODEL_VERSIONS.length - 1
1475
+ ? SUPPORTED_MODEL_VERSIONS[currentModelIndex + 1]
1476
+ : ACTIVE_MODEL_VERSION;
1401
1477
  const commands = {
1402
1478
  help: "filegrc help",
1403
1479
  version: "filegrc version",
@@ -1406,7 +1482,7 @@ function agentOverview(model) {
1406
1482
  build: "filegrc build [root]",
1407
1483
  validate: "filegrc validate [root] --json",
1408
1484
  model: "filegrc model --json",
1409
- migrate: "filegrc migrate --to-model 6 --preview --json",
1485
+ migrate: `filegrc migrate --to-model ${nextModelVersion} --preview --json`,
1410
1486
  describe: "filegrc describe <resource-type>",
1411
1487
  types: "filegrc types --json",
1412
1488
  guide: "filegrc guide [resource-type] --json",
@@ -1419,6 +1495,8 @@ function agentOverview(model) {
1419
1495
  search: "filegrc search <query> --json",
1420
1496
  obligations: "filegrc obligations --json",
1421
1497
  programReadiness: "filegrc program-readiness --json",
1498
+ programAmendment: "filegrc program-amendment <source-resource-id> --json",
1499
+ reviewBindings: "filegrc review-bindings <retention-or-mapping-id> --json",
1422
1500
  evidenceMap: "filegrc evidence-map --json",
1423
1501
  auditReadiness: "filegrc audit-readiness <audit-id> --json",
1424
1502
  prepareAudit: "filegrc prepare-audit <audit-id>",
@@ -1,6 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { modelSupports } from "../model/index.js";
3
- import { collectionRevision } from "./collection-revision.js";
3
+ import {
4
+ collectionRevision,
5
+ collectionRevisionMatches
6
+ } from "./collection-revision.js";
4
7
  import { scopedCollectionRecords } from "./collection-scope.js";
5
8
  import { applyResourceBatch } from "./files.js";
6
9
  import { getGitSummary } from "./git.js";
@@ -41,15 +44,25 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
41
44
  const allowsEmptyCollection = allowedDecisions.some((decision) => (
42
45
  decision === "zero-population" || decision === "externally-managed"
43
46
  ));
47
+ const revisionMatches = collectionRevisionMatches(
48
+ loaded,
49
+ resourceType,
50
+ review?.collectionRevision,
51
+ {
52
+ programId: program.id,
53
+ authoritativeSourceId,
54
+ currentRevision
55
+ }
56
+ );
44
57
  const complete = Boolean(
45
58
  review?.status === "active"
46
59
  && allowedDecisions.includes(review.decision)
47
- && review.collectionRevision === currentRevision
60
+ && revisionMatches
48
61
  );
49
62
  const stale = Boolean(
50
63
  review?.status === "active"
51
64
  && review.collectionRevision
52
- && review.collectionRevision !== currentRevision
65
+ && !revisionMatches
53
66
  );
54
67
  return {
55
68
  resourceType,
@@ -10,16 +10,36 @@ import { resolveProgram } from "./program.js";
10
10
  import { markdownEntries } from "./resource-markdown.js";
11
11
 
12
12
  export function collectionRevision(loaded, resourceType, options = {}) {
13
+ return calculateCollectionRevision(loaded, resourceType, options, false);
14
+ }
15
+
16
+ export function legacyCollectionRevision(loaded, resourceType, options = {}) {
17
+ return calculateCollectionRevision(loaded, resourceType, options, true);
18
+ }
19
+
20
+ export function collectionRevisionMatches(loaded, resourceType, storedRevision, options = {}) {
21
+ if (!storedRevision) return false;
22
+ const currentRevision = options.currentRevision
23
+ || collectionRevision(loaded, resourceType, options);
24
+ // Version 0.9.2 narrowed this hash basis. Keep unchanged 0.9.1 reviews valid
25
+ // until management records a new review on the current basis.
26
+ return storedRevision === currentRevision
27
+ || storedRevision === legacyCollectionRevision(loaded, resourceType, options);
28
+ }
29
+
30
+ function calculateCollectionRevision(loaded, resourceType, options, legacy) {
13
31
  const program = Object.hasOwn(options, "program")
14
32
  ? options.program
15
33
  : resolveProgram(loaded, options.programId);
16
- const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program)
34
+ const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program, { legacy })
17
35
  .map((input) => [input.record.id, input]));
18
36
  const authoritativeSource = loaded.resources.find(({ id }) => id === options.authoritativeSourceId);
19
37
  if (authoritativeSource) {
20
38
  inputs.set(authoritativeSource.id, {
21
39
  record: authoritativeSource,
22
- value: authoritativeSourceRevisionValue(authoritativeSource),
40
+ value: legacy
41
+ ? authoritativeSource
42
+ : authoritativeSourceRevisionValue(authoritativeSource),
23
43
  includeContent: true
24
44
  });
25
45
  }
@@ -29,7 +49,7 @@ export function collectionRevision(loaded, resourceType, options = {}) {
29
49
  revision: createHash("sha256")
30
50
  .update(JSON.stringify(canonicalRecordValue(loaded.model, record.type, value)))
31
51
  .digest("hex"),
32
- contentRevisions: (includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
52
+ contentRevisions: (legacy || includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
33
53
  try {
34
54
  const content = readFileSync(resolveDataPath(loaded.root, path), "utf8");
35
55
  return [{ path, revision: createHash("sha256").update(content).digest("hex") }];
@@ -5,8 +5,27 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
5
5
  if (!modelSupports(loaded.model, "program-scope")) {
6
6
  return loaded.resources.filter((record) => record.type === resourceType);
7
7
  }
8
+ if (resourceType === "person") {
9
+ return scopedProgramPeople(loaded, program);
10
+ }
8
11
  if (resourceType === "vendor") {
9
- return loaded.resources.filter((record) => record.type === "vendor");
12
+ const scopedVendorIds = new Set(programComponents(loaded, program).map(({ vendorId }) => vendorId).filter(Boolean));
13
+ const auditVendorIds = new Set(loaded.resources
14
+ .filter((record) => record.type === "audit" && record.auditorVendorId)
15
+ .map(({ auditorVendorId }) => auditorVendorId));
16
+ const auditOnlyVendorIds = Number(loaded.model.modelVersion) >= 7
17
+ ? auditVendorIds
18
+ : new Set(loaded.resources
19
+ .filter((record) => (
20
+ record.type === "vendor"
21
+ && auditVendorIds.has(record.id)
22
+ && /(?:audit|accounting|cpa)/i.test(record.category || "")
23
+ ))
24
+ .map(({ id }) => id));
25
+ return loaded.resources.filter((record) => (
26
+ record.type === "vendor"
27
+ && (!auditOnlyVendorIds.has(record.id) || scopedVendorIds.has(record.id))
28
+ ));
10
29
  }
11
30
  const scopedProgram = program || {};
12
31
  const components = programComponents(loaded, scopedProgram);
@@ -36,7 +55,44 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
36
55
  ));
37
56
  }
38
57
 
39
- export function collectionRevisionInputs(loaded, resourceType, program) {
58
+ function scopedProgramPeople(loaded, program = {}) {
59
+ const personIds = new Set(loaded.resources.filter(({ type }) => type === "person").map(({ id }) => id));
60
+ const systemIds = new Set(program.systemIds || []);
61
+ const controlIds = new Set(program.controlIds || []);
62
+ const policyIds = new Set(loaded.resources
63
+ .filter(({ type, status, programRole }) => type === "policy" && status !== "retired" && programRole !== "reference")
64
+ .map(({ id }) => id));
65
+ const components = programComponents(loaded, program);
66
+ const componentIds = new Set(components.map(({ id }) => id));
67
+ const vendorIds = new Set([
68
+ ...components.map(({ vendorId }) => vendorId).filter(Boolean),
69
+ ...scopedCollectionRecords(loaded, "vendor", program).map(({ id }) => id)
70
+ ]);
71
+ const sources = loaded.resources.filter((record) => (
72
+ ["workspace", "program", "appointment", "team"].includes(record.type)
73
+ || record.type === "system" && systemIds.has(record.id)
74
+ || record.type === "component" && componentIds.has(record.id)
75
+ || record.type === "vendor" && vendorIds.has(record.id)
76
+ || record.type === "control" && controlIds.has(record.id)
77
+ || record.type === "policy" && policyIds.has(record.id)
78
+ || record.type === "document" && record.workflowScope !== "engagement"
79
+ || record.type === "obligation" && (
80
+ (record.controlIds || []).some((id) => controlIds.has(id))
81
+ || (record.policyIds || []).some((id) => policyIds.has(id))
82
+ )
83
+ ));
84
+ const selected = new Set();
85
+ const visit = (value) => {
86
+ if (typeof value === "string" && personIds.has(value)) selected.add(value);
87
+ else if (Array.isArray(value)) value.forEach(visit);
88
+ else if (value && typeof value === "object") Object.values(value).forEach(visit);
89
+ };
90
+ sources.forEach(visit);
91
+ return loaded.resources.filter((record) => record.type === "person" && selected.has(record.id));
92
+ }
93
+
94
+ export function collectionRevisionInputs(loaded, resourceType, program, options = {}) {
95
+ const legacy = options.legacy === true;
40
96
  const reviewed = scopedCollectionRecords(loaded, resourceType, program);
41
97
  if (!modelSupports(loaded.model, "program-scope")) {
42
98
  return reviewed.map((record) => ({ record, value: record, includeContent: true }));
@@ -75,6 +131,13 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
75
131
  addIds((record.informationUses || []).map(({ informationTypeId }) => informationTypeId));
76
132
  }
77
133
  }
134
+ if (resourceType === "information-type" && !legacy) {
135
+ addIds(program?.systemIds);
136
+ addIds(programComponents(loaded, program || {}).map(({ id }) => id));
137
+ addIds(loaded.resources
138
+ .filter((record) => record.type === "vendor" && record.status !== "retired")
139
+ .map(({ id }) => id));
140
+ }
78
141
  if (resourceType === "complementary-control") {
79
142
  addIds(program?.systemIds);
80
143
  addIds(program?.controlIds);
@@ -90,8 +153,9 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
90
153
  record,
91
154
  value: reviewedIds.has(record.id)
92
155
  ? record
93
- : dependencyRevisionValue(resourceType, record),
94
- includeContent: reviewedIds.has(record.id)
156
+ : dependencyRevisionValue(resourceType, record, legacy),
157
+ includeContent: legacy
158
+ || reviewedIds.has(record.id)
95
159
  || dependencyContentAffectsRevision(resourceType, record.type)
96
160
  }));
97
161
  }
@@ -167,11 +231,16 @@ const dependencyFields = {
167
231
  },
168
232
  component: {
169
233
  system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
170
- control: ["id", "type", "status", "systemIds", "componentIds", "evidenceSourceComponentIds"],
234
+ control: ["id", "type", "systemIds", "componentIds", "evidenceSourceComponentIds"],
171
235
  vendor: ["id", "type", "status", "category", "criticality", "description", "startDate", "endDate"],
172
236
  classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
173
237
  "information-type": ["id", "type", "status", "classificationId", "description"]
174
238
  },
239
+ "information-type": {
240
+ system: ["id", "type", "status", "informationTypeIds"],
241
+ component: ["id", "type", "status", "systemUses", "informationUses"],
242
+ vendor: ["id", "type", "status", "informationTypeIds"]
243
+ },
175
244
  "complementary-control": {
176
245
  system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
177
246
  control: ["id", "type", "status", "statement", "activity", "systemIds", "componentIds", "evidenceSourceComponentIds"],
@@ -183,6 +252,21 @@ const dependencyFields = {
183
252
  }
184
253
  };
185
254
 
255
+ const legacyDependencyFieldOverrides = {
256
+ framework: {
257
+ system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"]
258
+ },
259
+ component: {
260
+ control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
261
+ vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"]
262
+ },
263
+ "complementary-control": {
264
+ system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
265
+ control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
266
+ component: ["id", "type", "status", "componentKind", "description", "vendorId", "systemUses", "informationUses", "internetExposed"]
267
+ }
268
+ };
269
+
186
270
  const dependencyContentTypes = {
187
271
  framework: new Set(["system"]),
188
272
  vendor: new Set(["document"]),
@@ -190,8 +274,11 @@ const dependencyContentTypes = {
190
274
  "complementary-control": new Set(["system", "control", "document", "component"])
191
275
  };
192
276
 
193
- function dependencyRevisionValue(resourceType, record) {
194
- const fields = dependencyFields[resourceType]?.[record.type];
277
+ function dependencyRevisionValue(resourceType, record, legacy) {
278
+ const fields = legacy
279
+ ? legacyDependencyFieldOverrides[resourceType]?.[record.type]
280
+ || dependencyFields[resourceType]?.[record.type]
281
+ : dependencyFields[resourceType]?.[record.type];
195
282
  if (!fields) return { id: record.id, type: record.type };
196
283
  return Object.fromEntries(fields
197
284
  .filter((field) => record[field] !== undefined)
@@ -14,6 +14,13 @@ export async function scaffoldDocumentActivation(input = process.cwd(), options
14
14
  const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
15
15
  const today = currentCalendarDate(loaded.workspace.timezone);
16
16
  return {
17
+ available: candidates.length > 0,
18
+ message: candidates.length
19
+ ? `${candidates.length} approved ${candidates.length === 1 ? "Document is" : "Documents are"} ready to activate.`
20
+ : `No ${options.auditId ? "engagement Document" : "program Document"} is ready to activate. Review the current readiness actions first.`,
21
+ nextCommand: candidates.length ? null : options.auditId
22
+ ? `npx filegrc audit-readiness ${options.auditId} --json`
23
+ : "npx filegrc program-path --next --json",
17
24
  documentIds: candidates.filter(({ resourceType }) => resourceType === "document").map(({ resourceId }) => resourceId),
18
25
  ...(options.auditId ? { auditId: options.auditId, workflowScope: "engagement" } : { workflowScope: "program" }),
19
26
  activatedByIds: [],
@@ -141,12 +148,17 @@ export async function scaffoldGovernedContentActivation(input = process.cwd(), o
141
148
  const loaded = await loadWorkspace(input);
142
149
  requireDocumentLifecycle(loaded);
143
150
  if (!modelSupports(loaded.model, "governed-training-activation")) {
144
- throw new Error("Unified governed-content activation requires a model v6 workspace.");
151
+ throw new Error("Unified governed-content activation requires a model v6 or later workspace.");
145
152
  }
146
153
  const candidates = await activationCandidates(loaded, { ...options, auditId: undefined });
147
154
  const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
148
155
  const today = currentCalendarDate(loaded.workspace.timezone);
149
156
  return {
157
+ available: candidates.length > 0,
158
+ message: candidates.length
159
+ ? `${candidates.length} approved governed-content ${candidates.length === 1 ? "record is" : "records are"} ready to activate.`
160
+ : "No program Document or Training record is ready to activate. Review the current readiness actions first.",
161
+ nextCommand: candidates.length ? null : "npx filegrc program-path --next --json",
150
162
  resourceIds: candidates.map(({ resourceId }) => resourceId),
151
163
  documentIds: candidates.filter(({ resourceType }) => resourceType === "document").map(({ resourceId }) => resourceId),
152
164
  trainingIds: candidates.filter(({ resourceType }) => resourceType === "training").map(({ resourceId }) => resourceId),
package/src/git.js CHANGED
@@ -390,8 +390,8 @@ function unavailableSnapshot(error, extra = {}) {
390
390
  }
391
391
 
392
392
  export async function getBrowserRepositoryState(input = process.cwd(), options = {}) {
393
- const root = resolveWorkspaceRoot(input);
394
- const config = await getRepositoryConfig(root);
393
+ const root = input?.entries && input?.root ? input.root : resolveWorkspaceRoot(input);
394
+ const config = await getRepositoryConfig(input);
395
395
  const gitSummary = options.repositorySnapshot ?? await getRepositorySnapshot(root);
396
396
  if (config.mode !== "trunk") {
397
397
  return {
@@ -852,8 +852,8 @@ function syncReadySummary(root, action) {
852
852
  return summary;
853
853
  }
854
854
 
855
- async function getRepositoryConfig(root) {
856
- const loaded = await loadWorkspace(root);
855
+ async function getRepositoryConfig(input) {
856
+ const loaded = input?.entries && input?.root ? input : await loadWorkspace(input);
857
857
  const renderer = loaded.resources.find(({ type, id }) => type === "renderer-settings" && id === "renderer-settings");
858
858
  const mode = renderer?.repositoryMode;
859
859
  const authoritativeBranch = cleanGitName(renderer?.authoritativeBranch);
package/src/index.js CHANGED
@@ -82,6 +82,7 @@ export {
82
82
  setupExternalReviewerGovernance
83
83
  } from "./external-reviewer.js";
84
84
  export { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
85
+ export { planProgramAmendment } from "./program-amendment.js";
85
86
  export {
86
87
  buildAgentProgramPath,
87
88
  PROGRAM_PATH,
@@ -99,6 +100,8 @@ export {
99
100
  export { searchResources, searchableValues } from "./search.js";
100
101
  export { effectiveResourceStatus } from "./resource-status.js";
101
102
  export { applyReconciliation, planReconciliation } from "./reconciliation.js";
103
+ export { assessRequirementMappingReadiness } from "./requirement-mapping.js";
104
+ export { assessRetentionReadiness, nearDuplicateInformationTypes, resourceReviewRevision, resourceReviewRevisions, retentionReviewResourceIds, retentionRuleIsCurrent, retentionUses } from "./retention.js";
102
105
  export { createFilegrcServer, serveWorkspace } from "./server.js";
103
106
  export { normalizeSetupPayload, planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
104
107
  export { createAppState, createResourceDetail } from "./state.js";