filegrc 0.13.5 → 0.14.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/README.md +4 -4
- package/model/index.js +6 -5
- package/model/v11.json +12200 -0
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/collection-review.js +59 -0
- package/src/collection-scope.js +106 -3
- package/src/document-activation.js +28 -4
- package/src/model-migration.js +70 -2
- package/src/policy-activation.js +16 -2
- package/src/program-path.js +6 -1
- package/src/program-readiness.js +21 -10
- package/src/validate.js +24 -1
- package/src/web.js +90 -10
- package/src/workflow.js +1 -3
package/src/web.js
CHANGED
|
@@ -72,6 +72,7 @@ const RESOURCE_GUIDE_INSTRUCTIONS = ${JSON.stringify(RESOURCE_INSTRUCTIONS)};
|
|
|
72
72
|
const dashboardProgramReadiness = ${dashboardProgramReadiness.toString()};
|
|
73
73
|
const STAGE_PAGE_SUMMARIES = ${JSON.stringify({
|
|
74
74
|
...RESOURCE_PAGE_SUMMARIES,
|
|
75
|
+
"utility:evidence-sources": "Check that each Control has an authoritative, retrievable evidence source.",
|
|
75
76
|
"utility:audit-packet": "Review fieldwork readiness and build the indexed evidence packet."
|
|
76
77
|
})};
|
|
77
78
|
const RECORD_TEXT_FIELDS = new Set(["description", "statement", "activity", "purpose", "scope", "objective", "applicabilityRationale", "summary", "rationale", "businessPurpose", "changeSummary", "decisionSummary", "decisionRationale", "recommendation", "remediationPlan", "auditorNotes", "notPerformedReason"]);
|
|
@@ -175,6 +176,7 @@ function render() {
|
|
|
175
176
|
else if (waitingFor.length) renderStateLoading(main, route, waitingFor);
|
|
176
177
|
else if (route.name === "home") renderHome(main);
|
|
177
178
|
else if (route.name === "stage") renderStageOverview(main, route.stageId, route.params);
|
|
179
|
+
else if (route.name === "evidence-sources") renderEvidenceSourcesPage(main);
|
|
178
180
|
else if (route.name === "obligations") renderObligations(main, route.params);
|
|
179
181
|
else if (route.name === "audit-packet") renderAuditPacket(main, route.params);
|
|
180
182
|
else if (route.name === "list") renderList(main, route.type, route.params);
|
|
@@ -205,6 +207,7 @@ function blockingStateSections(route) {
|
|
|
205
207
|
if (route.name === "repository") return ["repository"];
|
|
206
208
|
if (route.name === "obligations" || route.name === "stage" && route.stageId === "run") return ["program", "obligations"];
|
|
207
209
|
if (route.name === "audit-packet") return ["repository", "program", "obligations", "audits"];
|
|
210
|
+
if (route.name === "evidence-sources") return ["program", "workflow"];
|
|
208
211
|
if (route.name === "stage" && route.stageId === "audit") return ["program", "workflow", "audits"];
|
|
209
212
|
if (route.name === "stage") return ["program", "workflow"];
|
|
210
213
|
return [];
|
|
@@ -282,6 +285,7 @@ function parseRoute() {
|
|
|
282
285
|
if (!parts.length) return { name: "home" };
|
|
283
286
|
if (parts.length === 2 && parts[0] === "stage" && parts[1]) return { name: "stage", stageId: parts[1], params: new URLSearchParams(query) };
|
|
284
287
|
if (parts.length === 1 && parts[0] === "obligations") return { name: "obligations", params: new URLSearchParams(query) };
|
|
288
|
+
if (parts.length === 1 && parts[0] === "evidence-sources") return { name: "evidence-sources", params: new URLSearchParams(query) };
|
|
285
289
|
if (parts.length === 1 && parts[0] === "audit-packet") return { name: "audit-packet", params: new URLSearchParams(query) };
|
|
286
290
|
if (parts.length === 2 && parts[0] === "resources" && parts[1]) return { name: "list", type: parts[1], params: new URLSearchParams(query) };
|
|
287
291
|
if (parts.length === 3 && parts[0] === "resource" && parts[1] && parts[2]) return { name: "detail", type: parts[1], id: parts[2], params: new URLSearchParams(query) };
|
|
@@ -302,7 +306,8 @@ function buildNavigation(route) {
|
|
|
302
306
|
const sectionCurrent = (route.type && section.types.includes(route.type) && (!contextualStageId || contextualStageId === stage.id))
|
|
303
307
|
|| (section.relatedLinks || []).some((link) => route.type === link.type && contextualStageId === stage.id)
|
|
304
308
|
|| (section.utility === "obligation-board" && route.name === "obligations")
|
|
305
|
-
|| (section.utility === "audit-packet" && route.name === "audit-packet")
|
|
309
|
+
|| (section.utility === "audit-packet" && route.name === "audit-packet")
|
|
310
|
+
|| (section.utility === "evidence-sources" && route.name === "evidence-sources");
|
|
306
311
|
const sectionOpen = navigationGroupState[sectionKey] ?? (sectionCurrent || section.defaultOpen);
|
|
307
312
|
const resources = section.types
|
|
308
313
|
.map((type) => [type, state.model.resources[type]])
|
|
@@ -339,7 +344,8 @@ function readinessStageForRoute(route) {
|
|
|
339
344
|
(stage.supportingResourceTypes || []).includes(route.type)
|
|
340
345
|
|| stage.sections.some((section) => section.types.includes(route.type)
|
|
341
346
|
|| (section.utility === "obligation-board" && route.name === "obligations")
|
|
342
|
-
|| (section.utility === "audit-packet" && route.name === "audit-packet")
|
|
347
|
+
|| (section.utility === "audit-packet" && route.name === "audit-packet")
|
|
348
|
+
|| (section.utility === "evidence-sources" && route.name === "evidence-sources"))
|
|
343
349
|
));
|
|
344
350
|
}
|
|
345
351
|
|
|
@@ -366,6 +372,9 @@ function reviewedRequirementIds() {
|
|
|
366
372
|
function renderSidebarUtility(utility, route, direct = false) {
|
|
367
373
|
const directClass = direct ? "nav-direct " : "";
|
|
368
374
|
if (utility === "obligation-board") return "";
|
|
375
|
+
if (utility === "evidence-sources") {
|
|
376
|
+
return '<a class="' + directClass + (route.name === "evidence-sources" ? "current" : "") + '" href="#/evidence-sources"><span>Evidence Sources</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
|
|
377
|
+
}
|
|
369
378
|
if (utility === "audit-packet") {
|
|
370
379
|
return '<a class="' + directClass + 'audit-packet-link ' + (route.name === "audit-packet" ? "current" : "") + '" href="#/audit-packet"><span>Audit Evidence & Packet</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
|
|
371
380
|
}
|
|
@@ -383,8 +392,10 @@ function topbar(route) {
|
|
|
383
392
|
? "Repository"
|
|
384
393
|
: route.name === "obligations"
|
|
385
394
|
? "Work Queue"
|
|
386
|
-
|
|
395
|
+
: route.name === "audit-packet"
|
|
387
396
|
? "Audit Readiness"
|
|
397
|
+
: route.name === "evidence-sources"
|
|
398
|
+
? "Evidence Sources"
|
|
388
399
|
: route.name === "list" && route.type === "document"
|
|
389
400
|
? documentListTitle(route.params)
|
|
390
401
|
: state.model.resources[route.type]?.pluralTitle || "filegrc";
|
|
@@ -528,7 +539,7 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
528
539
|
const progress = stageProgress(stage);
|
|
529
540
|
main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
530
541
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
|
|
531
|
-
(stage.id === "policies" ? renderPolicyApprovalGuidance() + renderPoliciesTable() : renderStagePageIndex(stage)) + (stage.id === "controls" ?
|
|
542
|
+
(stage.id === "policies" ? renderPolicyApprovalGuidance() + renderPoliciesTable() : renderStagePageIndex(stage)) + (stage.id === "controls" ? renderFinishStepThree() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
|
|
532
543
|
main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
|
|
533
544
|
main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
|
|
534
545
|
event.currentTarget.remove();
|
|
@@ -540,6 +551,14 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
540
551
|
});
|
|
541
552
|
}
|
|
542
553
|
|
|
554
|
+
function renderEvidenceSourcesPage(main) {
|
|
555
|
+
main.innerHTML = '<div class="page"><nav class="breadcrumbs"><a href="#/stage/controls">Step 3</a><span>/</span><span>Evidence Sources</span></nav><section class="stage-overview-hero"><div><p class="kicker">Step 3.3</p><h2>Evidence Sources</h2><p>Check that each Control points to an authoritative Component that people can use to retrieve its expected evidence.</p></div></section>' + renderEvidenceReadiness() + '</div>';
|
|
556
|
+
main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
|
|
557
|
+
main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
|
|
558
|
+
event.currentTarget.remove();
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
543
562
|
function renderPolicyApprovalGuidance() {
|
|
544
563
|
const proposals = state.programReadiness?.policyLibraryProposals || [];
|
|
545
564
|
const proposalRows = proposals.map((proposal) => {
|
|
@@ -892,9 +911,9 @@ function recordCompletionState(record) {
|
|
|
892
911
|
return '<section class="record-completion-state panel detail-support-panel"><p class="kicker">Record status</p><h3>' + esc(properCase(record.status)) + '</h3><p>This record has no direct next step.</p></section>';
|
|
893
912
|
}
|
|
894
913
|
|
|
895
|
-
function collectionReviewPanel(type) {
|
|
914
|
+
function collectionReviewPanel(type, force = false) {
|
|
896
915
|
const assessment = state.collectionReviews?.[type];
|
|
897
|
-
if (!assessment) return "";
|
|
916
|
+
if (!assessment || (!force && !collectionReviewVisible(type))) return "";
|
|
898
917
|
const configuration = assessment.configuration;
|
|
899
918
|
const current = assessment.status === "current";
|
|
900
919
|
const needsFirstRecord = collectionNeedsFirstRecord(type);
|
|
@@ -923,6 +942,44 @@ function collectionReviewPanel(type) {
|
|
|
923
942
|
(current ? "" : '<div class="collection-review-foot">' + reviewSummary + action + '</div>') + '</section>';
|
|
924
943
|
}
|
|
925
944
|
|
|
945
|
+
function collectionReviewVisible(type) {
|
|
946
|
+
return type !== "control";
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderFinishStepThree() {
|
|
950
|
+
const stage = state.programReadiness?.stages?.find(({ id }) => id === "controls");
|
|
951
|
+
const items = stage?.items || [];
|
|
952
|
+
const oversightItem = items.find(({ id }) => id === "collection-review-control");
|
|
953
|
+
const oversight = state.collectionReviews?.control;
|
|
954
|
+
const activationItems = items.filter(({ id }) => (
|
|
955
|
+
id.startsWith("document-activation-")
|
|
956
|
+
|| id.startsWith("training-activation-")
|
|
957
|
+
|| id.startsWith("policy-activation-")
|
|
958
|
+
));
|
|
959
|
+
const implementationItems = items.filter(({ id }) => (
|
|
960
|
+
id !== "collection-review-control"
|
|
961
|
+
&& !id.startsWith("document-activation-")
|
|
962
|
+
&& !id.startsWith("training-activation-")
|
|
963
|
+
&& !id.startsWith("policy-activation-")
|
|
964
|
+
));
|
|
965
|
+
const next = implementationItems.find(({ status }) => status !== "complete");
|
|
966
|
+
if (!oversightItem) {
|
|
967
|
+
const href = next?.id?.startsWith("collection-review-") && next.resourceType
|
|
968
|
+
? "#/resources/" + encodeURIComponent(next.resourceType) + "?review-collection=1"
|
|
969
|
+
: next?.id?.startsWith("source-family-")
|
|
970
|
+
? "#/evidence-sources"
|
|
971
|
+
: next ? workflowItemHref({ ...next, state: next.status }) || "#/resources/control" : "#/resources/control";
|
|
972
|
+
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>';
|
|
973
|
+
}
|
|
974
|
+
if (!oversight?.complete) {
|
|
975
|
+
return '<section class="finish-step-heading"><p class="kicker">Finish Step 3</p><h2>Review the implemented Control collection</h2><p>One eligible reviewer confirms the implemented Controls as a batch. Management activates the approved program content after this review.</p></section>' + collectionReviewPanel("control", true);
|
|
976
|
+
}
|
|
977
|
+
if (activationItems.some(({ status }) => status !== "complete")) {
|
|
978
|
+
return '<section class="finish-step-heading"><p class="kicker">Finish Step 3</p><h2>Program Content Activation</h2><p>The Control collection review is current. Activate the unchanged approved program content to complete the implementation cutover.</p></section>' + renderDocumentActivationAssessments() + renderPolicyActivationAssessments();
|
|
979
|
+
}
|
|
980
|
+
return '<section class="finish-step panel complete"><div><p class="kicker">Step 3 complete</p><h2>Implementation cutover complete</h2><p>The Control collection review is current and required program content is active.</p></div><a class="button primary" href="#/stage/run">Continue to Step 4</a></section>';
|
|
981
|
+
}
|
|
982
|
+
|
|
926
983
|
function collectionNeedsFirstRecord(type) {
|
|
927
984
|
const assessment = state.collectionReviews?.[type];
|
|
928
985
|
if (!assessment || assessment.recordCount) return false;
|
|
@@ -1007,7 +1064,10 @@ function openCollectionReviewDialog(type) {
|
|
|
1007
1064
|
const assessment = state.collectionReviews?.[type];
|
|
1008
1065
|
if (!assessment) return;
|
|
1009
1066
|
const configuration = assessment.configuration;
|
|
1010
|
-
const
|
|
1067
|
+
const eligibleReviewerIds = type === "control" ? new Set(assessment.eligibleReviewerIds || []) : null;
|
|
1068
|
+
const people = resourcesOfType("person").filter(({ record }) => (
|
|
1069
|
+
record.status === "active" && (!eligibleReviewerIds || eligibleReviewerIds.has(record.id))
|
|
1070
|
+
));
|
|
1011
1071
|
const v4 = modelSupports("program-scope");
|
|
1012
1072
|
const sourceType = v4 ? "component" : "system";
|
|
1013
1073
|
const systems = resourcesOfType(sourceType).filter(({ record }) => record.status === "active");
|
|
@@ -1031,7 +1091,10 @@ function openCollectionReviewDialog(type) {
|
|
|
1031
1091
|
const dialog = document.createElement("dialog");
|
|
1032
1092
|
dialog.className = "commit-dialog event-dialog collection-review-dialog";
|
|
1033
1093
|
dialog.setAttribute("aria-labelledby", "collection-review-dialog-title");
|
|
1034
|
-
|
|
1094
|
+
const reviewerHelp = type === "control" && !people.length
|
|
1095
|
+
? '<p class="policy-activation-warning">No eligible reviewer is configured. Add an active external Person who does not own these Controls or their enabled Obligations.</p>'
|
|
1096
|
+
: "";
|
|
1097
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Scope confirmation</p><h2 id="collection-review-dialog-title">Confirm ' + esc(configuration.title.toLowerCase()) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>' + esc(configuration.description) + '</p><section class="event-dialog-steps collection-review-checks"><strong>Before confirming</strong><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section>' + reviewerHelp + '<div class="form-grid"><label><span>Conclusion</span><select name="decision" required>' + decisions + '</select></label><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + ((assessment.review?.reviewedByIds || []).includes(record.id) ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(currentDate()) + '"></label><label data-authoritative-system><span>Authoritative ' + (v4 ? "Component" : "System") + '</span><select name="authoritativeSourceId"><option value="">Select</option>' + systems.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (preservedAuthoritativeSourceId === record.id ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label class="full"><span>Review notes</span><textarea name="rationale" rows="3" required placeholder="Note what you confirmed and any scope decision that needs context.">' + esc(assessment.review?.rationale || "") + '</textarea></label></div><div class="workflow-preview"><strong>What this saves</strong><p>Confirm ' + assessment.recordCount + ' current ' + esc(pluralize("record", assessment.recordCount)) + '. If the collection or material scope changes, FileGRC will ask for another review.</p></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status review-save-status" role="status" aria-live="polite"></span><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary">Confirm and save</button></div></form>';
|
|
1035
1098
|
document.body.append(dialog);
|
|
1036
1099
|
dialog.showModal();
|
|
1037
1100
|
const form = dialog.querySelector("form");
|
|
@@ -1359,7 +1422,7 @@ function stagePageCard(stage, destination, index) {
|
|
|
1359
1422
|
const completionState = '<span class="stage-page-completion-state ' + (complete ? "complete" : "") + '">' + esc(derived.label) + '</span>';
|
|
1360
1423
|
const taskPreview = items.length
|
|
1361
1424
|
? '<div class="stage-page-tasks">' + items.slice(0, 3).map((item) => {
|
|
1362
|
-
const href = workflowItemHref(item) || destination.href;
|
|
1425
|
+
const href = destination.utility === "evidence-sources" ? destination.href : workflowItemHref(item) || destination.href;
|
|
1363
1426
|
return '<a href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong><small>' + esc(stagePageItemDetail(item)) + '</small></span></a>';
|
|
1364
1427
|
}).join("") + (items.length > 3 ? '<small class="stage-page-tasks-more">+' + (items.length - 3) + ' more on this page</small>' : "") + '</div>'
|
|
1365
1428
|
: "";
|
|
@@ -1402,6 +1465,14 @@ function derivedStagePageState(stage, destination) {
|
|
|
1402
1465
|
) {
|
|
1403
1466
|
return { complete: false, label: "No engagement" };
|
|
1404
1467
|
}
|
|
1468
|
+
if (destination.utility === "evidence-sources") {
|
|
1469
|
+
const evidenceItems = state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items
|
|
1470
|
+
.filter(({ id }) => id.startsWith("source-family-")) || [];
|
|
1471
|
+
const incomplete = evidenceItems.filter(({ status }) => status !== "complete").length;
|
|
1472
|
+
return incomplete
|
|
1473
|
+
? { complete: false, label: incomplete + " " + pluralize("source", incomplete) + " need work" }
|
|
1474
|
+
: { complete: evidenceItems.length > 0, label: evidenceItems.length ? "Ready" : "Not configured", countsTowardProgress: evidenceItems.length > 0 };
|
|
1475
|
+
}
|
|
1405
1476
|
const items = stagePageItems(stage, destination);
|
|
1406
1477
|
const deferredStates = new Set(["later", "scheduled", "upcoming", "waiting-external"]);
|
|
1407
1478
|
const blocking = items.filter(({ state }) => !deferredStates.has(state));
|
|
@@ -1411,7 +1482,9 @@ function derivedStagePageState(stage, destination) {
|
|
|
1411
1482
|
if (items.some(({ state }) => deferredStates.has(state))) {
|
|
1412
1483
|
return { complete: false, countsTowardProgress: false, label: "Later" };
|
|
1413
1484
|
}
|
|
1414
|
-
const collectionReview = destination.type
|
|
1485
|
+
const collectionReview = destination.type && collectionReviewVisible(destination.type)
|
|
1486
|
+
? state.collectionReviews?.[destination.type]
|
|
1487
|
+
: null;
|
|
1415
1488
|
if (collectionReview) {
|
|
1416
1489
|
return collectionReview.status === "current"
|
|
1417
1490
|
? { complete: true, label: "Reviewed" }
|
|
@@ -1438,6 +1511,11 @@ function stagePageItems(stage, destination) {
|
|
|
1438
1511
|
&& item.code === "governance.appointment.independent-policy-reviewer"
|
|
1439
1512
|
)
|
|
1440
1513
|
));
|
|
1514
|
+
if (destination.utility === "evidence-sources") {
|
|
1515
|
+
return (state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items || [])
|
|
1516
|
+
.filter(({ id, status }) => id.startsWith("source-family-") && status !== "complete")
|
|
1517
|
+
.map((item) => ({ ...item, key: item.id, state: item.status, subject: { type: "source-coverage" } }));
|
|
1518
|
+
}
|
|
1441
1519
|
if (stage.id === "policies" && destination.href === "#/stage/policies") {
|
|
1442
1520
|
return items.sort((left, right) => (
|
|
1443
1521
|
workflowItemStatePriority(left) - workflowItemStatePriority(right)
|
|
@@ -1582,6 +1660,7 @@ function sectionDestinations(section) {
|
|
|
1582
1660
|
destinations.push({ type: link.type, kind: "Record page", label: link.label, href: link.href, description: section.description });
|
|
1583
1661
|
}
|
|
1584
1662
|
if (section.utility === "obligation-board") destinations.push({ utility: section.utility, kind: "Working page", label: "Work Queue", href: "#/stage/run", description: "Complete recurring work, Policy Event tasks, and assigned follow-up with its due windows and linked proof." });
|
|
1663
|
+
if (section.utility === "evidence-sources") destinations.push({ utility: section.utility, kind: "Coverage page", label: "Evidence Sources", href: "#/evidence-sources", description: section.description });
|
|
1585
1664
|
if (section.utility === "audit-packet") destinations.push({ utility: section.utility, kind: "Working page", label: "Audit Evidence & Packet", href: "#/audit-packet", description: "Review filegrc Evidence and Evidence Artifacts, prepare fieldwork, and build the indexed packet." });
|
|
1586
1665
|
return destinations;
|
|
1587
1666
|
}
|
|
@@ -5986,6 +6065,7 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
|
|
|
5986
6065
|
`;
|
|
5987
6066
|
|
|
5988
6067
|
export const APP_STYLES = String.raw`
|
|
6068
|
+
.finish-step{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-top:24px;padding:20px 22px}.finish-step h2,.finish-step-heading h2{margin:5px 0 7px;font:500 24px Georgia,serif}.finish-step p:not(.kicker),.finish-step-heading p:not(.kicker){margin:0;color:var(--muted);font-size:12px;line-height:1.55}.finish-step.blocked{border-color:#d8bd78}.finish-step.complete{border-color:#b9dac6}.finish-step-heading{margin-top:28px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}
|
|
5989
6069
|
:root{--ink:#151827;--muted:#5d6475;--line:#dfe3ef;--paper:#f6f7fb;--panel:#fff;--accent:#0000a5;--accent-soft:#eef1ff;--accent-light:#8aa1ff;--focus:#0000e0;--amber:#8a5200;--red:#a13a31;--sidebar:linear-gradient(135deg,#000070 0%,#000035 60%);--primary-gradient:linear-gradient(135deg,#000070 0%,#000035 60%);--surface-soft:#f2f4fa;--surface-muted:#eceff7;--field:#fff;--field-readonly:#eef0f6;--code-bg:#10162b;--code-ink:#e8ebff;--shadow:0 8px 28px rgba(0,0,53,.08);color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
|
|
5990
6070
|
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}@keyframes spinner-rotate{to{transform:rotate(360deg)}}.spinner{display:inline-block;flex:0 0 auto;width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-rotate .7s linear infinite}.spinner-large{width:30px;height:30px;border-width:3px}.loading,.detail-loading,.muted.is-loading,.save-status.is-loading,.onboarding-save-status.is-loading,.button.is-loading{display:flex;align-items:center;gap:9px}.loading{min-height:100vh;justify-content:center;color:var(--muted)}.state-loading{display:flex;align-items:flex-start;gap:16px}.state-loading>.spinner{margin-top:5px;color:var(--accent)}.state-loading h2{margin:5px 0 8px}.state-loading p:last-child{margin:0;color:var(--muted)}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2px}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
5991
6071
|
.topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.program-select select{max-width:180px;border:1px solid var(--line);border-radius:8px;padding:9px 28px 9px 10px;background:var(--surface);color:var(--ink);font:inherit}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
|
package/src/workflow.js
CHANGED
|
@@ -576,9 +576,7 @@ function finalizationFields(record, model, resources, program) {
|
|
|
576
576
|
],
|
|
577
577
|
control: [
|
|
578
578
|
["procedureRevision", record.status === "implemented"],
|
|
579
|
-
["procedureEffectiveOn", record.status === "implemented"]
|
|
580
|
-
["implementationReviewedByIds", record.status === "implemented"],
|
|
581
|
-
["implementationReviewedOn", record.status === "implemented"]
|
|
579
|
+
["procedureEffectiveOn", record.status === "implemented"]
|
|
582
580
|
]
|
|
583
581
|
}[record.type] || [];
|
|
584
582
|
for (const [field, applies] of requested) {
|