filegrc 0.15.2 → 0.15.4

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.15.2",
3
+ "version": "0.15.4",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -997,24 +997,30 @@ function occurrenceContinuityStage(audit, records, model, asOf) {
997
997
  export function occurrenceProgressUnits(plan, { selectedControlIds, periodStart, periodThrough }) {
998
998
  return [
999
999
  ...plan.calendarItems.flatMap((occurrence) => {
1000
- const completedMemberIds = new Set(occurrence.completedMemberIds || []);
1000
+ const resolvedMemberIds = new Set(occurrence.resolvedMemberIds || occurrence.completedMemberIds || []);
1001
+ const membersComplete = occurrence.membershipFinal && (occurrence.expectedMemberIds || [])
1002
+ .every((resourceId) => resolvedMemberIds.has(resourceId));
1001
1003
  const conclusionComplete = occurrence.reconciliationStatus === "reconciled"
1002
- && ["complete", "complete-with-exceptions", "zero-population"].includes(occurrence.operatingResult);
1004
+ && ["complete", "complete-with-exceptions", "zero-population"].includes(occurrence.operatingResult)
1005
+ && membersComplete
1006
+ && occurrence.conclusionValid !== false;
1003
1007
  const occurrenceComplete = conclusionComplete || occurrence.legacySchedule && occurrence.status === "complete";
1004
- const memberUnits = (occurrence.expectedMemberIds || []).map((resourceId) => ({
1005
- id: `occurrence-${occurrence.occurrenceKey}-member-${resourceId}`,
1006
- status: occurrenceComplete || completedMemberIds.has(resourceId) ? "complete" : "action",
1007
- title: `${occurrence.title} member ${resourceId}`
1008
- }));
1009
- const membersComplete = memberUnits.every(({ status }) => status === "complete");
1010
- return [
1011
- ...memberUnits,
1008
+ const workComplete = occurrenceComplete || membersComplete;
1009
+ const workUnit = {
1010
+ id: `occurrence-${occurrence.occurrenceKey}-work`,
1011
+ status: workComplete ? "complete" : "action",
1012
+ title: `${occurrence.title} occurrence work`
1013
+ };
1014
+ return occurrence.legacySchedule
1015
+ ? [workUnit]
1016
+ : [
1017
+ workUnit,
1012
1018
  {
1013
1019
  id: `occurrence-${occurrence.occurrenceKey}-reconciliation`,
1014
- status: occurrenceComplete ? "complete" : membersComplete && occurrence.membershipFinal ? "action" : "blocked",
1020
+ status: occurrenceComplete ? "complete" : workComplete && occurrence.membershipFinal ? "action" : "blocked",
1015
1021
  title: `${occurrence.title} reconciliation`
1016
1022
  }
1017
- ];
1023
+ ];
1018
1024
  }),
1019
1025
  ...plan.eventRuns
1020
1026
  .filter((run) => run.status !== "canceled" && run.occurredOn >= periodStart && run.occurredOn <= periodThrough)
@@ -0,0 +1,31 @@
1
+ export function occurrenceMemberExceptionIsValid(member, occurrence, byId) {
2
+ const exception = byId.get(member.exceptionId);
3
+ const occurrenceStart = occurrence.coverage?.kind === "as-of"
4
+ ? occurrence.coverage.on
5
+ : occurrence.coverage?.startsOn;
6
+ const occurrenceEnd = occurrence.coverage?.kind === "as-of"
7
+ ? occurrence.coverage.on
8
+ : occurrence.coverage?.endsOn;
9
+ const exceptionCoversMember = exception?.scopeResourceIds?.includes(member.resourceId)
10
+ || exception?.scopeResourceIds?.includes(occurrence.obligationId);
11
+ const statusCoveredOccurrence = exception?.status === "approved"
12
+ || ["revoked", "closed"].includes(exception?.status)
13
+ && exception.resolution?.resolvedOn > occurrenceEnd;
14
+ return Boolean(
15
+ exception?.type === "exception"
16
+ && statusCoveredOccurrence
17
+ && exceptionCoversMember
18
+ && exception.approval?.approvedOn <= occurrenceStart
19
+ && exception.approval?.expiresOn >= occurrenceEnd
20
+ );
21
+ }
22
+
23
+ export function occurrenceMemberIsResolved(member, occurrence, byId, expectedCompletionIsValid = () => false) {
24
+ if (member.disposition === "expected") {
25
+ return member.result === "passed"
26
+ && (member.completionResourceIds || []).some((id) => expectedCompletionIsValid(id, member));
27
+ }
28
+ if (member.disposition === "exception") return occurrenceMemberExceptionIsValid(member, occurrence, byId);
29
+ if (member.disposition === "not-applicable") return Boolean(String(member.rationale || "").trim());
30
+ return false;
31
+ }
@@ -29,6 +29,7 @@ import { serializeWorkspaceMutation } from "./mutation.js";
29
29
  import { collectionReviewRevision, historicalCollectionReviewSnapshot } from "./collection-review-integrity.js";
30
30
  import { bindAttestationReportingRouteSet, reportingRouteRevision } from "./reporting-route-integrity.js";
31
31
  import { selectScopedCollectionRecords } from "./collection-scope.js";
32
+ import { occurrenceMemberIsResolved } from "./obligation-members.js";
32
33
 
33
34
  const COMPLETION_DATE_FIELDS = [
34
35
  "completedOn",
@@ -214,8 +215,22 @@ export function planObligations(resources, options = {}) {
214
215
  const completedMemberIds = new Set((reconciliation?.members || [])
215
216
  .filter((member) => member.result === "passed" && member.disposition === "expected")
216
217
  .map((member) => member.resourceId));
218
+ const resolvedMemberIds = new Set((reconciliation?.members || [])
219
+ .filter((member) => occurrenceMemberIsResolved(member, reconciliation, byId, (completionId) => {
220
+ const completion = byId.get(completionId);
221
+ return completionTypeMatches(completion, expectedCompletionTypes)
222
+ && completionMatchesMember(completion, activity.aggregate?.completionMemberField, member.resourceId)
223
+ && completionFallsInWindow(completion, window, rule?.timezone || workspace?.timezone || "UTC")
224
+ && completionPassesActivity(completion, activity);
225
+ }))
226
+ .map((member) => member.resourceId));
227
+ const resolutionsComplete = membershipFinal
228
+ && expectedMemberIds.every((resourceId) => resolvedMemberIds.has(resourceId));
229
+ const conclusionValid = ["complete", "complete-with-exceptions", "zero-population"].includes(reconciliation?.conclusion)
230
+ && resolutionsComplete;
217
231
  const successful = reconciliation
218
- ? reconciliation.status === "reconciled" && ["complete", "complete-with-exceptions", "zero-population"].includes(reconciliation.conclusion)
232
+ ? reconciliation.status === "reconciled"
233
+ && conclusionValid
219
234
  : legacyCompletions.length > 0;
220
235
  const timingStatus = occurrenceStatus(window, asOf, successful);
221
236
  const status = timingStatus === "complete" || programStatus === "accepted" ? timingStatus : "proposed";
@@ -244,6 +259,9 @@ export function planObligations(resources, options = {}) {
244
259
  completionResourceIds,
245
260
  expectedMemberIds,
246
261
  completedMemberIds: [...completedMemberIds],
262
+ resolvedMemberIds: [...resolvedMemberIds],
263
+ resolutionsComplete,
264
+ conclusionValid,
247
265
  expectedCount: reconciliation?.status === "open" && !membershipFinal
248
266
  ? expectedMemberIds.length
249
267
  : reconciliation?.expectedCount ?? expectedMemberIds.length,
@@ -282,8 +300,20 @@ export function planObligations(resources, options = {}) {
282
300
  const completedMemberIds = (occurrence.members || [])
283
301
  .filter(({ disposition, result }) => disposition === "expected" && result === "passed")
284
302
  .map(({ resourceId }) => resourceId);
303
+ const resolvedMemberIds = (occurrence.members || [])
304
+ .filter((member) => occurrenceMemberIsResolved(member, occurrence, byId, (completionId) => {
305
+ const completion = byId.get(completionId);
306
+ return completionTypeMatches(completion, activity.completionResourceTypes)
307
+ && completionMatchesMember(completion, activity.aggregate?.completionMemberField, member.resourceId)
308
+ && completionFallsInWindow(completion, window, rule.timezone || workspace?.timezone || "UTC")
309
+ && completionPassesActivity(completion, activity);
310
+ }))
311
+ .map(({ resourceId }) => resourceId);
312
+ const resolutionsComplete = (occurrence.members || []).every(({ resourceId }) => resolvedMemberIds.includes(resourceId));
313
+ const conclusionValid = ["complete", "complete-with-exceptions", "zero-population"].includes(occurrence.conclusion)
314
+ && resolutionsComplete;
285
315
  const successful = occurrence.status === "reconciled"
286
- && ["complete", "complete-with-exceptions", "zero-population"].includes(occurrence.conclusion);
316
+ && conclusionValid;
287
317
  const timingStatus = occurrenceStatus(window, asOf, successful);
288
318
  if (timingStatus === "complete" && !options.includeComplete) continue;
289
319
  calendarItems.push({
@@ -307,6 +337,9 @@ export function planObligations(resources, options = {}) {
307
337
  completionResourceIds: [...new Set((occurrence.members || []).flatMap(({ completionResourceIds = [] }) => completionResourceIds))],
308
338
  expectedMemberIds: (occurrence.members || []).map(({ resourceId }) => resourceId),
309
339
  completedMemberIds,
340
+ resolvedMemberIds,
341
+ resolutionsComplete,
342
+ conclusionValid,
310
343
  expectedCount: occurrence.expectedCount,
311
344
  completedCount: occurrence.completedCount,
312
345
  membershipFinal: true,
@@ -1750,8 +1783,8 @@ function plannedCompletionWindow(window) {
1750
1783
  };
1751
1784
  }
1752
1785
 
1753
- function completionFallsInWindow(record, window) {
1754
- const date = completionDate(record);
1786
+ function completionFallsInWindow(record, window, timezone = "UTC") {
1787
+ const date = completionDate(record, timezone);
1755
1788
  return Boolean(date && date >= window.dueWindowStart && date <= window.dueWindowEnd);
1756
1789
  }
1757
1790
 
@@ -1765,11 +1798,11 @@ function completionTypeMatches(record, expectedTypes = []) {
1765
1798
  return Boolean(record && (expectedTypes.length === 0 || expectedTypes.includes(record.type)));
1766
1799
  }
1767
1800
 
1768
- function completionDate(record) {
1801
+ function completionDate(record, timezone = "UTC") {
1769
1802
  const coverageDate = coverageEnd(record.coverage);
1770
1803
  if (parseCalendarDate(coverageDate)) return coverageDate;
1771
1804
  for (const field of COMPLETION_TIMESTAMP_FIELDS) {
1772
- if (isRfc3339Timestamp(record[field])) return record[field].slice(0, 10);
1805
+ if (isRfc3339Timestamp(record[field])) return currentCalendarDate(timezone, new Date(record[field]));
1773
1806
  }
1774
1807
  for (const field of COMPLETION_DATE_FIELDS) {
1775
1808
  if (parseCalendarDate(record[field])) return record[field];
@@ -707,15 +707,34 @@ function reportingRouteItem(records, byId, asOf, timezone = "UTC") {
707
707
  );
708
708
  }
709
709
 
710
- function collectionReviewReadinessItem(assessment) {
710
+ export function collectionReviewReadinessItem(assessment) {
711
711
  const proposalUnits = (assessment.recordProposals || []).map((proposal) => ({
712
712
  id: `${assessment.resourceType}-proposal-${proposal.resourceId}`,
713
713
  status: proposal.complete ? "complete" : "action",
714
714
  title: `${proposal.title} proposal`
715
715
  }));
716
+ const proposalBatchComplete = proposalUnits.every(({ status }) => status === "complete");
717
+ const scheduleApprovalBlocked = assessment.resourceType === "retention-schedule-item"
718
+ && proposalUnits.some(({ status }) => status !== "complete");
719
+ const batchProgressUnits = assessment.resourceType !== "retention-schedule-item" && proposalUnits.length
720
+ ? [
721
+ {
722
+ id: `${assessment.resourceType}-proposal-batch`,
723
+ status: proposalBatchComplete ? "complete" : "action",
724
+ title: `${assessment.configuration.title} proposals`
725
+ },
726
+ {
727
+ id: `${assessment.resourceType}-collection-review`,
728
+ status: proposalBatchComplete
729
+ ? assessment.complete ? "complete" : "action"
730
+ : "blocked",
731
+ title: `${assessment.configuration.title} collection review`
732
+ }
733
+ ]
734
+ : null;
716
735
  return item(
717
736
  `collection-review-${assessment.resourceType}`,
718
- assessment.complete ? "complete" : "action",
737
+ assessment.complete ? "complete" : scheduleApprovalBlocked ? "blocked" : "action",
719
738
  assessment.status === "stale"
720
739
  ? `Review ${assessment.configuration.title.toLowerCase()} again`
721
740
  : `Review ${assessment.configuration.title.toLowerCase()}`,
@@ -724,18 +743,7 @@ function collectionReviewReadinessItem(assessment) {
724
743
  {
725
744
  resourceType: assessment.resourceType,
726
745
  reviewPoints: assessment.configuration.reviewPoints,
727
- ...(proposalUnits.length ? {
728
- progressUnits: [
729
- ...proposalUnits,
730
- {
731
- id: `${assessment.resourceType}-collection-review`,
732
- status: proposalUnits.every(({ status }) => status === "complete")
733
- ? assessment.complete ? "complete" : "action"
734
- : "blocked",
735
- title: `${assessment.configuration.title} collection review`
736
- }
737
- ]
738
- } : {}),
746
+ ...(batchProgressUnits ? { progressUnits: batchProgressUnits } : {}),
739
747
  commands: [
740
748
  `npx filegrc review-collection ${assessment.resourceType} --scaffold`,
741
749
  `npx filegrc review-collection ${assessment.resourceType} REVIEW.json --preview --json`
@@ -1646,6 +1654,7 @@ async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
1646
1654
  : `${coveredControls.length} of ${family.controls.length} selected controls have an active authoritative ${componentSources ? "Component" : "System"} with the required source role, access owners, and extraction instructions.`,
1647
1655
  completeSources[0] || selectedSources[0] || { type: sourceType },
1648
1656
  {
1657
+ progressUnit: false,
1649
1658
  familyId: family.id,
1650
1659
  sourceKinds: family.sourceKinds,
1651
1660
  controlIds: family.controls.map((control) => control.id),
package/src/retention.js CHANGED
@@ -137,6 +137,7 @@ export async function assessRetentionReadiness(loaded, program, options = {}) {
137
137
  : `${duplicates.length} similar pair${duplicates.length === 1 ? " needs" : "s need"} management review. FileGRC will not merge records or rewrite relationships automatically.`,
138
138
  { type: "information-type" },
139
139
  {
140
+ progressUnit: false,
140
141
  candidates: duplicates,
141
142
  commands: [
142
143
  "npx filegrc list information-type --workflow --json",
package/src/validate.js CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  recordsAtRevision
45
45
  } from "./reporting-route-integrity.js";
46
46
  import { validateWorkflowHistoryIntegrity } from "./workflow-history-integrity.js";
47
+ import { occurrenceMemberExceptionIsValid } from "./obligation-members.js";
47
48
 
48
49
  let fingerprintFileReadObserver = null;
49
50
 
@@ -667,18 +668,7 @@ function validateObligationOccurrence(record, model, resources, entries, root, w
667
668
  ));
668
669
  }
669
670
  if (member.disposition === "exception") {
670
- const exception = byId.get(member.exceptionId);
671
- const occurrenceStart = record.coverage?.kind === "as-of" ? record.coverage.on : record.coverage?.startsOn;
672
- const occurrenceEnd = record.coverage?.kind === "as-of" ? record.coverage.on : record.coverage?.endsOn;
673
- const exceptionCoversMember = exception?.scopeResourceIds?.includes(member.resourceId)
674
- || exception?.scopeResourceIds?.includes(record.obligationId);
675
- if (
676
- exception?.type !== "exception"
677
- || exception.status !== "approved"
678
- || !exceptionCoversMember
679
- || exception.approval?.approvedOn > occurrenceStart
680
- || exception.approval?.expiresOn < occurrenceEnd
681
- ) {
671
+ if (!occurrenceMemberExceptionIsValid(member, record, byId)) {
682
672
  diagnostics.push(error(
683
673
  "invalid-member-exception",
684
674
  path,
package/src/web.js CHANGED
@@ -670,7 +670,8 @@ function renderRetentionSchedulePage(main, params = new URLSearchParams()) {
670
670
  const guideTrigger = '<button class="guide-trigger" id="resource-guide-trigger" type="button" aria-label="About Data Retention Schedule" aria-haspopup="dialog" aria-controls="resource-guide" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button>';
671
671
  const initialFilter = params.get("q") || "";
672
672
  const showHistory = params.get("history") === "1";
673
- const tableTools = '<div class="retention-table-tools"><label><span class="sr-only">Filter schedule rows</span><input type="search" data-retention-search placeholder="Filter schedule rows" value="' + esc(initialFilter) + '"></label><label class="history-toggle"><input type="checkbox" data-retention-history ' + (showHistory ? "checked" : "") + '> Show retired</label></div>';
673
+ const retiredRows = rows.filter(({ record }) => ["superseded", "retired"].includes(record.status)).length;
674
+ const tableTools = '<div class="retention-table-tools"><label><span class="sr-only">Filter schedule rows</span><input type="search" data-retention-search placeholder="Filter schedule rows" value="' + esc(initialFilter) + '"></label>' + (retiredRows ? '<label class="history-toggle"><input type="checkbox" data-retention-history ' + (showHistory ? "checked" : "") + '> Show ' + retiredRows + ' retired ' + pluralize("row", retiredRows) + '</label>' : '<input type="checkbox" data-retention-history hidden>') + '</div>';
674
675
  main.innerHTML = '<div class="page retention-schedule-page"><nav class="breadcrumbs"><a href="#/stage/policies">Step 2</a><span>/</span><span>Data Retention Schedule</span></nav>' +
675
676
  '<div class="page-intro"><div><p class="kicker">Step 2</p><div class="page-title-line"><h2>Data Retention Schedule</h2>' + guideTrigger + '</div><p>Owners finish each proposed retention row first. A separate reviewer then approves the complete document and all proposed rows once.</p></div><div class="actions">' + documentAction + add + '</div></div>' + resourceGuide("retention-schedule-item") +
676
677
  '<div class="retention-summary"><div><span class="retention-summary-label">Governing document</span>' + documentSummary + '</div><div><span class="retention-summary-label">Schedule revision</span><span><strong>' + proposedRows + ' completed ' + pluralize("proposal", proposedRows) + '</strong><small>Final approval follows every row proposal</small></span><span class="badge ' + (state.collectionReviews?.["retention-schedule-item"]?.status === "current" ? "good" : "warn") + '">' + (state.collectionReviews?.["retention-schedule-item"]?.status === "current" ? "Approved" : "Approval needed") + '</span></div></div>' +
@@ -746,7 +747,13 @@ function wireRetentionScheduleTable(total, initialPage = 1) {
746
747
  noResults.hidden = filtered.length !== 0 || total === 0;
747
748
  const firstVisible = filtered.length ? start + 1 : 0;
748
749
  const lastVisible = Math.min(start + LIST_PAGE_SIZE, filtered.length);
749
- count.textContent = firstVisible + "–" + lastVisible + " of " + filtered.length + " " + pluralize("row", filtered.length) + (filtered.length === total ? "" : " · " + total + " total");
750
+ const hiddenByHistory = historyToggle.checked ? 0 : [...table.querySelectorAll('[data-retention-row][data-history="true"]')]
751
+ .filter((row) => !query || row.textContent.toLowerCase().includes(query)).length;
752
+ const matching = filtered.length + hiddenByHistory;
753
+ count.textContent = firstVisible + "–" + lastVisible + " of " + filtered.length + (historyToggle.checked ? " shown " : " active ") + pluralize("row", filtered.length)
754
+ + (hiddenByHistory ? " · " + hiddenByHistory + " retired hidden" : "")
755
+ + (query ? " · " + matching + " matching" : "")
756
+ + " · " + total + " total";
750
757
  pagination.hidden = totalPages === 1;
751
758
  pageStatus.textContent = "Page " + pageNumber + " of " + totalPages;
752
759
  previous.disabled = pageNumber === 1;
@@ -1765,7 +1772,7 @@ function stagePageCard(stage, destination, index) {
1765
1772
  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>';
1766
1773
  }).join("") + (items.length > 3 ? '<small class="stage-page-tasks-more">+' + (items.length - 3) + ' more on this page</small>' : "") + '</div>'
1767
1774
  : "";
1768
- return '<article class="stage-page-card ' + (complete ? "complete" : "") + '"><a class="stage-page-card-link" href="' + destination.href + '" aria-label="Open ' + esc(destination.label) + '"></a><div class="stage-page-card-head"><div><small>' + esc(stepLabel) + '</small><h3>' + esc(destination.label) + '</h3></div>' + completionState + '</div><p>' + esc(summary) + '</p>' + taskPreview + '<div class="stage-page-card-foot"><span class="stage-page-open" aria-hidden="true">Open ›</span></div></article>';
1775
+ return '<article class="stage-page-card ' + (complete ? "complete" : "") + '"><div class="stage-page-card-head"><div><small>' + esc(stepLabel) + '</small><h3>' + esc(destination.label) + '</h3></div><div class="stage-page-card-actions">' + completionState + '<a class="stage-page-open" href="' + destination.href + '" aria-label="Open ' + esc(destination.label) + '"><span>Open ›</span></a></div></div><p>' + esc(summary) + '</p>' + taskPreview + '</article>';
1769
1776
  }
1770
1777
 
1771
1778
  function stagePageItemDetail(item) {
@@ -1891,7 +1898,7 @@ function stagePageItems(stage, destination) {
1891
1898
  }
1892
1899
 
1893
1900
  function workflowUiStage(stageId) {
1894
- return stageId === "operation" ? "run" : stageId;
1901
+ return ["operate", "operation"].includes(stageId) ? "run" : stageId;
1895
1902
  }
1896
1903
 
1897
1904
  function operationProgress() {
@@ -6483,11 +6490,13 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
6483
6490
  .nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:13.2px}
6484
6491
  .nav-heading-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:2px;align-items:stretch}.nav-heading-row>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr);gap:6px;align-items:center;width:100%;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0;text-decoration:none}.nav-heading-row>.nav-heading:hover,.nav-heading-row>.nav-heading.current{background:rgba(255,255,255,.1);color:#fff}.nav-toggle{display:grid;place-items:center;width:24px;height:auto;min-height:100%;padding:0;border:0;border-radius:6px;background:none;color:#aeb6d8;cursor:pointer}.nav-toggle:hover{background:rgba(255,255,255,.1);color:#fff}.nav-chevron{display:block;width:12px;height:12px;place-self:center;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}.nav-subheading-row{display:grid;grid-template-columns:minmax(0,1fr) 22px;gap:2px;align-items:center;width:100%;padding:0;border:0;border-radius:6px;background:none;color:#919bc4;cursor:pointer}.nav-subheading-row:hover{background:rgba(255,255,255,.1);color:#fff}.nav-subheading-row>.nav-subheading{display:flex;align-items:center;min-width:0;padding:7px;color:inherit;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780}.nav-group.open>.nav-heading-row .nav-chevron,.nav-group.open>.nav-subheading-row .nav-chevron{transform:rotate(90deg)}
6485
6492
  .stage-overview-hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;align-items:center;padding:26px 28px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stage-overview-hero h2,.group-overview-head h2{font:500 37.2px Georgia,serif;margin:7px 0}.stage-overview-hero>div>p:not(.kicker),.group-overview-head>div>p:not(.kicker){max-width:760px;color:var(--muted);font-size:14.4px;line-height:1.55;margin:0}.stage-progress-card{padding:15px 17px;background:var(--paper);border:1px solid var(--line);border-radius:9px}.stage-progress-card>div:first-child{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}.stage-progress-card>div:first-child>strong{font:500 33.6px Georgia,serif}.stage-progress-card p{color:var(--muted);font-size:10.8px;line-height:1.45;margin:9px 0 0}.badge.neutral{background:#e5e8f2;color:#555e73}.stage-overview-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:15px;margin-top:15px;align-items:start}.stage-plan ol,.group-plan ol{display:grid;gap:12px;padding-left:20px;margin:0}.stage-plan li,.group-plan li{padding-left:4px;color:var(--ink);font-size:13.2px;line-height:1.5}.stage-groups{min-width:0}.stage-groups>.section-head{margin:4px 0 13px}.stage-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-group-card{position:relative;display:block;min-height:138px;padding:18px 38px 17px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.stage-group-card:hover{border-color:var(--accent-light)}.stage-group-card h3{font-size:15.6px;margin:0 0 7px}.stage-group-card p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.stage-group-card small{display:block;color:var(--accent);font-size:9.6px;font-weight:700;margin-top:12px}.stage-group-arrow{position:absolute;right:16px;top:16px;color:var(--accent);font-size:24px}.group-overview-head{display:flex;justify-content:space-between;align-items:end;gap:25px;margin-bottom:15px}.stage-status-link{display:grid;grid-template-columns:auto auto;align-items:center;gap:4px 12px;min-width:155px;padding:12px 14px;background:var(--panel);border:1px solid var(--line);border-radius:9px;text-decoration:none}.stage-status-link>strong{font:500 28.8px Georgia,serif;text-align:right}.stage-status-link>small{grid-column:1/-1;color:var(--muted);font-size:9.6px;text-align:right}.relationship-note{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;align-items:center;margin-bottom:15px;padding:17px 20px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.relationship-note h3{font-size:15.6px;margin:5px 0 0}.relationship-note>p{color:var(--muted);font-size:12px;line-height:1.55;margin:0}.relationship-note code{font-size:10.8px}.group-plan{margin-bottom:24px}.group-related-links{display:flex;align-items:center;gap:8px;margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.group-related-links>span{color:var(--muted);font-size:10.8px;margin-right:auto}.group-destinations>.section-head{margin-bottom:12px}.group-destination-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.group-destination-card{display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:16px;min-height:150px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.group-destination-card:hover{border-color:var(--accent-light)}.group-destination-card h3{font-size:15.6px;margin:5px 0 7px}.group-destination-card p:not(.kicker){color:var(--muted);font-size:10.8px;line-height:1.5;margin:0}.destination-rollup{align-self:center;text-align:right}.destination-rollup strong,.destination-rollup small{display:block}.destination-rollup strong{font:500 32.4px Georgia,serif}.destination-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:3px}
6486
- .collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.workflow-guidance-more>summary{cursor:pointer;font-weight:750}.workflow-findings-more{margin-top:9px}.workflow-guidance-more>p{margin:8px 0 0}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.policy-lifecycle-note{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px;margin-top:18px}.policy-lifecycle-note h3{margin:5px 0 8px}.policy-lifecycle-note p:not(.kicker),.policy-library-proposals p{color:var(--muted);font-size:11px;line-height:1.55}.policy-library-proposals{display:grid;gap:8px}.policy-library-proposals article{padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.policy-library-proposals p{margin:5px 0 8px}.policy-activation{display:grid;gap:12px;margin-top:22px}.policy-activation-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.policy-activation-card{padding:17px 18px;border:1px solid #d8bd78;border-radius:10px;background:var(--panel)}.policy-activation-card.active-and-operating{border-color:#b9dac6}.policy-activation-card h3{margin:7px 0 0}.policy-activation-gaps{display:grid;gap:9px;margin-top:13px}.policy-activation-gaps>div{padding-top:8px;border-top:1px solid var(--line)}.policy-activation-gaps small{display:block;margin-bottom:6px;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.policy-activation-warning{margin:11px 0 0;padding:9px 10px;border-radius:7px;background:#f7e9cf;color:#6d4917;font-size:10.4px;line-height:1.45}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-link{position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-card-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
6493
+ .collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.workflow-guidance-more>summary{cursor:pointer;font-weight:750}.workflow-findings-more{margin-top:9px}.workflow-guidance-more>p{margin:8px 0 0}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.policy-lifecycle-note{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px;margin-top:18px}.policy-lifecycle-note h3{margin:5px 0 8px}.policy-lifecycle-note p:not(.kicker),.policy-library-proposals p{color:var(--muted);font-size:11px;line-height:1.55}.policy-library-proposals{display:grid;gap:8px}.policy-library-proposals article{padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.policy-library-proposals p{margin:5px 0 8px}.policy-activation{display:grid;gap:12px;margin-top:22px}.policy-activation-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.policy-activation-card{padding:17px 18px;border:1px solid #d8bd78;border-radius:10px;background:var(--panel)}.policy-activation-card.active-and-operating{border-color:#b9dac6}.policy-activation-card h3{margin:7px 0 0}.policy-activation-gaps{display:grid;gap:9px;margin-top:13px}.policy-activation-gaps>div{padding-top:8px;border-top:1px solid var(--line)}.policy-activation-gaps small{display:block;margin-bottom:6px;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.policy-activation-warning{margin:11px 0 0;padding:9px 10px;border-radius:7px;background:#f7e9cf;color:#6d4917;font-size:10.4px;line-height:1.45}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
6494
+ .stage-page-card-actions{display:flex;align-items:center;gap:10px}.stage-page-card-actions .stage-page-open{white-space:nowrap}.stage-page-open::after{content:"";position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-open:focus-visible::after{outline:2px solid var(--focus);outline-offset:2px}.stage-page-open>span{position:relative;z-index:2}
6487
6495
  .workflow-findings>a:hover{border-color:var(--accent-light);box-shadow:0 3px 9px rgba(21,40,33,.05)}.workflow-findings>a:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{align-items:flex-start}.stage-page-completion-state{flex:0 0 auto;max-width:150px;padding:5px 8px;border-radius:99px;background:#f7e9cf;color:#855717;font-size:9.6px;font-weight:750;line-height:1.25;text-align:right}.stage-page-completion-state.complete{background:#ddefe5;color:#176143}.obligation-card.workflow-target{outline:2px solid var(--focus);outline-offset:3px}.obligation-card-foot{flex-wrap:wrap}.obligation-card-foot .obligation-links{flex:1 1 120px}
6488
6496
  .collection-review-panel.current details{margin-top:8px}.collection-review-detail-content{margin-top:10px}.collection-review-complete-summary{display:flex;align-items:baseline;gap:10px}.collection-review-complete-summary>.kicker{margin:0;white-space:nowrap}.collection-review-panel.current .collection-review-foot{justify-content:flex-end;margin-top:8px}
6489
6497
  .collection-review-current-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.collection-review-panel.current .collection-review-details{flex:0 0 auto;margin:0;padding:0;border:0;background:transparent}.collection-review-panel.current .collection-review-details>summary{color:var(--accent);white-space:nowrap}.collection-review-panel.current .collection-review-details[open]{flex-basis:100%;order:3;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel.current .collection-review-details[open]>summary{margin-bottom:10px}.collection-review-panel.current .collection-review-details[open] .collection-review-detail-content{margin-top:0}.collection-review-panel.current .button{margin-left:auto}
6490
6498
  @media(max-width:520px){.collection-review-complete-summary{flex:1 1 180px;min-width:0;flex-wrap:wrap}.collection-review-complete-summary .collection-review-result{min-width:0;flex-wrap:wrap}.collection-review-complete-summary .collection-review-result span{overflow-wrap:anywhere}.collection-review-panel.current .button{margin-left:0}}
6499
+ @media(max-width:520px){.stage-page-card-head{align-items:stretch;flex-direction:column;gap:10px}.stage-page-card-actions{justify-content:space-between}}
6491
6500
  .evidence-attachments .panel-head{align-items:flex-start}.evidence-attachments .panel-head h3{margin:0}.evidence-attachments .panel-head p{margin:4px 0 0;color:var(--muted);font-size:10px}.evidence-attachments ul{list-style:none;margin:0;padding:0}.evidence-attachments li{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 0;border-top:1px solid var(--line)}.evidence-attachments li:first-child{border-top:0}.evidence-attachments strong,.evidence-attachments small{display:block}.evidence-attachments strong{font-size:11px}.evidence-attachments small{margin-top:2px;color:var(--muted);font-size:9px;overflow-wrap:anywhere}.evidence-attachments .attachment-empty{display:block;color:var(--muted);font-size:10px;line-height:1.45}.danger-text{color:var(--red)}
6492
6501
  .policy-lifecycle-note.single{grid-template-columns:1fr}
6493
6502
  .policy-content-table{margin-top:22px}.policy-content-table>.section-head{align-items:end}.policy-content-table>.section-head h2{font:500 27.6px Georgia,serif;margin:5px 0 6px}.policy-content-table>.section-head p:not(.kicker){max-width:720px;margin:0;color:var(--muted);font-size:12px;line-height:1.5}.policy-content-tools{display:flex;align-items:center;justify-content:flex-end;gap:9px;flex-wrap:wrap}.policy-content-tools input[type="search"]{min-width:210px;border:1px solid var(--line);border-radius:7px;background:var(--field);padding:9px 11px}.history-toggle{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:11px}.policy-content-add{position:relative}.policy-content-add>summary{list-style:none}.policy-content-add>summary::-webkit-details-marker{display:none}.policy-content-add>div{position:absolute;right:0;z-index:5;display:grid;min-width:150px;margin-top:6px;padding:5px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow)}.policy-content-add button{border:0;border-radius:5px;background:transparent;padding:8px 10px;color:var(--ink);text-align:left;cursor:pointer}.policy-content-add button:hover{background:var(--accent-soft)}.policy-content-type span,.policy-content-approval span{display:block}.policy-content-type small,.policy-content-approval small{display:block;margin-top:3px;color:var(--muted);font-size:9.6px}.policy-content-filter-empty{margin:10px 0 0;padding:16px;border:1px dashed var(--line);border-radius:8px;color:var(--muted);font-size:12px;text-align:center}.policy-content-count{margin:9px 2px 0;color:var(--muted);font-size:10.8px;text-align:right}
package/src/workflow.js CHANGED
@@ -27,6 +27,8 @@ import { resolveProgram } from "./program.js";
27
27
  import { resourceReviewRevisions, retentionReviewResourceIds, retentionRuleIsCurrent } from "./retention.js";
28
28
  import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
29
29
  import { planReconciliation } from "./reconciliation.js";
30
+ import { documentIsAuditSpecific } from "./program-lifecycle.js";
31
+ import { resourceProgramContext } from "./program-path.js";
30
32
  import { currentCalendarDate } from "./time.js";
31
33
  import { measureTiming } from "./timing.js";
32
34
  import { validateWorkspace } from "./validate.js";
@@ -388,10 +390,10 @@ function recordFinalizationFindings(loaded, program) {
388
390
  if (["workspace", "renderer-settings"].includes(record.type)) continue;
389
391
  const incomplete = recordIncompleteReason(record, loaded, program);
390
392
  if (incomplete) {
391
- findings.push(finalizationFinding(record, incomplete));
393
+ findings.push(finalizationFinding(record, incomplete, loaded.model));
392
394
  }
393
395
  for (const missing of finalizationFields(record, loaded.model, loaded.resources, program)) {
394
- findings.push(fieldFinding(record, missing));
396
+ findings.push(fieldFinding(record, missing, loaded.model));
395
397
  }
396
398
  }
397
399
  return findings;
@@ -591,13 +593,13 @@ function finalizationFields(record, model, resources, program) {
591
593
  return fields;
592
594
  }
593
595
 
594
- function finalizationFinding(record, details) {
596
+ function finalizationFinding(record, details, model) {
595
597
  const code = `record.${record.type}.${record.id}.finalize`;
596
598
  return {
597
599
  key: code,
598
600
  code,
599
- assessment: recordAssessment(record.type),
600
- stage: recordStage(record.type),
601
+ assessment: recordAssessment(record, model),
602
+ stage: recordStage(record, model),
601
603
  state: details.state,
602
604
  severity: details.state === "blocked" ? "error" : "warning",
603
605
  requiredness: details.requiredness,
@@ -609,13 +611,13 @@ function finalizationFinding(record, details) {
609
611
  };
610
612
  }
611
613
 
612
- function fieldFinding(record, missing) {
614
+ function fieldFinding(record, missing, model) {
613
615
  const code = `record.${record.type}.${record.id}.field.${missing.field}`;
614
616
  return {
615
617
  key: code,
616
618
  code,
617
- assessment: recordAssessment(record.type),
618
- stage: recordStage(record.type),
619
+ assessment: recordAssessment(record, model),
620
+ stage: recordStage(record, model),
619
621
  state: "ready",
620
622
  severity: "warning",
621
623
  requiredness: missing.requiredness,
@@ -1732,22 +1734,27 @@ function shellArgument(value) {
1732
1734
  : `'${text.replaceAll("'", "'\\''")}'`;
1733
1735
  }
1734
1736
 
1735
- function recordAssessment(type) {
1736
- if (["audit", "audit-request", "audit-population"].includes(type)) return "audit-readiness";
1737
- if (["action-item", "obligation-event", "control-activity"].includes(type)) return "period-health";
1737
+ function recordAssessment(record, model) {
1738
+ const stage = workflowStageForRecord(record, model);
1739
+ if (stage === "audit") return "audit-readiness";
1740
+ if (stage === "operate") return "period-health";
1738
1741
  return "program-configuration";
1739
1742
  }
1740
1743
 
1741
- function recordStage(type) {
1742
- if (["person", "appointment", "team", "framework", "requirement", "commitment", "system", "vendor"].includes(type)) {
1743
- return "scope";
1744
- }
1745
- if (["policy", "document", "training"].includes(type)) return "policies";
1746
- if (["control", "complementary-control", "source-coverage", "obligation"].includes(type)) return "controls";
1747
- if (["audit", "audit-request", "audit-population"].includes(type)) return "audit";
1744
+ export function workflowStageForRecord(record, model) {
1745
+ const type = record.type;
1746
+ if (documentIsAuditSpecific(record, model)) return "audit";
1747
+ if (type === "source-coverage") return "controls";
1748
+ const stage = resourceProgramContext(type, model)?.id;
1749
+ if (stage === "run") return "operate";
1750
+ if (stage) return stage;
1748
1751
  return "operate";
1749
1752
  }
1750
1753
 
1754
+ function recordStage(record, model) {
1755
+ return workflowStageForRecord(record, model);
1756
+ }
1757
+
1751
1758
  function appointmentRequiredness(record, model) {
1752
1759
  return model.appointmentTemplates?.[record.appointmentKind]?.requiredness || "conditional";
1753
1760
  }