filegrc 0.9.2 → 0.10.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/model/index.js +4 -4
- package/model/v7.json +10359 -0
- package/package.json +1 -1
- package/src/applicability-scope.js +211 -0
- package/src/audit-preparation.js +156 -20
- package/src/batch-review.js +40 -24
- package/src/cli.js +20 -7
- package/src/collection-review.js +16 -3
- package/src/collection-revision.js +23 -3
- package/src/collection-scope.js +82 -7
- package/src/document-activation.js +13 -1
- package/src/model-migration.js +224 -34
- package/src/obligations.js +128 -31
- package/src/policy-activation.js +5 -0
- package/src/program-lifecycle.js +1 -1
- package/src/program-path.js +1 -4
- package/src/program-readiness.js +20 -3
- package/src/reconciliation.js +15 -3
- package/src/server.js +2 -1
- package/src/setup.js +1 -1
- package/src/state.js +2 -1
- package/src/validate.js +25 -7
- package/src/web.js +121 -43
- package/src/workflow.js +7 -6
package/src/cli.js
CHANGED
|
@@ -223,6 +223,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
223
223
|
: `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
|
|
224
224
|
} else {
|
|
225
225
|
console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
|
|
226
|
+
if (result.migrationReportPath) console.log(`Migration report: ${result.migrationReportPath}`);
|
|
226
227
|
}
|
|
227
228
|
return result;
|
|
228
229
|
}
|
|
@@ -261,7 +262,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
261
262
|
const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
|
|
262
263
|
const programId = flags.program || audit?.programId;
|
|
263
264
|
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
|
|
264
|
-
const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
|
|
265
|
+
const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
|
|
266
|
+
auditId,
|
|
267
|
+
asOf: flags["as-of"]
|
|
268
|
+
}) : null;
|
|
265
269
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
266
270
|
const output = selectProgramPathOutput(result, flags);
|
|
267
271
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
@@ -481,7 +485,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
481
485
|
}
|
|
482
486
|
if (command === "audit-readiness") {
|
|
483
487
|
const loaded = await loadWorkspace(root);
|
|
484
|
-
const result = await assessAuditPreparation(loaded, {
|
|
488
|
+
const result = await assessAuditPreparation(loaded, {
|
|
489
|
+
auditId: positionals[0] || flags.audit,
|
|
490
|
+
asOf: flags["as-of"]
|
|
491
|
+
});
|
|
485
492
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
486
493
|
else {
|
|
487
494
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} management items complete`);
|
|
@@ -1115,7 +1122,7 @@ Usage:
|
|
|
1115
1122
|
filegrc build [root] [--output .filegrc/site]
|
|
1116
1123
|
filegrc validate [root] [--json]
|
|
1117
1124
|
filegrc model [--json|--write-docs|--check-docs]
|
|
1118
|
-
filegrc migrate --to-model
|
|
1125
|
+
filegrc migrate --to-model <${SUPPORTED_MODEL_VERSIONS.join("|")}> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
1119
1126
|
filegrc describe <resource-type>
|
|
1120
1127
|
filegrc types [--json]
|
|
1121
1128
|
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
@@ -1129,7 +1136,7 @@ Usage:
|
|
|
1129
1136
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
1130
1137
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
1131
1138
|
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
1132
|
-
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
1139
|
+
filegrc audit-readiness [audit-id] [--as-of YYYY-MM-DD] [--require-ready] [--json]
|
|
1133
1140
|
filegrc prepare-audit <audit-id> [--json]
|
|
1134
1141
|
filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes] [--risk-level normal|high] [--json]
|
|
1135
1142
|
filegrc external-reviewer-setup --scaffold
|
|
@@ -1214,7 +1221,8 @@ workspaces migrate to v2 first. Continue one version at a time through v${ACTIVE
|
|
|
1214
1221
|
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1215
1222
|
specific Assets, Vendors, normalized information, and Evidence Artifacts. Model v5 separates
|
|
1216
1223
|
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.
|
|
1224
|
+
Training the same approval and activation split and moves its schedule into Obligations. Model v7
|
|
1225
|
+
keeps issued historical Documents neutral and requires a current activation for legacy Training. v3 migration
|
|
1218
1226
|
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1219
1227
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1220
1228
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
@@ -1231,7 +1239,8 @@ Options:
|
|
|
1231
1239
|
--help Show this help
|
|
1232
1240
|
|
|
1233
1241
|
Start with:
|
|
1234
|
-
npx filegrc
|
|
1242
|
+
npx filegrc guide --json
|
|
1243
|
+
# Use the next model version shown by the guide.`);
|
|
1235
1244
|
return;
|
|
1236
1245
|
}
|
|
1237
1246
|
if (command === "program-readiness") {
|
|
@@ -1398,6 +1407,10 @@ Options:
|
|
|
1398
1407
|
}
|
|
1399
1408
|
|
|
1400
1409
|
function agentOverview(model) {
|
|
1410
|
+
const currentModelIndex = SUPPORTED_MODEL_VERSIONS.indexOf(String(model.modelVersion));
|
|
1411
|
+
const nextModelVersion = currentModelIndex >= 0 && currentModelIndex < SUPPORTED_MODEL_VERSIONS.length - 1
|
|
1412
|
+
? SUPPORTED_MODEL_VERSIONS[currentModelIndex + 1]
|
|
1413
|
+
: ACTIVE_MODEL_VERSION;
|
|
1401
1414
|
const commands = {
|
|
1402
1415
|
help: "filegrc help",
|
|
1403
1416
|
version: "filegrc version",
|
|
@@ -1406,7 +1419,7 @@ function agentOverview(model) {
|
|
|
1406
1419
|
build: "filegrc build [root]",
|
|
1407
1420
|
validate: "filegrc validate [root] --json",
|
|
1408
1421
|
model: "filegrc model --json",
|
|
1409
|
-
migrate:
|
|
1422
|
+
migrate: `filegrc migrate --to-model ${nextModelVersion} --preview --json`,
|
|
1410
1423
|
describe: "filegrc describe <resource-type>",
|
|
1411
1424
|
types: "filegrc types --json",
|
|
1412
1425
|
guide: "filegrc guide [resource-type] --json",
|
package/src/collection-review.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { modelSupports } from "../model/index.js";
|
|
3
|
-
import {
|
|
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
|
-
&&
|
|
60
|
+
&& revisionMatches
|
|
48
61
|
);
|
|
49
62
|
const stale = Boolean(
|
|
50
63
|
review?.status === "active"
|
|
51
64
|
&& review.collectionRevision
|
|
52
|
-
&&
|
|
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:
|
|
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") }];
|
package/src/collection-scope.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 }));
|
|
@@ -90,8 +146,9 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
|
90
146
|
record,
|
|
91
147
|
value: reviewedIds.has(record.id)
|
|
92
148
|
? record
|
|
93
|
-
: dependencyRevisionValue(resourceType, record),
|
|
94
|
-
includeContent:
|
|
149
|
+
: dependencyRevisionValue(resourceType, record, legacy),
|
|
150
|
+
includeContent: legacy
|
|
151
|
+
|| reviewedIds.has(record.id)
|
|
95
152
|
|| dependencyContentAffectsRevision(resourceType, record.type)
|
|
96
153
|
}));
|
|
97
154
|
}
|
|
@@ -167,7 +224,7 @@ const dependencyFields = {
|
|
|
167
224
|
},
|
|
168
225
|
component: {
|
|
169
226
|
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
170
|
-
control: ["id", "type", "
|
|
227
|
+
control: ["id", "type", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
171
228
|
vendor: ["id", "type", "status", "category", "criticality", "description", "startDate", "endDate"],
|
|
172
229
|
classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
|
|
173
230
|
"information-type": ["id", "type", "status", "classificationId", "description"]
|
|
@@ -183,6 +240,21 @@ const dependencyFields = {
|
|
|
183
240
|
}
|
|
184
241
|
};
|
|
185
242
|
|
|
243
|
+
const legacyDependencyFieldOverrides = {
|
|
244
|
+
framework: {
|
|
245
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"]
|
|
246
|
+
},
|
|
247
|
+
component: {
|
|
248
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
249
|
+
vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"]
|
|
250
|
+
},
|
|
251
|
+
"complementary-control": {
|
|
252
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
253
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
254
|
+
component: ["id", "type", "status", "componentKind", "description", "vendorId", "systemUses", "informationUses", "internetExposed"]
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
186
258
|
const dependencyContentTypes = {
|
|
187
259
|
framework: new Set(["system"]),
|
|
188
260
|
vendor: new Set(["document"]),
|
|
@@ -190,8 +262,11 @@ const dependencyContentTypes = {
|
|
|
190
262
|
"complementary-control": new Set(["system", "control", "document", "component"])
|
|
191
263
|
};
|
|
192
264
|
|
|
193
|
-
function dependencyRevisionValue(resourceType, record) {
|
|
194
|
-
const fields =
|
|
265
|
+
function dependencyRevisionValue(resourceType, record, legacy) {
|
|
266
|
+
const fields = legacy
|
|
267
|
+
? legacyDependencyFieldOverrides[resourceType]?.[record.type]
|
|
268
|
+
|| dependencyFields[resourceType]?.[record.type]
|
|
269
|
+
: dependencyFields[resourceType]?.[record.type];
|
|
195
270
|
if (!fields) return { id: record.id, type: record.type };
|
|
196
271
|
return Object.fromEntries(fields
|
|
197
272
|
.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),
|