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/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 &amp; 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
- : route.name === "audit-packet"
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" ? renderDocumentActivationAssessments() + renderPolicyActivationAssessments() + renderRetentionReadiness() + renderEvidenceReadiness() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
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 people = resourcesOfType("person").filter(({ record }) => record.status === "active");
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
- 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><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>';
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 ? state.collectionReviews?.[destination.type] : null;
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) {