filegrc 0.13.3 → 0.13.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/model/v10.json CHANGED
@@ -1436,9 +1436,9 @@
1436
1436
  "policyBasis": "Policies rely on governed documents for detailed plans, schedules, procedures, charters, and reports. Approval accepts the intended values and exact content. Activation records that the linked requirements were implemented and the approved Document was put into use.",
1437
1437
  "cadence": "Complete required program Documents in Step 2, obtain approval from a reviewer who is separate from the owner, implement their linked requirements in Step 3, then activate the unchanged approved revision. Follow the linked review Obligation after activation. Prepare Audit Documents in Step 5, then approve and activate each one there as separate revision-bound updates.",
1438
1438
  "reviewPoints": [
1439
- "Write companion Markdown as a standalone company artifact. Keep FileGRC commands, record-entry instructions, readiness states, relationship IDs, and starter-library mechanics in guides and calculated work unless FileGRC itself is the document's subject.",
1440
- "Replace every bracketed prompt with a reviewed fact before approval, activation, signature, or delivery.",
1441
- "Describe the business fact in ordinary terms and keep resource relationships in the Document record and supporting records."
1439
+ "Keep companion Markdown to the policy, procedure, evidence, or narrative the Document records. Put FileGRC instructions and derived actions in the UI or guides unless FileGRC is the subject.",
1440
+ "Replace every bracketed prompt with a reviewed fact before approval, activation, signing, or delivery.",
1441
+ "Use ordinary business language; keep resource relationships in this record and its supporting records."
1442
1442
  ],
1443
1443
  "sourceResourceIds": [
1444
1444
  "policy-information-security",
@@ -1771,9 +1771,9 @@
1771
1771
  "cadence": "Create an Evidence Artifact only for a real retained artifact or approved reference. Identify its source Component or source records, Controls, collection and verification facts, coverage, revision, and classification.",
1772
1772
  "emptyState": "No Evidence Artifacts have been collected. Add one only when a real retained artifact or approved external reference exists.",
1773
1773
  "reviewPoints": [
1774
- "Use artifactKind signed-record and artifactSubtype signed-management-representation for the fixed signed management representation letter.",
1775
- "For a signed management representation, record the actual signing timestamp in businessEventAt; collectedOn records when FileGRC received the artifact, not when management signed it.",
1776
- "Use artifactKind third-party-report and artifactSubtype soc2-report for the final report issued by the CPA firm, and record the report's actual issuance timestamp in sourceGeneratedAt."
1774
+ "For a signed management representation, use artifactKind signed-record and artifactSubtype signed-management-representation.",
1775
+ "For that representation, record the signing time in businessEventAt; use collectedOn for when FileGRC received it.",
1776
+ "For a final CPA report, use artifactKind third-party-report and artifactSubtype soc2-report, then record its issuance time in sourceGeneratedAt."
1777
1777
  ],
1778
1778
  "sourceResourceIds": [
1779
1779
  "policy-information-security"
@@ -2865,9 +2865,9 @@
2865
2865
  "policyBasis": "Controls translate approved Policies and applicable Requirements into owned procedures that management can operate and prove. FileGRC does not infer technical implementation from policy prose. Configuration facts belong in Controls, Components, Systems, Obligations, and Evidence.",
2866
2866
  "cadence": "A Control may be implemented while its governing Policy, required program Document, or Training is approved but inactive. Before marking it implemented, record its owner, actual procedure in Record Markdown, bounded System scope, operation pattern, authoritative evidence-source Components, implementation date, and enabled calendar or event Obligations. Confirm each source Component is active, has the evidence role required by the Control family and current access owners, and includes repeatable retrieval instructions in Record Markdown. Enabled Obligations remain dormant until all of their governing content is active and effective.",
2867
2867
  "reviewPoints": [
2868
- "Confirm the Control is needed for the applicable Requirements, customer Commitments, and current service boundary.",
2869
- "Confirm the procedure describes what people actually do, and keep starter-selection and FileGRC record-entry instructions out of the Control statement and activity. A Control may name FileGRC when FileGRC is an actual operating Component or evidence source.",
2870
- "Use a not-applicable, external, or zero-population conclusion only when it is factually true for the current scope."
2868
+ "Confirm the Control is needed for the applicable Requirements, Commitments, and service boundary.",
2869
+ "Describe the actual procedure. Put starter-selection and FileGRC record-entry instructions in the UI or guides, unless FileGRC is an operating Component or evidence source.",
2870
+ "Use not applicable, external, or zero population only when it is true for the current scope."
2871
2871
  ],
2872
2872
  "sourceResourceIds": [
2873
2873
  "policy-information-security"
@@ -4389,9 +4389,9 @@
4389
4389
  "policyBasis": "The information security policy requires security training and added role-specific instruction when a person’s responsibilities or data access warrant it.",
4390
4390
  "cadence": "Approve the exact training content in Step 2. During Step 3, finish the linked Controls and Obligations, then activate the unchanged approved revision. Assign training through Obligations at onboarding, annually, and after relevant material changes or incidents.",
4391
4391
  "reviewPoints": [
4392
- "Write workforce-facing material as standalone company training. Keep FileGRC commands, record-entry instructions, and data-model terms in guides and assignment records.",
4393
- "State required behavior without implying that every user needs a paid product or that customer MFA applies without an approved requirement.",
4394
- "Tie each completion to the exact training revision the person reviewed."
4392
+ "Keep workforce material focused on required behavior. Put FileGRC commands, record-entry instructions, and data-model terms in guides and assignments.",
4393
+ "Do not imply every user needs a paid product or that customer MFA applies without an approved requirement.",
4394
+ "Bind each completion to the exact revision the person reviewed."
4395
4395
  ],
4396
4396
  "sourceResourceIds": [
4397
4397
  "policy-information-security"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1599,6 +1599,12 @@ function containsOpenPlaceholder(source) {
1599
1599
  function managementDocumentContentIssues(source, definition, audit) {
1600
1600
  if (!source) return ["Add the required Markdown content."];
1601
1601
  if (containsOpenPlaceholder(source)) return ["Replace every bracketed preparation placeholder before approval."];
1602
+ if (definition.kind === "soc2-management-assertion" && audit) {
1603
+ const incompatibleKind = audit.auditKind === "soc-2-type-1" ? "Type 2" : "Type 1";
1604
+ if (new RegExp(`^#{1,6}\\s+${incompatibleKind} Assertion(?: Draft)?\\s*$`, "imu").test(source)) {
1605
+ return [`Remove the ${incompatibleKind} assertion from this ${audit.auditKind === "soc-2-type-1" ? "Type 1" : "Type 2"} engagement document.`];
1606
+ }
1607
+ }
1602
1608
  const words = source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || [];
1603
1609
  if (definition.minimumWords && words.length < definition.minimumWords) {
1604
1610
  return [`Add substantive content; this document has ${words.length} words and the preparation check expects at least ${definition.minimumWords}.`];
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Use
4
4
 
5
- This schedule records how long {{company_name}} keeps important record classes and what happens when each period ends. The policy owner and data owners must complete the organization-specific rows before approval.
5
+ This schedule records how long {{company_name}} keeps important record classes and what happens when each period ends.
6
6
 
7
7
  Retention periods may come from law, contract, tax, audit, security, or a documented business need. Use the longest applicable period, but do not keep data indefinitely without a reason.
8
8
 
@@ -10,7 +10,7 @@ Retention periods may come from law, contract, tax, audit, security, or a docume
10
10
 
11
11
  The structured Retention Schedule Items linked to this document are its schedule rows. Each approved item must name the covered Information Types and operational scope, owner, cutoff, period, disposition action, instructions, and authority. Planned items are review prompts and are not approved retention behavior.
12
12
 
13
- Management must cover important information used by Systems, Components, and Vendors, including security logs, backups or alternate recovery copies, governance records, audit evidence, customer and service records, and incident records when those classes exist. No starter period or disposition action is an approved organization value.
13
+ Management must cover important information used by Systems, Components, and Vendors, including security logs, backups or alternate recovery copies, governance records, audit evidence, customer and service records, and incident records when those classes exist. A proposed period or disposition action is not an approved organization value.
14
14
 
15
15
  ## Holds and exceptions
16
16
 
@@ -55,9 +55,10 @@ const DOCUMENT_CONTENT_UPDATES = [
55
55
  priorRevision: "45a408e8139bd57f42dda5ca5ae5c8cd4480b4e7bf08834f60058148a3a63475",
56
56
  additionalPriorRevisions: new Set([
57
57
  "d80b99ce53d1012cc169bbbc2afab8d0597bfbe9f30ac0812a8d5bbeb2ed9f90",
58
- "dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7"
58
+ "dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7",
59
+ "4a48c15a4e20e4f29028cf2ff8597315eb51878814120125b5268356b923c9db"
59
60
  ]),
60
- currentRevision: "4a48c15a4e20e4f29028cf2ff8597315eb51878814120125b5268356b923c9db",
61
+ currentRevision: "1738d29786cd6a027c9a2bb339ce36ce3e15c858a55ec503f4e690e65273598b",
61
62
  currentSourcePath: "./policy-library/data-retention-schedule-v2.md",
62
63
  replacements: [
63
64
  ["FileGRC detects the bracketed prompts as approval blockers. Remove each prompt only after replacing it with a reviewed fact.", "Remove each bracketed prompt only after replacing it with a reviewed fact."],
@@ -73,9 +74,10 @@ const DOCUMENT_CONTENT_UPDATES = [
73
74
  additionalPriorRevisions: new Set([
74
75
  "51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae",
75
76
  "5107f5c6015089a7c64e87692976aa48f727b8c03988f93f71e8f3e5084a05ac",
76
- "970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714"
77
+ "970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714",
78
+ "ff8b5764ba3073d7ab6678c7da607850de5a82de4383cb54417cd74d82c05305"
77
79
  ]),
78
- currentRevision: "ff8b5764ba3073d7ab6678c7da607850de5a82de4383cb54417cd74d82c05305",
80
+ currentRevision: "72a6049f2eed25525417fc3eb6263c231e44193d3c5cbbf6675bb98459374d0c",
79
81
  replacements: [
80
82
  ["## Reporting routes", "## Reporting channels"],
81
83
  ["The primary reporting route is {{security_contact_email}}.", "Use the current approved primary security Reporting Route. The Reporting Route record is the source of truth for its channel, destination, owner, priority, and effective period."],
@@ -90,7 +92,14 @@ const DOCUMENT_CONTENT_UPDATES = [
90
92
  ["- Restore-validation method and governed schedule", "- Restore-validation method and approved schedule"],
91
93
  ["- Dependencies, fallback paths, and validation steps", "- Dependencies, fallback paths, and validation steps\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
92
94
  ["[Confirm or replace before activation: The starter proposal for important production data is a daily backup, 30-day retention period, and annual restore validation. Record the approved choice for every important System in its Control, Component, System, Retention Schedule, and Obligation records.]", "[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]"],
93
- ["on the approved governed schedule", "on the approved schedule"]
95
+ ["on the approved governed schedule", "on the approved schedule"],
96
+ ["[Complete before activation: Describe how workers can find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]", "[Describe how workers find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]"],
97
+ ["The Policy Owner maintains this plan and ensures that the organization assigns these duties before activation:", "The Policy Owner maintains this plan and ensures that the organization assigns these duties:"],
98
+ ["[Complete before activation: Record the emergency contact arrangement, its owner, alternate communication channel, protected storage location, and review schedule.]", "[Describe the emergency contact arrangement, its owner, alternate communication channel, protected storage location, and review schedule.]"],
99
+ ["[Complete before activation: Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]", "[Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]"],
100
+ ["[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]", "[Document the management-approved recovery approach for every important System in its recovery procedures and the Data Retention Schedule. A daily backup, 30-day retention period, and annual restore validation are examples, not approved organization values.]"],
101
+ ["[Complete before activation: Record the protected alternate location and access method responders will use when the primary identity, source-control, or collaboration Systems are unavailable. Confirm that authorized responders can retrieve the plan without exposing plaintext secrets or decryption keys.]", "[Describe the protected alternate location and access method responders use when the primary identity, source-control, or collaboration Systems are unavailable. Confirm that authorized responders can retrieve the plan without exposing plaintext secrets or decryption keys.]"],
102
+ ["- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision", "- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
94
103
  ],
95
104
  summary: "Express incident and recovery requirements as a standalone plan, keep FileGRC record-entry guidance outside it, and make numeric recovery targets conditional."
96
105
  },
@@ -98,9 +107,12 @@ const DOCUMENT_CONTENT_UPDATES = [
98
107
  id: "document-soc2-management-representation",
99
108
  path: "documents/document-soc2-management-representation.md",
100
109
  priorRevision: "59e7e5929eeb24fa6bc23e1ed1553206901c33ba249b372ad0f42282ab7a64c7",
101
- currentRevision: "83de736025d07d8085fba93a212a98e7b3e951aded9894b933eab1e77c3a0d29",
110
+ additionalPriorRevisions: new Set(["83de736025d07d8085fba93a212a98e7b3e951aded9894b933eab1e77c3a0d29"]),
111
+ currentRevision: "45fcfa1bfa280fe96c65220df48d55b2bd0f238c54a6dc18e8f2d9ccd38a7f96",
102
112
  replacements: [
103
- ["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"]
113
+ ["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"],
114
+ ["> The service auditor normally supplies the required representation-letter wording near the end of fieldwork. Replace this preparation note with the agreed final letter, obtain the required management signature, and retain the signed fixed-format copy as linked evidence.", "> [Insert the final management representation agreed with the service auditor.]"],
115
+ ["Before signing, reconcile the auditor's letter to:", "The signer confirms that the letter agrees with:"]
104
116
  ],
105
117
  summary: "Keep the representation-letter template independent of FileGRC relationship fields."
106
118
  },
@@ -121,11 +133,14 @@ const DOCUMENT_CONTENT_UPDATES = [
121
133
  id: "document-soc2-system-description",
122
134
  path: "documents/document-soc2-system-description.md",
123
135
  priorRevision: "dc15bc2141be0d63a832eb6f7734bc7381e77582030534bcaf636138782f8e84",
124
- currentRevision: "929a7cd87ca78a2d9caa349cad0ae23583ad79184f26ba50c1694c62549aab9e",
136
+ additionalPriorRevisions: new Set(["929a7cd87ca78a2d9caa349cad0ae23583ad79184f26ba50c1694c62549aab9e"]),
137
+ currentRevision: "0e2e0252e0e0a5be821851de1d168fbcf22bfcca41d80836c657583ab38ba154",
125
138
  replacements: [
126
139
  ["reconcile it to the filegrc records", "reconcile it to management's authoritative records"],
127
140
  ["Link the filegrc commitment records.", "Reconcile the summary to supporting commitment records."],
128
- ["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."]
141
+ ["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."],
142
+ ["> Draft preparation document. Complete every bracketed item, reconcile it to management's authoritative records, and have the service auditor review the final presentation.", "> [Describe the service as it existed for the stated reporting date or period.]"],
143
+ ["[Summarize customer commitments, contractual security promises, internal objectives, and the system requirements needed to meet them. Reconcile the summary to supporting commitment records.]", "[Summarize customer commitments, contractual security promises, internal objectives, and the system requirements needed to meet them.]"]
129
144
  ],
130
145
  summary: "Keep the SOC 2 System Description as a generic management deliverable."
131
146
  }
package/src/web.js CHANGED
@@ -847,13 +847,13 @@ function openPolicyActivationDialog() {
847
847
  }
848
848
 
849
849
  function workflowGuidance(options = {}) {
850
- const workflow = state.workflow;
850
+ const workflow = options.workflow || state.workflow;
851
851
  if (!workflow) return "";
852
852
  const matches = (item) => {
853
853
  if (options.type === "audit" && options.id && item.auditId === options.id) return true;
854
854
  if (options.stageId && item.stage !== options.stageId) return false;
855
- if (options.type && item.subject?.type !== options.type && item.source?.type !== options.type) return false;
856
- if (options.id && item.subject?.id !== options.id && item.source?.id !== options.id) return false;
855
+ if (options.type && item.subject?.type !== options.type) return false;
856
+ if (options.id && item.subject?.id !== options.id) return false;
857
857
  return true;
858
858
  };
859
859
  const activeStates = new Set(["blocked", "due", "open", "overdue", "ready", "scheduled", "upcoming", "waiting-external"]);
@@ -880,9 +880,16 @@ function workflowGuidance(options = {}) {
880
880
  const rows = visible.map(renderRow).join("");
881
881
  const remaining = items.slice(visible.length);
882
882
  const more = remaining.length
883
- ? '<details class="workflow-guidance-more"><summary>Show ' + remaining.length + ' more ' + pluralize("item", remaining.length) + '</summary><div class="workflow-findings workflow-findings-more">' + remaining.map(renderRow).join("") + '</div><p><code>filegrc workflow --json</code> returns the same checklist for headless review.</p></details>'
883
+ ? '<details class="workflow-guidance-more"><summary>Show ' + remaining.length + ' more ' + pluralize("item", remaining.length) + '</summary><div class="workflow-findings workflow-findings-more">' + remaining.map(renderRow).join("") + '</div><p><code>filegrc workflow --json</code> returns this same checklist for CLI and agent use.</p></details>'
884
884
  : "";
885
- return '<section class="workflow-guidance panel detail-support-panel detail-workflow-panel"><div class="panel-head"><div><p class="kicker">To-do</p><h3>' + esc(options.title || "Checklist") + '</h3><p>' + esc(status) + '</p></div><span class="badge ' + (blocking.length ? "warn" : "good") + '">' + (blocking.length ? "Needs work" : "Current") + '</span></div><div class="workflow-findings">' + rows + '</div>' + more + '</section>';
885
+ const completionRequirements = options.type && resourceReviewCriteria(options.type, { trigger: true });
886
+ return '<section class="workflow-guidance panel detail-support-panel detail-workflow-panel"><div class="panel-head"><div><p class="kicker">To-do</p><h3>' + esc(options.title || "Checklist") + '</h3></div><span class="badge ' + (blocking.length ? "warn" : "good") + '">' + (blocking.length ? "Needs work" : "Current") + '</span></div><p class="workflow-status">' + esc(status) + (completionRequirements ? '. ' + completionRequirements : "") + '</p><div class="workflow-findings">' + rows + '</div>' + more + '</section>';
887
+ }
888
+
889
+ function recordCompletionState(record) {
890
+ const completeStatuses = new Set(["approved", "active", "complete", "completed", "closed", "done", "reconciled"]);
891
+ if (!completeStatuses.has(record.status)) return "";
892
+ 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>';
886
893
  }
887
894
 
888
895
  function collectionReviewPanel(type) {
@@ -899,14 +906,21 @@ function collectionReviewPanel(type) {
899
906
  const reviewSummary = current
900
907
  ? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + "." + reviewNote + '</span></p>'
901
908
  : '<p class="collection-review-result"><strong>' + (needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
909
+ const compactReviewSummary = current
910
+ ? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + ".</span></p>"
911
+ : reviewSummary;
902
912
  const action = state.readOnly
903
913
  ? ""
904
914
  : needsFirstRecord
905
915
  ? '<a class="button primary" href="#/resources/' + encodeURIComponent(type) + '?new=1">Add first ' + esc(state.model.resources[type].title.toLowerCase()) + '</a>'
906
916
  : '<button class="button ' + (current ? "" : "primary") + '" type="button" data-review-collection="' + esc(type) + '">' + (current ? "Review again" : "Review and confirm") + '</button>';
907
- return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '"><div class="collection-review-head"><div><p class="kicker">Scope confirmation</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + (current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required") + '</span></div>' +
908
- '<details ' + (current ? "" : "open") + '><summary>What to review</summary><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></details>' +
909
- '<div class="collection-review-foot">' + reviewSummary + action + '</div></section>';
917
+ const status = current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required";
918
+ const details = '<details ' + (current ? 'class="collection-review-details"' : "open") + '><summary>' + (current ? "Show scope confirmation" : "What to review") + '</summary><div class="collection-review-detail-content"><div class="collection-review-head"><div><p class="kicker">Scope confirmation</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + status + '</span></div><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>' + (current ? reviewSummary : "") + '</div></details>';
919
+ const summary = current
920
+ ? '<div class="collection-review-current-row"><div class="collection-review-complete-summary"><span class="kicker">Scope confirmation</span>' + compactReviewSummary + '</div>' + details + action + '</div>'
921
+ : details;
922
+ return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '">' + summary +
923
+ (current ? "" : '<div class="collection-review-foot">' + reviewSummary + action + '</div>') + '</section>';
910
924
  }
911
925
 
912
926
  function collectionNeedsFirstRecord(type) {
@@ -931,13 +945,31 @@ function collectionEmptyState(type, definition) {
931
945
  : "No records exist. Use the scope confirmation above to record an allowed empty-collection conclusion, or add the records management identified.";
932
946
  }
933
947
 
934
- function resourceReviewCriteria(type, collapsed = false) {
948
+ function resourceReviewCriteria(type, options = {}) {
949
+ const collapsed = options === true || options.collapsed;
950
+ const trigger = typeof options === "object" && options.trigger;
951
+ const title = typeof options === "object" && options.title ? options.title : "Review criteria";
935
952
  const reviewPoints = state.model.resources[type]?.guidance?.reviewPoints || [];
936
953
  if (!reviewPoints.length) return "";
954
+ if (trigger) return '<button type="button" class="workflow-completion-requirements" data-completion-requirements="' + esc(type) + '">Completion requirements</button>';
937
955
  const points = '<ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>';
938
956
  return collapsed
939
- ? '<details class="resource-review-criteria compact"><summary>Review criteria</summary>' + points + '</details>'
940
- : '<section class="resource-review-criteria panel detail-support-panel detail-review-panel"><div class="panel-head"><h3>Review criteria</h3></div>' + points + '</section>';
957
+ ? '<details class="resource-review-criteria compact"><summary>' + esc(title) + '</summary>' + points + '</details>'
958
+ : '<section class="resource-review-criteria panel detail-support-panel detail-review-panel"><div class="panel-head"><h3>' + esc(title) + '</h3></div>' + points + '</section>';
959
+ }
960
+
961
+ function openCompletionRequirementsDialog(type) {
962
+ const definition = state.model.resources[type];
963
+ const reviewPoints = definition?.guidance?.reviewPoints || [];
964
+ if (!reviewPoints.length) return;
965
+ const dialog = document.createElement("dialog");
966
+ dialog.className = "alert-dialog completion-requirements-dialog";
967
+ dialog.innerHTML = '<div class="dialog-head"><div><p class="kicker">Completion requirements</p><h2>' + esc(titleCase(definition.title)) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Check these before considering this record complete.</p><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul><div class="dialog-actions"><button type="button" class="button primary">Close</button></div>';
968
+ document.body.append(dialog);
969
+ const close = () => dialog.close();
970
+ dialog.querySelectorAll("button").forEach((button) => button.addEventListener("click", close));
971
+ dialog.addEventListener("close", () => dialog.remove(), { once: true });
972
+ dialog.showModal();
941
973
  }
942
974
 
943
975
  function recordWorkflowItems(type, id) {
@@ -2841,8 +2873,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2841
2873
  : personParticipation(entry);
2842
2874
  const supportPanels = renderDetailSupport({
2843
2875
  hasRecordBody,
2844
- workflowPanel: workflowGuidance({ type, id, title: "Next steps" }),
2845
- reviewPanel: resourceReviewCriteria(type),
2876
+ workflowPanel: workflowGuidance({ type, id, title: "Next steps", workflow: entry.workflow }) || recordCompletionState(entry.record),
2846
2877
  metadataPanel: '<section class="panel detail-support-panel detail-metadata-panel"><div class="panel-head"><h3>Record details</h3></div><dl class="metadata">' + sourceMetadata + visible.map(([name, value]) => '<div><dt>' + esc(fields[name]?.label || humanize(name)) + '</dt><dd>' + formatValue(name === "status" ? displayStatus(entry.record) : value, name, type) + '</dd></div>').join("") + '</dl></section>',
2847
2878
  attachmentPanel,
2848
2879
  participationPanel,
@@ -2850,6 +2881,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2850
2881
  historyPanel
2851
2882
  });
2852
2883
  main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence &amp; Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' + (!definition.singleton ? '<button class="button danger" id="delete-resource">Delete</button>' : "") : "") + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain + supportPanels + '</div></div>';
2884
+ main.querySelectorAll("[data-completion-requirements]").forEach((button) => button.addEventListener("click", () => openCompletionRequirementsDialog(button.dataset.completionRequirements)));
2853
2885
  main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
2854
2886
  main.querySelector("[data-external-reviewer-governance]")?.addEventListener("click", openExternalReviewerGovernanceDialog);
2855
2887
  main.querySelector("[data-next-audit-cycle]")?.addEventListener("click", () => openNextAuditCycleDialog(entry.record));
@@ -2987,11 +3019,11 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2987
3019
  }
2988
3020
  }
2989
3021
 
2990
- function renderDetailSupport({ hasRecordBody, workflowPanel, reviewPanel, metadataPanel, attachmentPanel, participationPanel, connectionsPanel, historyPanel }) {
2991
- const panels = workflowPanel + reviewPanel + metadataPanel + attachmentPanel + participationPanel + connectionsPanel + historyPanel;
3022
+ function renderDetailSupport({ hasRecordBody, workflowPanel, metadataPanel, attachmentPanel, participationPanel, connectionsPanel, historyPanel }) {
3023
+ const panels = workflowPanel + metadataPanel + attachmentPanel + participationPanel + connectionsPanel + historyPanel;
2992
3024
  if (hasRecordBody) return '<aside>' + panels + '</aside>';
2993
3025
  const stacks = [
2994
- { name: "guidance", content: workflowPanel + reviewPanel },
3026
+ { name: "guidance", content: workflowPanel },
2995
3027
  { name: "record", content: metadataPanel + attachmentPanel + historyPanel },
2996
3028
  { name: "relationships", content: participationPanel + connectionsPanel }
2997
3029
  ].filter(({ content }) => content);
@@ -3023,7 +3055,7 @@ async function loadResourceDetail(type, id) {
3023
3055
  let token = state.stateToken;
3024
3056
  let detail = null;
3025
3057
  for (let attempt = 0; attempt < 3; attempt += 1) {
3026
- const tokenQuery = token ? "?token=" + encodeURIComponent(token) + "&history=false" : "?history=false";
3058
+ const tokenQuery = token ? "?token=" + encodeURIComponent(token) + "&history=false&workflow=true" : "?history=false&workflow=true";
3027
3059
  const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
3028
3060
  if (response.status === 409 && token) {
3029
3061
  await refreshExpiredAppState(token);
@@ -5971,6 +6003,9 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
5971
6003
  .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}
5972
6004
  .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}
5973
6005
  .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}
6006
+ .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}
6007
+ .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}
6008
+ @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}}
5974
6009
  .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)}
5975
6010
  .policy-lifecycle-note.single{grid-template-columns:1fr}
5976
6011
  .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-count{margin:9px 2px 0;color:var(--muted);font-size:10.8px;text-align:right}
@@ -6142,6 +6177,9 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
6142
6177
  .record-table td:has(.relation){min-width:150px}
6143
6178
  .record-table .relation{border-radius:7px;line-height:1.35;overflow-wrap:anywhere;word-break:normal}
6144
6179
  @media(max-width:760px){.record-table td:has(.relation){min-width:0}.record-table td[data-label]>.relation{grid-column:2}}
6180
+ @media(max-width:760px){.diagnostics>div{grid-template-columns:max-content minmax(0,1fr)}}
6181
+ .workflow-guidance .workflow-status{margin:4px 0 12px;color:var(--muted);font-size:10px}.workflow-completion-requirements{display:inline;appearance:none;-webkit-appearance:none;white-space:nowrap;padding:0;border:0;background:none;color:var(--accent);font:inherit;font-weight:750;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.workflow-completion-requirements:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.completion-requirements-dialog ul{margin:14px 0 0;padding-left:20px;color:var(--muted);font-size:12px;line-height:1.55}
6182
+ .record-completion-state{background:var(--surface-soft)}.record-completion-state h3{margin:4px 0 5px;font-size:16.8px}.record-completion-state p:last-child{margin:0;color:var(--muted);font-size:11px;line-height:1.45}
6145
6183
  `;
6146
6184
 
6147
6185
  function safeJson(value) {
package/src/workflow.js CHANGED
@@ -217,18 +217,34 @@ export function buildWorkflowDelta(before, after) {
217
217
  }
218
218
 
219
219
  export function workflowForResource(workflow, type, id) {
220
- if (!workflow) return { findings: [], workItems: [], recommended: null };
220
+ if (!workflow) return {
221
+ findings: [],
222
+ workItems: [],
223
+ related: { findings: [], workItems: [], recommended: null },
224
+ recommended: null
225
+ };
221
226
  const matches = (item) => item.subject?.type === type && item.subject?.id === id;
222
- const findings = workflow.findings.filter(matches);
223
- const workItems = workflow.workItems.filter((item) => (
224
- matches(item)
225
- || item.source?.type === type && item.source?.id === id
226
- ));
227
+ const related = (item) => (
228
+ item.source?.type === type
229
+ && item.source?.id === id
230
+ && !matches(item)
231
+ );
232
+ const directFindings = workflow.findings.filter(matches);
233
+ const directWorkItems = workflow.workItems.filter(matches);
234
+ const relatedFindings = workflow.findings.filter(related);
235
+ const relatedWorkItems = workflow.workItems.filter(related);
236
+ const findings = [...directFindings, ...relatedFindings];
237
+ const workItems = [...directWorkItems, ...relatedWorkItems];
227
238
  return {
228
239
  contractVersion: workflow.contractVersion,
229
240
  assessments: workflow.assessments,
230
241
  findings,
231
242
  workItems,
243
+ related: {
244
+ findings: relatedFindings,
245
+ workItems: relatedWorkItems,
246
+ recommended: [...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0] || null
247
+ },
232
248
  recommended: [...findings, ...workItems].sort(compareRecommended)[0] || null
233
249
  };
234
250
  }
@@ -430,7 +446,7 @@ function recordIncompleteReason(record, loaded, program) {
430
446
  return {
431
447
  state: "ready",
432
448
  requiredness: "required",
433
- message: "Review this criterion against the current service scope and record the applicability decision."
449
+ message: "Review this criterion against the current service scope, then record the applicability decision."
434
450
  };
435
451
  }
436
452
  if (record.type === "appointment" && record.status === "planned") {
@@ -440,16 +456,16 @@ function recordIncompleteReason(record, loaded, program) {
440
456
  return {
441
457
  state: "ready",
442
458
  requiredness: appointmentRequiredness(record, loaded.model),
443
- message: "Assign a holder, confirm the authority scope and independence needs, then activate this Appointment on its real start date."
459
+ message: "Assign a holder, confirm the authority and independence needed, then activate this Appointment on its actual start date."
444
460
  };
445
461
  }
446
462
  const messages = {
447
- draft: "Complete the record, its relationships, and required Markdown before moving it to review.",
448
- planned: "Review this planned record against the actual program and complete its finalization checks.",
449
- proposed: "Review the proposed work, owner, schedule, and completion profile before activating it.",
463
+ draft: "Complete the record, its relationships, and required Markdown before review.",
464
+ planned: "Check this planned record against the actual program, then complete its finalization checks.",
465
+ proposed: "Review the proposed work, owner, schedule, and completion requirements before activation.",
450
466
  "in-review": "Complete independent review and bind the approval to the exact content revision.",
451
467
  open: "Complete or formally dispose of this open work with the required proof.",
452
- "in-progress": "Finish the work, record its result, and link its completion proof.",
468
+ "in-progress": "Finish the work, record the result, and link the completion proof.",
453
469
  blocked: "Resolve the recorded blockers before completing this work.",
454
470
  "partially-implemented": "Finish the remaining control design, operation, source, and scheduling work."
455
471
  };
@@ -517,7 +533,7 @@ function finalizationFields(record, model, resources, program) {
517
533
  fields.push({
518
534
  field: "applicabilityReview",
519
535
  requiredness: "required",
520
- message: "Record or refresh the reviewed applicability decision, rationale, reviewer, and date. FileGRC records the current material scope automatically."
536
+ message: "Record or refresh the applicability decision, rationale, reviewer, and date. FileGRC records the current material scope automatically."
521
537
  });
522
538
  }
523
539
  if (record.type === "policy" && model.resources.policy?.fields?.programRole && !record.programRole) {
@@ -570,7 +586,7 @@ function finalizationFields(record, model, resources, program) {
570
586
  fields.push({
571
587
  field,
572
588
  requiredness: "conditional",
573
- message: `Complete ${fieldLabel(field)} before treating this ${fieldLabel(record.type).toLowerCase()} as finalized.`
589
+ message: `Add ${fieldLabel(field)} before treating this ${fieldLabel(record.type).toLowerCase()} as complete.`
574
590
  });
575
591
  }
576
592
  return fields;
@@ -676,9 +692,9 @@ async function sourceCoverageFindings(loaded, program) {
676
692
  ? "The source family has a reviewed authoritative path, valid coverage dates, retrieval ownership, and reconciliation method."
677
693
  : record
678
694
  ? record.status === "active"
679
- ? "Complete any missing source details and link a passed retrieval test after a candidate period is set."
680
- : "Finish the planned source-family decision, retrieval method, retention, validity dates, and pre-period dry run when a candidate period is set."
681
- : "Create a source-family coverage record and choose FileGRC, an external authoritative System, reviewed zero population, or reviewed not applicable.",
695
+ ? "Add missing source details and link a passed retrieval test after setting a candidate period."
696
+ : "Finish the source decision, retrieval method, retention, validity dates, and pre-period dry run after setting a candidate period."
697
+ : "Create a source-coverage record and choose FileGRC, an external authoritative System, zero population, or not applicable.",
682
698
  subject: { type: "source-coverage", ...(record ? { id: record.id } : {}) },
683
699
  dependencies: [],
684
700
  actions: record