filegrc 0.16.8 → 0.16.9

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.8",
3
+ "version": "0.16.9",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1831,6 +1831,9 @@ function item(id, status, title, message, resource = {}, options = {}) {
1831
1831
  message,
1832
1832
  ...(resource.type ? { resourceType: resource.type } : {}),
1833
1833
  ...(resource.id ? { resourceId: resource.id } : {}),
1834
+ ...(resource.type ? { destination: resource.id
1835
+ ? { kind: "record", resourceType: resource.type, resourceId: resource.id }
1836
+ : { kind: "collection", resourceType: resource.type } } : {}),
1834
1837
  ...(options.commands?.length ? { commands: options.commands } : {}),
1835
1838
  ...(options.affectedActionItemIds?.length ? { affectedActionItemIds: options.affectedActionItemIds } : {}),
1836
1839
  ...(options.affectedEventIds?.length ? { affectedEventIds: options.affectedEventIds } : {}),
@@ -774,9 +774,23 @@ export function collectionReviewReadinessItem(assessment) {
774
774
  ? { id: firstIncomplete.resourceId, type: assessment.resourceType }
775
775
  : needsFirstRecord ? { type: assessment.resourceType }
776
776
  : approvalAction ? { type: approvalAction.resourceType, ...(approvalAction.resourceId ? { id: approvalAction.resourceId } : {}) }
777
- : assessment.review || { type: assessment.resourceType },
777
+ : { type: assessment.resourceType },
778
778
  {
779
779
  resourceType: approvalAction?.resourceType || assessment.resourceType,
780
+ ...(!proposalAction && !needsFirstRecord && !approvalAction && assessment.review?.id
781
+ ? { subject: { type: "collection-review", id: assessment.review.id } }
782
+ : {}),
783
+ destination: proposalAction
784
+ ? { kind: "record", resourceType: assessment.resourceType, resourceId: firstIncomplete.resourceId }
785
+ : needsFirstRecord
786
+ ? { kind: "collection", resourceType: assessment.resourceType, create: true }
787
+ : approvalAction
788
+ ? approvalAction.reviewCollection
789
+ ? { kind: "collection-review", resourceType: approvalAction.resourceType }
790
+ : approvalAction.resourceId
791
+ ? { kind: "record", resourceType: approvalAction.resourceType, resourceId: approvalAction.resourceId }
792
+ : { kind: "collection", resourceType: approvalAction.resourceType, ...(approvalAction.createResource ? { create: true, scheduleDocument: true } : {}) }
793
+ : { kind: "collection-review", resourceType: assessment.resourceType },
780
794
  reviewPoints: assessment.configuration.reviewPoints,
781
795
  ...(approvalAction?.createResource ? { createResource: true } : {}),
782
796
  ...(pendingRetentionRow && !proposalAction ? { unresolvedAssignments: [{
@@ -842,6 +856,7 @@ function collectionApprovalIssueAction(assessment) {
842
856
  if (issue.code === "unreviewed-similar-information-types") return {
843
857
  title: "Review similar Information Types",
844
858
  resourceType: "information-type",
859
+ reviewCollection: true,
845
860
  commands: ["npx filegrc list information-type --workflow --json", "npx filegrc review-collection information-type --scaffold > REVIEW.json", "npx filegrc review-collection information-type REVIEW.json --preview --json", "npx filegrc review-collection information-type REVIEW.json --yes --json"]
846
861
  };
847
862
  return {
package/src/web.js CHANGED
@@ -53,6 +53,38 @@ export function pageActionIdentity(href = "", fallback = "") {
53
53
  return route.startsWith("#/resource/") ? route : fallback || href;
54
54
  }
55
55
 
56
+ export function resolveWorkflowDestination(destination, options = {}) {
57
+ if (!destination?.resourceType) return "";
58
+ if (options.resourceTypes && !options.resourceTypes[destination.resourceType]) return "";
59
+ const type = encodeURIComponent(destination.resourceType);
60
+ if (destination.kind === "record") {
61
+ const exists = !options.resources || options.resources.some((entry) => {
62
+ const record = entry.record || entry;
63
+ return record.type === destination.resourceType && record.id === destination.resourceId;
64
+ });
65
+ return destination.resourceId && exists
66
+ ? `#/resource/${type}/${encodeURIComponent(destination.resourceId)}${destination.edit ? "?edit=1" : ""}`
67
+ : resolveWorkflowDestination({ kind: "collection", resourceType: destination.resourceType }, options);
68
+ }
69
+ if (destination.kind === "collection-review") {
70
+ if (destination.resourceType === "retention-schedule-item" && options.retentionScheduleApproval) {
71
+ return "#/retention-schedule";
72
+ }
73
+ return `#/resources/${type}?review-collection=1`;
74
+ }
75
+ if (destination.kind === "collection") {
76
+ const create = destination.create && !options.readOnly;
77
+ if (destination.resourceType === "retention-schedule-item" && options.retentionScheduleApproval) {
78
+ return create ? "#/retention-schedule?new=1" : "#/retention-schedule";
79
+ }
80
+ if (destination.resourceType === "document" && create && destination.scheduleDocument) {
81
+ return "#/resources/document?new=1&documentScope=program&documentKind=schedule&programRole=required&title=Data%20Retention%20Schedule";
82
+ }
83
+ return `#/resources/${type}${create ? "?new=1" : ""}`;
84
+ }
85
+ return "";
86
+ }
87
+
56
88
  export function renderIndex(state = null) {
57
89
  const snapshot = state
58
90
  ? `<script id="filegrc-data" type="application/json">${safeJson(state)}</script>`
@@ -100,6 +132,7 @@ const RESOURCE_GUIDE_OUTPUTS = ${JSON.stringify(RESOURCE_OUTPUTS)};
100
132
  const dashboardProgramReadiness = ${dashboardProgramReadiness.toString()};
101
133
  const workflowHrefBelongsToPage = ${workflowHrefBelongsToPage.toString()};
102
134
  const pageActionIdentity = ${pageActionIdentity.toString()};
135
+ const resolveWorkflowDestination = ${resolveWorkflowDestination.toString()};
103
136
  const selectDefaultAudit = ${selectDefaultAudit.toString()};
104
137
  const STAGE_PAGE_SUMMARIES = ${JSON.stringify({
105
138
  ...RESOURCE_PAGE_SUMMARIES,
@@ -786,11 +819,7 @@ function renderRetentionScheduleIssues() {
786
819
  ));
787
820
  if (!actions.length) return "";
788
821
  return '<section class="retention-issues"><div class="section-head"><div><p class="kicker">Before approval</p><h2>Items to resolve</h2></div></div><div class="retention-issue-list">' + actions.map((item) => {
789
- const href = item.href || (item.id?.startsWith("retention-use-")
790
- ? retentionScheduleItemHref(item)
791
- : item.resourceId && item.resourceType
792
- ? '#/resource/' + encodeURIComponent(item.resourceType) + '/' + encodeURIComponent(item.resourceId)
793
- : '#/resources/' + encodeURIComponent(item.resourceType || "retention-schedule-item"));
822
+ const href = item.href || workflowItemHref(item) || "#/retention-schedule";
794
823
  return '<a href="' + href + '"><span><strong>' + esc(item.title) + '</strong><small>' + esc(item.message) + '</small></span><b>Review</b></a>';
795
824
  }).join("") + '</div></section>';
796
825
  }
@@ -1286,9 +1315,7 @@ function renderFinishStepThree() {
1286
1315
  ));
1287
1316
  const next = implementationItems.find(({ status }) => status !== "complete");
1288
1317
  if (!oversightItem) {
1289
- const href = next?.id?.startsWith("collection-review-") && next.resourceType
1290
- ? "#/resources/" + encodeURIComponent(next.resourceType) + "?review-collection=1"
1291
- : next?.id?.startsWith("source-family-")
1318
+ const href = next?.id?.startsWith("source-family-")
1292
1319
  ? "#/evidence-sources"
1293
1320
  : next ? workflowItemHref({ ...next, state: next.status }) || "#/resources/control" : "#/resources/control";
1294
1321
  return '<section class="finish-step panel blocked"><div><p class="kicker">Finish Step 3</p><h2>Complete implementation first</h2><p>' + esc(next?.message || "Finish the Step 3 work areas before the final review becomes available.") + '</p></div><a class="button primary" href="' + esc(href) + '">Continue implementation</a></section>';
@@ -1490,6 +1517,16 @@ function openCollectionReviewDialog(type) {
1490
1517
  }
1491
1518
 
1492
1519
  function workflowItemHref(item) {
1520
+ const destinationOptions = {
1521
+ retentionScheduleApproval: modelSupports("retention-schedule-approval"),
1522
+ readOnly: state.readOnly,
1523
+ resourceTypes: state.model.resources,
1524
+ resources: state.resources
1525
+ };
1526
+ const destinationHref = (destination) => resolveWorkflowDestination(destination, destinationOptions);
1527
+ if (item.destination) {
1528
+ return destinationHref(item.destination);
1529
+ }
1493
1530
  const source = item.source?.id && item.source?.type && item.source.type !== "unknown"
1494
1531
  ? item.source
1495
1532
  : null;
@@ -1518,19 +1555,18 @@ function workflowItemHref(item) {
1518
1555
  && item.subject?.id
1519
1556
  && state.resources.some(({ record }) => record.type === applicabilityType && record.id === item.subject.id)
1520
1557
  ) {
1521
- return "#/resource/" + encodeURIComponent(applicabilityType) + "/" + encodeURIComponent(item.subject.id);
1558
+ return destinationHref({ kind: "record", resourceType: applicabilityType, resourceId: item.subject.id });
1522
1559
  }
1523
1560
  if (commands.some((command) => command.includes(" scaffold retention-schedule-item"))) {
1524
1561
  return retentionScheduleItemHref(item);
1525
1562
  }
1526
1563
  if (applicabilityType && state.model.resources[applicabilityType]) {
1527
- return "#/resources/" + encodeURIComponent(applicabilityType) + "?review=1";
1564
+ return destinationHref({ kind: "collection", resourceType: applicabilityType }) + "?review=1";
1528
1565
  }
1529
1566
  const collectionReviewCommand = commands.find((command) => command.includes(" review-collection "));
1530
1567
  const collectionReviewType = collectionReviewCommand?.match(/review-collection\s+([a-z0-9-]+)/)?.[1];
1531
1568
  if (collectionReviewType && state.model.collectionReviews?.[collectionReviewType]) {
1532
- if (collectionReviewType === "retention-schedule-item" && modelSupports("retention-schedule-approval")) return "#/retention-schedule";
1533
- return "#/resources/" + encodeURIComponent(collectionReviewType) + "?review-collection=1";
1569
+ return destinationHref({ kind: "collection-review", resourceType: collectionReviewType });
1534
1570
  }
1535
1571
  if (
1536
1572
  commands.some((command) => command.includes(" get ") && command.includes(" --mutation"))
@@ -1538,25 +1574,25 @@ function workflowItemHref(item) {
1538
1574
  && item.subject?.type
1539
1575
  && state.model.resources[item.subject.type]
1540
1576
  ) {
1541
- return "#/resource/" + encodeURIComponent(item.subject.type) + "/" + encodeURIComponent(item.subject.id) + "?edit=1";
1577
+ return destinationHref({ kind: "record", resourceType: item.subject.type, resourceId: item.subject.id, edit: true });
1542
1578
  }
1543
1579
  if (commands.some((command) => command.includes(" external-reviewer-setup"))) {
1544
1580
  const reviewer = resourcesOfType("appointment")
1545
1581
  .find(({ record }) => record.appointmentKind === "independent-policy-reviewer");
1546
- if (reviewer) return "#/resource/appointment/" + encodeURIComponent(reviewer.record.id);
1582
+ if (reviewer) return destinationHref({ kind: "record", resourceType: "appointment", resourceId: reviewer.record.id });
1547
1583
  }
1548
1584
  if (commands.some((command) => command.includes(" evidence-map"))) return "#/stage/controls";
1549
1585
  const reference = item.subject?.id && item.subject?.type
1550
1586
  ? item.subject
1551
1587
  : source;
1552
1588
  if (reference && reference.type !== "unknown") {
1553
- return "#/resource/" + encodeURIComponent(reference.type) + "/" + encodeURIComponent(reference.id);
1589
+ return destinationHref({ kind: "record", resourceType: reference.type, resourceId: reference.id });
1554
1590
  }
1555
1591
  const missingType = item.subject?.type && state.model.resources[item.subject.type]
1556
1592
  ? item.subject.type
1557
1593
  : null;
1558
1594
  if (missingType) {
1559
- return "#/resources/" + encodeURIComponent(missingType) + (state.readOnly ? "" : "?new=1");
1595
+ return destinationHref({ kind: "collection", resourceType: missingType, create: !state.readOnly });
1560
1596
  }
1561
1597
  if (item.auditId) return "#/audit-packet?auditId=" + encodeURIComponent(item.auditId);
1562
1598
  const stage = {
@@ -1714,13 +1750,9 @@ function retentionSchedulePageItems() {
1714
1750
  ...item,
1715
1751
  key: item.id,
1716
1752
  state: "ready",
1717
- href: item.id === "collection-review-retention-schedule-item"
1718
- ? retentionCollectionActionHref(item)
1719
- : item.id?.startsWith("retention-use-")
1753
+ href: item.id?.startsWith("retention-use-")
1720
1754
  ? retentionScheduleItemHref(item)
1721
- : item.resourceId && item.resourceType
1722
- ? '#/resource/' + encodeURIComponent(item.resourceType) + '/' + encodeURIComponent(item.resourceId)
1723
- : workflowItemHref(item) || "#/retention-schedule",
1755
+ : workflowItemHref(item) || "#/retention-schedule",
1724
1756
  subject: item.resourceId && item.resourceType
1725
1757
  ? { type: item.resourceType, id: item.resourceId }
1726
1758
  : item.subject
@@ -1728,19 +1760,6 @@ function retentionSchedulePageItems() {
1728
1760
  ]);
1729
1761
  }
1730
1762
 
1731
- function retentionCollectionActionHref(item) {
1732
- if (item.resourceType === "document") return item.resourceId
1733
- ? '#/resource/document/' + encodeURIComponent(item.resourceId) + '?stage=policies'
1734
- : item.createResource
1735
- ? '#/resources/document?new=1&documentScope=program&documentKind=schedule&programRole=required&title=Data%20Retention%20Schedule'
1736
- : '#/resources/document';
1737
- if (item.resourceType === "information-type") return '#/resources/information-type?review-collection=1';
1738
- if (item.resourceType === "retention-schedule-item" && item.resourceId && item.resourceId !== item.id) {
1739
- return '#/resource/retention-schedule-item/' + encodeURIComponent(item.resourceId) + '?stage=policies';
1740
- }
1741
- return "#/retention-schedule";
1742
- }
1743
-
1744
1763
  function distinctPageActionItems(items) {
1745
1764
  const distinct = new Map();
1746
1765
  for (const item of items) {
@@ -3017,11 +3036,7 @@ function renderAuditPreparation(preparation) {
3017
3036
  const status = preparation.status === "management-ready" ? "Management work complete" : preparation.counts.action + " actions remaining";
3018
3037
  const stages = preparation.stages.map((stage) => {
3019
3038
  const items = stage.items.map((item) => {
3020
- const href = item.resourceId
3021
- ? '#/resource/' + encodeURIComponent(item.resourceType) + '/' + encodeURIComponent(item.resourceId)
3022
- : item.resourceType
3023
- ? '#/resources/' + encodeURIComponent(item.resourceType)
3024
- : "";
3039
+ const href = workflowItemHref(item);
3025
3040
  const content = '<span class="preparation-status ' + esc(item.status) + '" aria-hidden="true">' + preparationStatusMark(item.status) + '</span><span><strong>' + esc(item.title) + '</strong><small>' + esc(item.message) + '</small></span>';
3026
3041
  return href ? '<a href="' + href + '">' + content + '</a>' : '<div>' + content + '</div>';
3027
3042
  }).join("");
package/src/workflow.js CHANGED
@@ -1115,9 +1115,9 @@ export function packetDeliveryIssue(delivery) {
1115
1115
 
1116
1116
  function normalizeFinding(code, item, context) {
1117
1117
  const state = findingState(item.status);
1118
- const subject = item.resourceType
1118
+ const subject = item.subject || (item.resourceType
1119
1119
  ? { type: item.resourceType, ...(item.resourceId ? { id: item.resourceId } : {}) }
1120
- : null;
1120
+ : null);
1121
1121
  const dependencies = (item.unresolvedAssignments || []).map((assignment) => ({
1122
1122
  type: assignment.resourceType,
1123
1123
  id: assignment.resourceId,
@@ -1138,6 +1138,7 @@ function normalizeFinding(code, item, context) {
1138
1138
  .filter(([, passed]) => passed === false)
1139
1139
  .map(([name]) => name) } : {}),
1140
1140
  ...(item.resourceId ? { resourceId: item.resourceId } : {}),
1141
+ ...(item.destination ? { destination: item.destination } : {}),
1141
1142
  ...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
1142
1143
  ...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),
1143
1144
  ...(item.informationTypeId ? { informationTypeId: item.informationTypeId } : {}),