filegrc 0.16.1 → 0.16.2
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 +1 -1
- package/src/cli.js +5 -1
- package/src/collection-scope.js +18 -0
- package/src/program-readiness.js +48 -1
- package/src/web.js +7 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1846,7 +1846,11 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1846
1846
|
nextActions: (current?.items || []).filter((item) => item.status === "action")
|
|
1847
1847
|
};
|
|
1848
1848
|
});
|
|
1849
|
-
const
|
|
1849
|
+
const recommendedStage = readiness.stages.find((stage) => stage.items.includes(readiness.firstAction));
|
|
1850
|
+
const recommendedId = recommendedStage?.id === "operation" ? "run" : recommendedStage?.id;
|
|
1851
|
+
const currentStep = stages.find(({ id }) => id === recommendedId)
|
|
1852
|
+
|| stages.find((stage) => !["complete", "operating", "management-ready"].includes(stage.status))
|
|
1853
|
+
|| stages.at(-1);
|
|
1850
1854
|
return {
|
|
1851
1855
|
schemaVersion: 1,
|
|
1852
1856
|
dataModelVersion: String(model.modelVersion),
|
package/src/collection-scope.js
CHANGED
|
@@ -65,6 +65,24 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
|
65
65
|
));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// These records can change which members enter a collection even when they
|
|
69
|
+
// are not part of its current revision inputs yet.
|
|
70
|
+
const COLLECTION_MEMBERSHIP_SOURCES = {
|
|
71
|
+
person: ["workspace", "program", "appointment", "team", "system", "component", "vendor", "control", "policy", "document", "obligation"],
|
|
72
|
+
vendor: ["program", "system", "component", "audit"],
|
|
73
|
+
component: ["program", "system", "control"],
|
|
74
|
+
"complementary-control": ["program", "system", "component", "control"],
|
|
75
|
+
"information-type": ["program", "system", "component", "vendor"],
|
|
76
|
+
"retention-schedule-item": ["program", "system", "component", "vendor", "control", "information-type", "source-coverage"],
|
|
77
|
+
framework: ["program"],
|
|
78
|
+
system: ["program"],
|
|
79
|
+
control: ["program"]
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function collectionMembershipSourceTypes(resourceType) {
|
|
83
|
+
return COLLECTION_MEMBERSHIP_SOURCES[resourceType] || [];
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
export function retentionScheduleReviewScope(loaded) {
|
|
69
87
|
const programs = loaded.resources.filter((record) => (
|
|
70
88
|
record.type === "program" && record.status !== "retired"
|
package/src/program-readiness.js
CHANGED
|
@@ -3,6 +3,7 @@ import { modelSupports } from "../model/index.js";
|
|
|
3
3
|
import { applicabilityReviewIsCurrent } from "./applicability-scope.js";
|
|
4
4
|
import { assessRequiredAppointments } from "./appointments.js";
|
|
5
5
|
import { assessCollectionReviews } from "./collection-review.js";
|
|
6
|
+
import { collectionMembershipSourceTypes, collectionRevisionInputs, collectionScopeRevisionFacts } from "./collection-scope.js";
|
|
6
7
|
import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
|
|
7
8
|
import { coverageEnd, coverageStart } from "./coverage.js";
|
|
8
9
|
import { planObligations } from "./obligations.js";
|
|
@@ -233,6 +234,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
233
234
|
policyStage,
|
|
234
235
|
controlStage
|
|
235
236
|
];
|
|
237
|
+
const prioritizedSetupItems = prioritizeReviewDependencies(evidenceGateStages, loaded, program);
|
|
236
238
|
for (const current of evidenceGateStages) finalizeStage(current);
|
|
237
239
|
const evidenceReady = evidenceGateStages.every((current) => current.counts.action === 0);
|
|
238
240
|
const stages = [
|
|
@@ -260,7 +262,8 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
260
262
|
const items = stages.flatMap((current) => current.items);
|
|
261
263
|
const managedItems = items.filter((current) => !["info", "later"].includes(current.status));
|
|
262
264
|
const complete = managedItems.filter((current) => current.status === "complete").length;
|
|
263
|
-
const firstAction =
|
|
265
|
+
const firstAction = [...prioritizedSetupItems, ...stages.at(-1).items]
|
|
266
|
+
.find((current) => current.status === "action") || null;
|
|
264
267
|
const target = {
|
|
265
268
|
programId: program?.id || null,
|
|
266
269
|
goal: program?.assuranceGoal || "none",
|
|
@@ -1966,6 +1969,50 @@ function finalizeStage(current) {
|
|
|
1966
1969
|
current.status = current.counts.action ? "action" : current.counts.later ? "later" : "complete";
|
|
1967
1970
|
}
|
|
1968
1971
|
|
|
1972
|
+
// A collection confirmation can be performed early, but an unfinished input
|
|
1973
|
+
// would make it stale again. Derive the ordering from the same revision inputs
|
|
1974
|
+
// used to decide whether that confirmation is current.
|
|
1975
|
+
export function prioritizeReviewDependencies(stages, loaded, program) {
|
|
1976
|
+
const items = stages.flatMap(({ items }) => items);
|
|
1977
|
+
const edges = new Map(items.map((_, index) => [index, new Set()]));
|
|
1978
|
+
const indegree = items.map(() => 0);
|
|
1979
|
+
for (const [reviewIndex, review] of items.entries()) {
|
|
1980
|
+
if (review.status !== "action" || !review.id.startsWith("collection-review-")) continue;
|
|
1981
|
+
const resourceType = review.id.slice("collection-review-".length);
|
|
1982
|
+
if (!loaded.model.collectionReviews?.[resourceType]) continue;
|
|
1983
|
+
const inputTypes = new Set(collectionRevisionInputs(loaded, resourceType, program)
|
|
1984
|
+
.map(({ record }) => record.type));
|
|
1985
|
+
for (const type of collectionMembershipSourceTypes(resourceType)) inputTypes.add(type);
|
|
1986
|
+
const scopeFacts = collectionScopeRevisionFacts(loaded, resourceType, program);
|
|
1987
|
+
if (Object.keys(scopeFacts).some((key) => key !== "programId")) inputTypes.add("program");
|
|
1988
|
+
for (const key of Object.keys(scopeFacts)) {
|
|
1989
|
+
if (!key.endsWith("Ids")) continue;
|
|
1990
|
+
const type = key.slice(0, -3).replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1991
|
+
if (loaded.model.resources?.[type]) inputTypes.add(type);
|
|
1992
|
+
}
|
|
1993
|
+
for (const [sourceIndex, source] of items.entries()) {
|
|
1994
|
+
if (sourceIndex === reviewIndex || source.status !== "action"
|
|
1995
|
+
|| source.id.startsWith("collection-review-")
|
|
1996
|
+
|| !inputTypes.has(source.resourceType)) continue;
|
|
1997
|
+
edges.get(sourceIndex).add(reviewIndex);
|
|
1998
|
+
indegree[reviewIndex]++;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
const remaining = new Set(items.map((_, index) => index));
|
|
2002
|
+
const ordered = [];
|
|
2003
|
+
while (remaining.size) {
|
|
2004
|
+
const next = [...remaining].find((index) => indegree[index] === 0);
|
|
2005
|
+
if (next === undefined) break;
|
|
2006
|
+
remaining.delete(next);
|
|
2007
|
+
ordered.push(items[next]);
|
|
2008
|
+
for (const dependent of edges.get(next)) indegree[dependent]--;
|
|
2009
|
+
}
|
|
2010
|
+
if (ordered.length !== items.length) return items;
|
|
2011
|
+
const rank = new Map(ordered.map((item, index) => [item, index]));
|
|
2012
|
+
for (const stage of stages) stage.items.sort((left, right) => rank.get(left) - rank.get(right));
|
|
2013
|
+
return ordered;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1969
2016
|
function item(id, status, title, message, resource = {}, details = {}) {
|
|
1970
2017
|
return {
|
|
1971
2018
|
id,
|
package/src/web.js
CHANGED
|
@@ -589,6 +589,13 @@ function readinessOverview() {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
function nextProgramStageHref() {
|
|
592
|
+
const firstAction = state.programReadiness?.firstAction;
|
|
593
|
+
const recommendedStage = state.programReadiness?.stages?.find((stage) => stage.items.some((item) => (
|
|
594
|
+
item.id === firstAction?.id && item.resourceId === firstAction?.resourceId
|
|
595
|
+
)));
|
|
596
|
+
if (recommendedStage) {
|
|
597
|
+
return "#/stage/" + encodeURIComponent(recommendedStage.id === "operation" ? "run" : recommendedStage.id);
|
|
598
|
+
}
|
|
592
599
|
const nextStage = READINESS_STAGES.find((stage) => {
|
|
593
600
|
const progress = stageProgress(stage);
|
|
594
601
|
return progress.complete < progress.total;
|