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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
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 currentStep = stages.find((stage) => !["complete", "operating", "management-ready"].includes(stage.status)) || stages.at(-1);
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),
@@ -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"
@@ -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 = items.find((current) => current.status === "action") || null;
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;