filegrc 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/model/index.js +4 -4
- package/model/v7.json +10359 -0
- package/package.json +1 -1
- package/src/applicability-scope.js +211 -0
- package/src/audit-preparation.js +156 -20
- package/src/batch-review.js +40 -24
- package/src/cli.js +20 -7
- package/src/collection-review.js +16 -3
- package/src/collection-revision.js +23 -3
- package/src/collection-scope.js +82 -7
- package/src/document-activation.js +13 -1
- package/src/model-migration.js +224 -34
- package/src/obligations.js +128 -31
- package/src/policy-activation.js +5 -0
- package/src/program-lifecycle.js +1 -1
- package/src/program-path.js +1 -4
- package/src/program-readiness.js +20 -3
- package/src/reconciliation.js +15 -3
- package/src/server.js +2 -1
- package/src/setup.js +1 -1
- package/src/state.js +2 -1
- package/src/validate.js +25 -7
- package/src/web.js +121 -43
- package/src/workflow.js +7 -6
package/src/program-lifecycle.js
CHANGED
|
@@ -22,7 +22,7 @@ export function governedDocumentIsOperating(document, asOf, model) {
|
|
|
22
22
|
if (document?.type !== "document" || document.status !== "active") return false;
|
|
23
23
|
if (!document.effectiveOn || document.effectiveOn > asOf) return false;
|
|
24
24
|
if (!modelSupports(model, "governed-document-activation")) return true;
|
|
25
|
-
if (
|
|
25
|
+
if (["legacy-v4", "historical"].includes(document.activationBasis)) {
|
|
26
26
|
return document.workflowScope === "engagement"
|
|
27
27
|
&& Boolean(document.approvedOn && document.approvedContentRevisions);
|
|
28
28
|
}
|
package/src/program-path.js
CHANGED
|
@@ -87,11 +87,7 @@ export const PROGRAM_PATH = [
|
|
|
87
87
|
"filegrc guide appointment --json",
|
|
88
88
|
"filegrc guide system --json",
|
|
89
89
|
"filegrc guide component --json",
|
|
90
|
-
"filegrc review-collection person --scaffold",
|
|
91
|
-
"filegrc review-collection framework --scaffold",
|
|
92
90
|
"filegrc review-collection vendor --scaffold",
|
|
93
|
-
"filegrc review-collection system --scaffold",
|
|
94
|
-
"filegrc review-collection component --scaffold",
|
|
95
91
|
"filegrc list system --json"
|
|
96
92
|
]
|
|
97
93
|
},
|
|
@@ -132,6 +128,7 @@ export const PROGRAM_PATH = [
|
|
|
132
128
|
"filegrc get CONTROL_ID --mutation",
|
|
133
129
|
"filegrc guide obligation --json",
|
|
134
130
|
"filegrc list obligation --json",
|
|
131
|
+
"filegrc review-collection component --scaffold",
|
|
135
132
|
"filegrc review-collection complementary-control --scaffold",
|
|
136
133
|
"filegrc activate-content --scaffold",
|
|
137
134
|
"filegrc activate-policies --scaffold",
|
package/src/program-readiness.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { modelSupports } from "../model/index.js";
|
|
3
|
+
import { applicabilityReviewIsCurrent } from "./applicability-scope.js";
|
|
3
4
|
import { assessRequiredAppointments } from "./appointments.js";
|
|
4
5
|
import { assessCollectionReviews } from "./collection-review.js";
|
|
5
6
|
import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
|
|
@@ -52,6 +53,9 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
52
53
|
.map(collectionReviewReadinessItem));
|
|
53
54
|
const sourceStage = await evidenceSourcesStage(scope, byId, loaded.model, readMarkdown);
|
|
54
55
|
controlStage.items.push(...sourceStage.items);
|
|
56
|
+
controlStage.items.push(...collectionReviews
|
|
57
|
+
.filter(({ resourceType }) => resourceType === "component")
|
|
58
|
+
.map(collectionReviewReadinessItem));
|
|
55
59
|
const governedContent = await governedContentItems(scope, records, byId, readMarkdown, asOf, loaded.model);
|
|
56
60
|
controlStage.items.push(...governedContent.items);
|
|
57
61
|
const policyActivations = await assessPolicyActivations(
|
|
@@ -72,7 +76,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
72
76
|
records,
|
|
73
77
|
byId,
|
|
74
78
|
loaded.model,
|
|
75
|
-
collectionReviews.filter(({ resourceType }) =>
|
|
79
|
+
collectionReviews.filter(({ resourceType }) => ["person", "framework", "system", "vendor"].includes(resourceType))
|
|
76
80
|
),
|
|
77
81
|
policyStage,
|
|
78
82
|
controlStage
|
|
@@ -103,6 +107,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
103
107
|
const firstAction = items.find((current) => current.status === "action") || null;
|
|
104
108
|
|
|
105
109
|
return {
|
|
110
|
+
program,
|
|
106
111
|
schemaVersion: 1,
|
|
107
112
|
dataModelVersion: String(loaded.model.modelVersion),
|
|
108
113
|
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
@@ -261,7 +266,10 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
261
266
|
record.status === "active"
|
|
262
267
|
&& record.statement
|
|
263
268
|
&& record.effectiveOn
|
|
264
|
-
&& (
|
|
269
|
+
&& (!model.resources.commitment?.fields?.applicabilityReview || (
|
|
270
|
+
record.applicabilityReview?.decision === "applicable"
|
|
271
|
+
&& applicabilityReviewIsCurrent(record.applicabilityReview, record, workspace, records, model)
|
|
272
|
+
))
|
|
265
273
|
&& currentPartyPeople(record.ownerIds, byId).size > 0
|
|
266
274
|
&& (record.requirementIds || []).length > 0
|
|
267
275
|
&& (record.controlIds || []).length > 0
|
|
@@ -290,7 +298,15 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
const selectedRequirementIds = new Set(scope.requirements.map((record) => record.id));
|
|
293
|
-
const
|
|
301
|
+
const requirementById = new Map(records
|
|
302
|
+
.filter(({ type }) => type === "requirement")
|
|
303
|
+
.map((record) => [record.id, record]));
|
|
304
|
+
const v4Decisions = new Map((workspace?.requirementApplicability || [])
|
|
305
|
+
.filter((decision) => (
|
|
306
|
+
requirementById.has(decision.requirementId)
|
|
307
|
+
&& applicabilityReviewIsCurrent(decision, requirementById.get(decision.requirementId), workspace, records, model)
|
|
308
|
+
))
|
|
309
|
+
.map((decision) => [decision.requirementId, decision.decision]));
|
|
294
310
|
const applicableRequirements = records.filter((record) => (
|
|
295
311
|
record.type === "requirement"
|
|
296
312
|
&& scope.frameworks.some((framework) => framework.id === record.frameworkId)
|
|
@@ -1162,6 +1178,7 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1162
1178
|
const checks = {
|
|
1163
1179
|
...(model.resources.control?.fields?.applicabilityReview ? {
|
|
1164
1180
|
applicability: control.applicabilityReview?.decision === "applicable"
|
|
1181
|
+
&& applicabilityReviewIsCurrent(control.applicabilityReview, control, scope.program, [...byId.values()], model)
|
|
1165
1182
|
} : {}),
|
|
1166
1183
|
implemented: control.status === "implemented",
|
|
1167
1184
|
owner: (control.ownerIds || []).length > 0,
|
package/src/reconciliation.js
CHANGED
|
@@ -11,7 +11,9 @@ const TRANSITIONS = {
|
|
|
11
11
|
person: [
|
|
12
12
|
{
|
|
13
13
|
eventType: "person-started",
|
|
14
|
-
applies: (before, after) => after?.status === "active"
|
|
14
|
+
applies: (before, after) => after?.status === "active"
|
|
15
|
+
&& after?.affiliation !== "external"
|
|
16
|
+
&& before?.status !== "active",
|
|
15
17
|
message: "Confirm whether activating this Person represents a workforce start that needs policy-event work."
|
|
16
18
|
},
|
|
17
19
|
{
|
|
@@ -95,16 +97,26 @@ export async function planReconciliation(input = process.cwd()) {
|
|
|
95
97
|
const loaded = input?.resources && input?.model && input?.entries
|
|
96
98
|
? input
|
|
97
99
|
: await loadWorkspace(input);
|
|
100
|
+
const headRevision = gitRevision(loaded.root);
|
|
98
101
|
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
99
102
|
return {
|
|
100
103
|
contractVersion: 1,
|
|
101
|
-
gitRevision:
|
|
104
|
+
gitRevision: headRevision,
|
|
102
105
|
changedPaths: [],
|
|
103
106
|
candidates: [],
|
|
104
107
|
message: "Direct-file transition reconciliation is available in model v3 and newer workspaces."
|
|
105
108
|
};
|
|
106
109
|
}
|
|
107
110
|
const changedPaths = gitChangedPaths(loaded.root);
|
|
111
|
+
if (!headRevision) {
|
|
112
|
+
return {
|
|
113
|
+
contractVersion: 1,
|
|
114
|
+
gitRevision: null,
|
|
115
|
+
changedPaths,
|
|
116
|
+
candidates: [],
|
|
117
|
+
message: "Commit the initial workspace before FileGRC checks later direct-file changes for Policy Events."
|
|
118
|
+
};
|
|
119
|
+
}
|
|
108
120
|
const currentByPath = new Map(loaded.entries.map((entry) => [
|
|
109
121
|
`data/${entry.relativePath}`,
|
|
110
122
|
entry
|
|
@@ -176,7 +188,7 @@ export async function planReconciliation(input = process.cwd()) {
|
|
|
176
188
|
}
|
|
177
189
|
return {
|
|
178
190
|
contractVersion: 1,
|
|
179
|
-
gitRevision:
|
|
191
|
+
gitRevision: headRevision,
|
|
180
192
|
changedPaths,
|
|
181
193
|
candidates: candidates.sort((a, b) => a.id.localeCompare(b.id))
|
|
182
194
|
};
|
package/src/server.js
CHANGED
|
@@ -319,7 +319,8 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
319
319
|
const payload = await readJson(request);
|
|
320
320
|
const completeSetup = async () => {
|
|
321
321
|
return browserMutation(input, options, {
|
|
322
|
-
message: (setupResult) => `${payload.draft === true ? "Save onboarding draft" : "Complete onboarding"} for ${setupResult.workspace.organizationName}
|
|
322
|
+
message: (setupResult) => `${payload.draft === true ? "Save onboarding draft" : "Complete onboarding"} for ${setupResult.workspace.organizationName}`,
|
|
323
|
+
fastResponse: prefersFastMutation(request)
|
|
323
324
|
}, () => setupWorkspace(input, payload));
|
|
324
325
|
};
|
|
325
326
|
return json(response, 200, await completeSetup());
|
package/src/setup.js
CHANGED
|
@@ -81,7 +81,7 @@ export function summarizeSetupResult(result) {
|
|
|
81
81
|
},
|
|
82
82
|
system: setupSystemSummary(result.system),
|
|
83
83
|
target: setupTargetSummary(result.program || result.workspace, {
|
|
84
|
-
modelVersion: result.workspace?.dataModelVersion || (result.program ? "
|
|
84
|
+
modelVersion: result.workspace?.dataModelVersion || (result.program ? "7" : "3")
|
|
85
85
|
}),
|
|
86
86
|
renderer: result.renderer ? setupRendererSummary(result.renderer) : null,
|
|
87
87
|
commitment: result.commitment || null,
|
package/src/state.js
CHANGED
|
@@ -94,8 +94,9 @@ async function createAppStateUnlocked(input, options) {
|
|
|
94
94
|
(audits.length ? audits : [null]).map(async (audit) => {
|
|
95
95
|
const preparation = await assessAuditPreparation(loaded, {
|
|
96
96
|
auditId: audit?.id,
|
|
97
|
+
asOf,
|
|
97
98
|
generatedAt,
|
|
98
|
-
programReadiness
|
|
99
|
+
...(audit ? {} : { programReadiness })
|
|
99
100
|
});
|
|
100
101
|
return [audit?.id || "none", preparation];
|
|
101
102
|
})
|
package/src/validate.js
CHANGED
|
@@ -3,7 +3,10 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { getResourceDefinition, modelSupports } from "../model/index.js";
|
|
5
5
|
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
collectionRevision,
|
|
8
|
+
collectionRevisionMatches
|
|
9
|
+
} from "./collection-revision.js";
|
|
7
10
|
import { isSafeGitName } from "./git-name.js";
|
|
8
11
|
import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
|
|
9
12
|
import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
|
|
@@ -153,16 +156,20 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
diagnostics.sort((a, b) => `${a.severity}:${a.path}:${a.code}`.localeCompare(`${b.severity}:${b.path}:${b.code}`));
|
|
156
|
-
|
|
159
|
+
const result = {
|
|
157
160
|
ok: !diagnostics.some(({ severity }) => severity === "error"),
|
|
158
161
|
diagnostics,
|
|
159
162
|
counts: {
|
|
160
163
|
resources: loaded.resources.length,
|
|
161
164
|
errors: diagnostics.filter(({ severity }) => severity === "error").length,
|
|
162
165
|
warnings: diagnostics.filter(({ severity }) => severity === "warning").length
|
|
163
|
-
}
|
|
164
|
-
loaded
|
|
166
|
+
}
|
|
165
167
|
};
|
|
168
|
+
Object.defineProperty(result, "loaded", {
|
|
169
|
+
value: loaded,
|
|
170
|
+
enumerable: false
|
|
171
|
+
});
|
|
172
|
+
return result;
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagnostics) {
|
|
@@ -246,7 +253,7 @@ function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagno
|
|
|
246
253
|
`Program-scoped Document "${document.title}" cannot fill an Audit engagement or management-Document field.`
|
|
247
254
|
));
|
|
248
255
|
}
|
|
249
|
-
if (
|
|
256
|
+
if (!["legacy-v4", "historical"].includes(document.activationBasis)) continue;
|
|
250
257
|
const historicalAuditIds = new Set(auditRefs
|
|
251
258
|
.filter(({ audit }) => ["issued", "delivered", "complete"].includes(audit.status))
|
|
252
259
|
.map(({ audit }) => audit.id));
|
|
@@ -254,7 +261,7 @@ function validateDocumentWorkflowScopes(resources, model, byId, pathById, diagno
|
|
|
254
261
|
diagnostics.push(error(
|
|
255
262
|
"invalid-legacy-document-activation",
|
|
256
263
|
path,
|
|
257
|
-
`
|
|
264
|
+
`The historical activation basis is reserved for an engagement Document tied to exactly one issued, delivered, or completed Audit.`
|
|
258
265
|
));
|
|
259
266
|
}
|
|
260
267
|
}
|
|
@@ -313,7 +320,18 @@ function validateCollectionReview(record, loaded, byId, path, diagnostics) {
|
|
|
313
320
|
? record.authoritativeComponentId || record.authoritativeSystemId
|
|
314
321
|
: null
|
|
315
322
|
});
|
|
316
|
-
const current =
|
|
323
|
+
const current = collectionRevisionMatches(
|
|
324
|
+
loaded,
|
|
325
|
+
record.resourceType,
|
|
326
|
+
record.collectionRevision,
|
|
327
|
+
{
|
|
328
|
+
program,
|
|
329
|
+
authoritativeSourceId: record.decision === "externally-managed"
|
|
330
|
+
? record.authoritativeComponentId || record.authoritativeSystemId
|
|
331
|
+
: null,
|
|
332
|
+
currentRevision
|
|
333
|
+
}
|
|
334
|
+
);
|
|
317
335
|
if (current && !recordCount && record.decision === "complete") {
|
|
318
336
|
diagnostics.push(error(
|
|
319
337
|
"invalid-collection-review-decision",
|
package/src/web.js
CHANGED
|
@@ -102,7 +102,7 @@ start().catch((error) => {
|
|
|
102
102
|
async function start() {
|
|
103
103
|
const embedded = document.querySelector("#filegrc-data");
|
|
104
104
|
state = embedded ? JSON.parse(embedded.textContent) : await fetchJson("/api/state");
|
|
105
|
-
window.addEventListener("hashchange",
|
|
105
|
+
window.addEventListener("hashchange", handleRouteChange);
|
|
106
106
|
window.addEventListener("resize", positionCurrentOnboarding);
|
|
107
107
|
window.addEventListener("scroll", positionCurrentOnboarding, true);
|
|
108
108
|
render();
|
|
@@ -112,6 +112,11 @@ async function start() {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function handleRouteChange() {
|
|
116
|
+
document.querySelectorAll("dialog.editor[open], dialog.commit-dialog[open], dialog.alert-dialog[open]").forEach((dialog) => dialog.close());
|
|
117
|
+
render();
|
|
118
|
+
}
|
|
119
|
+
|
|
115
120
|
function render() {
|
|
116
121
|
resourceGuideCleanup?.();
|
|
117
122
|
resourceGuideCleanup = null;
|
|
@@ -287,20 +292,25 @@ function renderHome(main) {
|
|
|
287
292
|
const setupPending = !setupSystem
|
|
288
293
|
|| setupSystem.status !== "active"
|
|
289
294
|
|| activeProgram().assuranceGoal === "none";
|
|
290
|
-
const acceptedEventTriggers = state.obligations.triggers
|
|
291
|
-
const openObligations = state.obligations.items
|
|
295
|
+
const acceptedEventTriggers = activePolicyEventTriggers(state.obligations.triggers);
|
|
296
|
+
const openObligations = activeOperationItems(state.obligations.items);
|
|
292
297
|
const previewObligations = distinctObligationPreviews(openObligations, 3);
|
|
293
|
-
const obligationHeading = openObligations.some((item) => item.status !== "proposed") ? "Due Windows" : "Starter Proposals";
|
|
294
298
|
const setupBanner = setupPending ? initialSetupBanner() : "";
|
|
295
299
|
const auditPanel = program.evidenceReady
|
|
296
300
|
? '<section class="panel audit-panel"><div class="panel-head"><div><p class="kicker">Optional next phase</p><h3>' + esc(activeFirm ? titleCase(activeAudit.record.title) : "Target: " + program.target.label) + '</h3></div>' + (activeAudit ? '<a href="#/resource/audit/' + encodeURIComponent(activeAudit.record.id) + '">Open audit</a>' : '<a href="#/resources/audit">Engagements</a>') + '</div>' +
|
|
297
301
|
(activeAudit ? auditProgress(activeAudit.record) + auditEngagementPrompt(activeAudit.record) : auditEngagementPrompt()) + '</section>'
|
|
298
302
|
: "";
|
|
303
|
+
const obligationPanel = program.evidenceReady || openObligations.length
|
|
304
|
+
? '<section class="panel obligation-panel"><div class="panel-head"><div><p class="kicker">Policy obligations</p><h3>Due Windows</h3></div><a href="#/stage/run">Open board</a></div>' + obligationPreview(previewObligations) + '</section>'
|
|
305
|
+
: "";
|
|
306
|
+
const eventPanel = acceptedEventTriggers.length
|
|
307
|
+
? '<section class="panel event-reminder-panel"><div class="panel-head"><div><p class="kicker">Event reminders</p><h3>Did Something Change?</h3></div><a href="#/stage/run?section=events">Trigger work</a></div>' + eventReminderPreview(orderedPolicyEventTriggers(acceptedEventTriggers).slice(0, 4)) + '</section>'
|
|
308
|
+
: "";
|
|
309
|
+
const operationPanels = obligationPanel + eventPanel;
|
|
310
|
+
const overviewPanels = operationPanels + auditPanel;
|
|
299
311
|
main.innerHTML = '<div class="page home-page"><section class="hero overview-hero"><div><p class="kicker">Current program state</p><h2>' + esc(titleCase(state.workspace.title)) + '</h2><p>' + esc(state.workspace.description || "Governance, risk, controls, evidence, and audit work maintained as plain files in Git.") + '</p></div></section>' + setupBanner + readinessOverview() +
|
|
300
|
-
'<div class="overview-grid"
|
|
301
|
-
|
|
302
|
-
auditPanel + '</div></div>';
|
|
303
|
-
main.querySelector("#resume-setup")?.addEventListener("click", () => requestOnboarding({ setupOnly: Boolean(initialSetupSystem()) }));
|
|
312
|
+
(overviewPanels ? '<div class="overview-grid">' + overviewPanels + '</div>' : '') + '</div>';
|
|
313
|
+
main.querySelector("#resume-setup")?.addEventListener("click", () => requestOnboarding({ setupOnly: true }));
|
|
304
314
|
}
|
|
305
315
|
|
|
306
316
|
function initialSetupSystem() {
|
|
@@ -1116,7 +1126,7 @@ function stagePageItems(stage, destination) {
|
|
|
1116
1126
|
].filter((item) => (
|
|
1117
1127
|
activeStates.has(item.state)
|
|
1118
1128
|
&& (
|
|
1119
|
-
item.stage === stage.id
|
|
1129
|
+
workflowUiStage(item.stage) === stage.id
|
|
1120
1130
|
|| stage.id === "scope"
|
|
1121
1131
|
&& destination.type === "appointment"
|
|
1122
1132
|
&& item.code === "governance.appointment.independent-policy-reviewer"
|
|
@@ -1130,6 +1140,7 @@ function stagePageItems(stage, destination) {
|
|
|
1130
1140
|
));
|
|
1131
1141
|
}
|
|
1132
1142
|
return items.filter((item) => {
|
|
1143
|
+
if (destination.type === "program" && item.key === "program.scope.criteria") return false;
|
|
1133
1144
|
if (
|
|
1134
1145
|
destination.type === "requirement"
|
|
1135
1146
|
&& item.subject?.type === "requirement"
|
|
@@ -1162,6 +1173,10 @@ function stagePageItems(stage, destination) {
|
|
|
1162
1173
|
));
|
|
1163
1174
|
}
|
|
1164
1175
|
|
|
1176
|
+
function workflowUiStage(stageId) {
|
|
1177
|
+
return stageId === "operation" ? "run" : stageId;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1165
1180
|
function operationProgress() {
|
|
1166
1181
|
const program = state.programReadiness;
|
|
1167
1182
|
const goal = program?.target?.goal || activeProgram().assuranceGoal || "none";
|
|
@@ -1173,7 +1188,7 @@ function operationProgress() {
|
|
|
1173
1188
|
: Boolean(program?.evidenceReady);
|
|
1174
1189
|
const overdue = state.obligations.counts.overdue || 0;
|
|
1175
1190
|
const blocked = state.obligations.counts.blocked || 0;
|
|
1176
|
-
const complete = Boolean(program?.
|
|
1191
|
+
const complete = Boolean(program?.operating);
|
|
1177
1192
|
if (complete) {
|
|
1178
1193
|
return {
|
|
1179
1194
|
percent: 100,
|
|
@@ -1214,6 +1229,19 @@ function operationProgress() {
|
|
|
1214
1229
|
detail: "Record the management candidate period start when evidence collection begins."
|
|
1215
1230
|
};
|
|
1216
1231
|
}
|
|
1232
|
+
if (program?.evidenceReady && candidateStarted) {
|
|
1233
|
+
const actions = program?.stages?.find((stage) => stage.id === "operation")?.counts?.action || 0;
|
|
1234
|
+
return {
|
|
1235
|
+
percent: 0,
|
|
1236
|
+
complete: 0,
|
|
1237
|
+
total: 1,
|
|
1238
|
+
status: "Needs work",
|
|
1239
|
+
tone: "warn",
|
|
1240
|
+
detail: actions
|
|
1241
|
+
? actions + " Step 4 " + pluralize("item", actions) + (actions === 1 ? " needs" : " need") + " work."
|
|
1242
|
+
: "Complete the current Step 4 readiness work."
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1217
1245
|
return {
|
|
1218
1246
|
percent: 0,
|
|
1219
1247
|
complete: 0,
|
|
@@ -1278,42 +1306,67 @@ function renderExternalEvidenceSection() {
|
|
|
1278
1306
|
function renderObligations(main, params = new URLSearchParams()) {
|
|
1279
1307
|
const stage = READINESS_STAGES.find((candidate) => candidate.id === "run");
|
|
1280
1308
|
const plan = state.obligations;
|
|
1309
|
+
const operationLocked = !state.programReadiness?.evidenceReady;
|
|
1281
1310
|
const visibleCardLimit = 6;
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
const cards =
|
|
1285
|
-
const more =
|
|
1286
|
-
? '<button class="button obligation-more" type="button" data-expand-obligations="' + status + '" data-total="' +
|
|
1311
|
+
const renderBoard = (items, statuses) => statuses.map((status) => {
|
|
1312
|
+
const statusItems = obligationBoardItems(items, status);
|
|
1313
|
+
const cards = statusItems.map((item, index) => obligationCard(item, index >= visibleCardLimit)).join("");
|
|
1314
|
+
const more = statusItems.length > visibleCardLimit
|
|
1315
|
+
? '<button class="button obligation-more" type="button" data-expand-obligations="' + status + '" data-total="' + statusItems.length + '" aria-expanded="false">Show ' + (statusItems.length - visibleCardLimit) + ' more</button>'
|
|
1287
1316
|
: "";
|
|
1288
|
-
return '<section class="obligation-column" data-obligation-column="' + status + '"><div class="obligation-column-head"><span class="badge status-' + status + '">' + esc(properCase(status)) + '</span><strong>' +
|
|
1317
|
+
return '<section class="obligation-column" data-obligation-column="' + status + '"><div class="obligation-column-head"><span class="badge status-' + status + '">' + esc(properCase(status)) + '</span><strong>' + statusItems.length + '</strong></div><div class="obligation-cards">' + (statusItems.length ? cards : empty("Nothing " + status + ".")) + '</div>' + more + '</section>';
|
|
1289
1318
|
}).join("");
|
|
1290
1319
|
const eventTriggerLimit = 6;
|
|
1291
1320
|
const orderedTriggers = orderedPolicyEventTriggers(plan.triggers);
|
|
1292
|
-
const
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1321
|
+
const acceptedTriggers = activePolicyEventTriggers(orderedTriggers);
|
|
1322
|
+
const proposedTriggers = orderedTriggers.filter(({ programStatus }) => programStatus === "proposed");
|
|
1323
|
+
const currentItems = activeOperationItems(plan.items);
|
|
1324
|
+
const proposedItems = plan.items.filter(({ status }) => status === "proposed");
|
|
1325
|
+
const renderEvents = (items, scope, description) => {
|
|
1326
|
+
const triggers = items.map((trigger, index) => policyEventTrigger(trigger, index, index >= eventTriggerLimit, scope)).join("");
|
|
1327
|
+
const eventMore = items.length > eventTriggerLimit
|
|
1328
|
+
? '<button class="button policy-event-more" type="button" data-expand-policy-events="' + scope + '" data-total="' + items.length + '" aria-expanded="false">Show ' + (items.length - eventTriggerLimit) + ' more events</button>'
|
|
1329
|
+
: "";
|
|
1330
|
+
return '<section class="workflow-section event-reminders" data-policy-event-section="' + scope + '"><div class="section-head"><div><p class="kicker">Changes that create work</p><h2>Policy Events</h2><p>' + description + '</p></div></div><div class="policy-event-list">' + (triggers || empty("No event-driven obligations are configured.")) + '</div>' + eventMore + '</section>';
|
|
1331
|
+
};
|
|
1332
|
+
const renderQueue = (items, statuses, description) => '<section class="workflow-section work-queue-section"><div class="section-head"><div><p class="kicker">Recurring, event, and assigned work</p><h2>Work Queue</h2><p>' + description + '</p></div><div class="page-actions">' + (!state.readOnly ? '<button class="button" type="button" data-new-action-item>New task</button>' : "") + '<a class="button" href="#/resources/obligation">Edit schedules</a></div></div><div class="obligation-board">' + renderBoard(items, statuses) + '</div></section>';
|
|
1296
1333
|
const feedback = policyEventFeedback
|
|
1297
1334
|
? '<section class="policy-event-feedback" role="status" aria-live="polite"><span class="status-dot good"></span><div><strong>Work added to the Work Queue</strong><p>' + esc(policyEventFeedback.name + " created " + policyEventFeedback.taskCount + " " + pluralize("task", policyEventFeedback.taskCount) + ".") + '</p></div><button class="button" type="button" data-view-added-work>View Work Queue</button><button class="icon-button" type="button" data-dismiss-policy-event-feedback aria-label="Dismiss confirmation">×</button></section>'
|
|
1298
1335
|
: "";
|
|
1336
|
+
const operationGate = operationLocked
|
|
1337
|
+
? '<section class="workflow-section operation-gate"><div><p class="kicker">Finish Step 3 first</p><h2>Get the program Evidence Ready</h2><p>Confirm the remaining Controls, schedules, evidence sources, and governed content. Any adopted work and Policy Events that can happen now stay available below.</p></div><a class="button primary" href="#/stage/controls">Continue Evidence Ready work</a></section>'
|
|
1338
|
+
: "";
|
|
1339
|
+
const activeOperation = operationLocked
|
|
1340
|
+
? (acceptedTriggers.length ? renderEvents(acceptedTriggers, "active", "Trigger the matching workflow when an event occurs. filegrc adds every required action to the Work Queue with its owner and deadline.") : "") +
|
|
1341
|
+
(currentItems.length ? renderQueue(currentItems, ["upcoming", "blocked", "due", "overdue"], "Complete work the program has already adopted. Each card shows its due window, source, and next action.") : "")
|
|
1342
|
+
: "";
|
|
1343
|
+
const setupTriggers = operationLocked ? proposedTriggers : orderedTriggers;
|
|
1344
|
+
const setupItems = operationLocked ? proposedItems : plan.items;
|
|
1345
|
+
const setupStatuses = operationLocked ? ["proposed"] : ["proposed", "upcoming", "blocked", "due", "overdue"];
|
|
1346
|
+
const operationSetupOpen = operationLocked ? "" : " open";
|
|
1347
|
+
const operationSetupSummary = operationLocked
|
|
1348
|
+
? '<summary>Preview ' + proposedTriggers.length + ' proposed Policy Events and ' + proposedItems.length + ' proposed Work Queue items</summary>'
|
|
1349
|
+
: "";
|
|
1299
1350
|
main.innerHTML = '<div class="page obligation-board-page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
1300
1351
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(stageProgress(stage)) + '</section>' +
|
|
1301
1352
|
feedback +
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
'<
|
|
1305
|
-
|
|
1353
|
+
operationGate +
|
|
1354
|
+
activeOperation +
|
|
1355
|
+
'<details class="operation-setup-preview"' + operationSetupOpen + '>' + operationSetupSummary +
|
|
1356
|
+
renderEvents(setupTriggers, "setup", operationLocked ? "Review these starter workflows. They become available when their governing work is adopted." : "Trigger the matching workflow when an event occurs. filegrc adds every required action to the Work Queue with its owner and deadline.") +
|
|
1357
|
+
renderQueue(setupItems, setupStatuses, operationLocked ? "Review proposed schedules before adopting them." : "Complete scheduled work and assigned follow-up here. Each card shows its due window, source, and next action.") +
|
|
1358
|
+
renderExternalEvidenceSection() + '</details></div>';
|
|
1306
1359
|
main.querySelectorAll("[data-start-event]").forEach((button) => button.addEventListener("click", () => {
|
|
1307
1360
|
const trigger = plan.triggers.find((item) => item.eventType === button.dataset.startEvent);
|
|
1308
1361
|
if (trigger) openObligationEventDialog(trigger);
|
|
1309
1362
|
}));
|
|
1310
|
-
main.
|
|
1363
|
+
main.querySelectorAll("[data-expand-policy-events]").forEach((button) => button.addEventListener("click", (event) => {
|
|
1311
1364
|
const button = event.currentTarget;
|
|
1312
1365
|
const expanded = button.getAttribute("aria-expanded") === "true";
|
|
1313
|
-
|
|
1366
|
+
button.closest("[data-policy-event-section]").querySelectorAll(".policy-event-row[data-collapsed]").forEach((row) => { row.hidden = expanded; });
|
|
1314
1367
|
button.setAttribute("aria-expanded", String(!expanded));
|
|
1315
|
-
button.textContent = expanded ? "Show " + (
|
|
1316
|
-
});
|
|
1368
|
+
button.textContent = expanded ? "Show " + (Number(button.dataset.total) - eventTriggerLimit) + " more events" : "Show fewer events";
|
|
1369
|
+
}));
|
|
1317
1370
|
main.querySelector("[data-view-added-work]")?.addEventListener("click", () => main.querySelector(".work-queue-section")?.scrollIntoView({ behavior: "smooth", block: "start" }));
|
|
1318
1371
|
main.querySelector("[data-dismiss-policy-event-feedback]")?.addEventListener("click", (event) => {
|
|
1319
1372
|
policyEventFeedback = null;
|
|
@@ -1336,9 +1389,9 @@ function renderObligations(main, params = new URLSearchParams()) {
|
|
|
1336
1389
|
const item = plan.items.find((candidate) => candidate.key === button.dataset.completeAction);
|
|
1337
1390
|
if (item) openActionCompletion(item);
|
|
1338
1391
|
}));
|
|
1339
|
-
main.
|
|
1392
|
+
main.querySelectorAll("[data-new-action-item]").forEach((button) => button.addEventListener("click", () => openEditor("action-item", null, {
|
|
1340
1393
|
description: "Create a task only when follow-up from another record needs its own assignee, deadline, and completion proof. Point it to that source record; it will remain in Work Queue until done or canceled."
|
|
1341
|
-
}));
|
|
1394
|
+
})));
|
|
1342
1395
|
const requestedEvent = params.get("event");
|
|
1343
1396
|
if (requestedEvent || params.get("section") === "events") {
|
|
1344
1397
|
queueMicrotask(() => {
|
|
@@ -1391,8 +1444,16 @@ function orderedPolicyEventTriggers(triggers) {
|
|
|
1391
1444
|
));
|
|
1392
1445
|
}
|
|
1393
1446
|
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1447
|
+
function activePolicyEventTriggers(triggers) {
|
|
1448
|
+
return triggers.filter(({ programStatus }) => programStatus === "accepted");
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function activeOperationItems(items) {
|
|
1452
|
+
return items.filter(({ status }) => ["upcoming", "blocked", "due", "overdue"].includes(status));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function policyEventTrigger(trigger, index, collapsed = false, scope = "events") {
|
|
1456
|
+
const tooltipId = "policy-event-tooltip-" + scope + "-" + index;
|
|
1396
1457
|
const proposed = trigger.programStatus === "proposed";
|
|
1397
1458
|
const unavailable = state.readOnly || proposed;
|
|
1398
1459
|
const availability = proposed
|
|
@@ -1473,7 +1534,7 @@ function actionCompletionPlan(item) {
|
|
|
1473
1534
|
href: "#/resource/action-item/" + encodeURIComponent(action.record.id)
|
|
1474
1535
|
};
|
|
1475
1536
|
}
|
|
1476
|
-
const type = state.model.obligationActivities?.[obligation.record.activityType]?.completionType;
|
|
1537
|
+
const type = item.completionType || state.model.obligationActivities?.[obligation.record.activityType]?.completionType;
|
|
1477
1538
|
if (!type) {
|
|
1478
1539
|
return {
|
|
1479
1540
|
blocked: "Review completion type",
|
|
@@ -1484,7 +1545,7 @@ function actionCompletionPlan(item) {
|
|
|
1484
1545
|
}
|
|
1485
1546
|
|
|
1486
1547
|
function obligationCompletionPlan(item) {
|
|
1487
|
-
const type = state.model.obligationActivities?.[item.activityType]?.completionType || "evidence";
|
|
1548
|
+
const type = item.completionType || state.model.obligationActivities?.[item.activityType]?.completionType || "evidence";
|
|
1488
1549
|
if (!currentPeopleForParties(item.ownerIds || []).length) {
|
|
1489
1550
|
return { type, blocked: "Assign current owner", href: "#/resource/obligation/" + encodeURIComponent(item.obligationId) };
|
|
1490
1551
|
}
|
|
@@ -1552,10 +1613,13 @@ function obligationCompletionSeed(type, item, obligation) {
|
|
|
1552
1613
|
return { ...common, status: "complete", scopeResourceIds: obligation.scopeResourceIds || [], reviewerIds: reviewerPeople, completedOn: date, outcome: "passed", changesRequired: false, evidenceIds: [], coverage: rangeCoverage(item.dueWindowStart, item.dueWindowEnd) };
|
|
1553
1614
|
}
|
|
1554
1615
|
if (type === "risk-assessment") {
|
|
1555
|
-
return { ...common, status: "complete", completedOn: date, assessmentKind: "enterprise-risk", scope: "In-scope SOC 2 systems and dependencies", assessorIds: responsiblePeople, reviewerIds: reviewerPeople, methodology:
|
|
1616
|
+
return { ...common, status: "complete", completedOn: date, assessmentKind: "enterprise-risk", scope: "In-scope SOC 2 systems and dependencies", assessorIds: responsiblePeople, reviewerIds: reviewerPeople, methodology: activeProgram().riskMethodology?.method || "Documented risk methodology", summary: "Assessment completed; link the supporting evidence and resulting risks.", evidenceIds: [], approvedOn: date };
|
|
1556
1617
|
}
|
|
1557
1618
|
if (type === "attestation") {
|
|
1558
|
-
|
|
1619
|
+
const personId = [...(item.subjectResourceIds || []), ...(obligation.scopeResourceIds || [])].find((id) => state.resources.some(({ record }) => record.id === id && record.type === "person"));
|
|
1620
|
+
const primarySubjectIds = [...new Set([obligation.templateResourceId, ...(obligation.scopeResourceIds || [])].filter((id) => state.resources.some(({ record }) => record.id === id && ["policy", "document", "training", "action-item"].includes(record.type))))];
|
|
1621
|
+
const subjectResourceIds = primarySubjectIds.length ? primarySubjectIds : [...(obligation.policyIds || [])];
|
|
1622
|
+
return { ...common, status: "completed", subjectResourceIds, personId, attestationKind: item.activityType || "completion", assignedOn: item.dueWindowStart, dueOn: item.dueWindowEnd, completedOn: date, attestationMethod: "git-approval" };
|
|
1559
1623
|
}
|
|
1560
1624
|
if (type === "access-review") {
|
|
1561
1625
|
return { ...common, status: "complete", completedOn: date, reviewerIds: responsiblePeople, systemIds: inScopeSystems, scope: "Privileged, production, and important-system access", outcome: "passed", evidenceIds: [], approvedByIds: reviewerPeople, approvedOn: date, coverage: rangeCoverage(item.dueWindowStart, item.dueWindowEnd) };
|
|
@@ -1570,15 +1634,23 @@ function obligationCompletionSeed(type, item, obligation) {
|
|
|
1570
1634
|
return { ...common, status: "complete", controlId: obligation.controlIds?.[0] || "", testKinds: [item.activityType || "control-operation"], performedBy: "management", testerIds: responsiblePeople, reviewerIds: reviewerPeople, completedOn: date, reviewedOn: date, outcome: "passed", evidenceIds: [], coverage: rangeCoverage(item.dueWindowStart, item.dueWindowEnd) };
|
|
1571
1635
|
}
|
|
1572
1636
|
if (type === "control-activity") {
|
|
1637
|
+
const allowedScopeTypes = new Set(state.model.relationGroups?.["obligation-scope"] || []);
|
|
1638
|
+
const requestedScopeIds = item.subjectResourceIds || item.scopeResourceIds || obligation.scopeResourceIds || [];
|
|
1639
|
+
const validScopeIds = requestedScopeIds.filter((id) => state.resources.some(({ record }) => (
|
|
1640
|
+
record.id === id && allowedScopeTypes.has(record.type)
|
|
1641
|
+
)));
|
|
1642
|
+
const fallbackScopeIds = inScopeSystems.length
|
|
1643
|
+
? inScopeSystems
|
|
1644
|
+
: (item.controlIds || obligation.controlIds || []).length
|
|
1645
|
+
? (item.controlIds || obligation.controlIds)
|
|
1646
|
+
: [state.workspace.id];
|
|
1573
1647
|
return {
|
|
1574
1648
|
...common,
|
|
1575
1649
|
status: "complete",
|
|
1576
1650
|
profileId: item.completionProfile || item.activityType,
|
|
1577
1651
|
obligationId: item.obligationId,
|
|
1578
1652
|
controlIds: item.controlIds || obligation.controlIds || [],
|
|
1579
|
-
scopeResourceIds:
|
|
1580
|
-
? (item.subjectResourceIds || item.scopeResourceIds || obligation.scopeResourceIds)
|
|
1581
|
-
: [state.workspace.id],
|
|
1653
|
+
scopeResourceIds: validScopeIds.length ? validScopeIds : fallbackScopeIds,
|
|
1582
1654
|
performerIds: responsiblePeople,
|
|
1583
1655
|
completedAt: timestamp,
|
|
1584
1656
|
method: "",
|
|
@@ -3089,7 +3161,7 @@ async function saveOnboarding(draft = false) {
|
|
|
3089
3161
|
try {
|
|
3090
3162
|
const response = await localFetch("/api/setup", {
|
|
3091
3163
|
method: "POST",
|
|
3092
|
-
headers: { "content-type": "application/json" },
|
|
3164
|
+
headers: { "content-type": "application/json", prefer: "respond-async" },
|
|
3093
3165
|
body: JSON.stringify({
|
|
3094
3166
|
serviceName: onboardingDraft.serviceName,
|
|
3095
3167
|
boundary: onboardingDraft.scope,
|
|
@@ -4538,6 +4610,12 @@ function applyFastMutationPatch(result) {
|
|
|
4538
4610
|
state.resources.push({ record, content: {}, history: [], detailsLoaded: false });
|
|
4539
4611
|
}
|
|
4540
4612
|
}
|
|
4613
|
+
for (const record of [result.workspace, result.program, result.system, result.renderer, result.commitment].filter(Boolean)) {
|
|
4614
|
+
const entry = state.resources.find(({ record: current }) => current.id === record.id);
|
|
4615
|
+
if (entry) entry.record = record;
|
|
4616
|
+
else state.resources.push({ record, content: {}, history: [], detailsLoaded: false });
|
|
4617
|
+
if (record.type === "workspace") state.workspace = record;
|
|
4618
|
+
}
|
|
4541
4619
|
if (result.synchronization) {
|
|
4542
4620
|
state.repository = {
|
|
4543
4621
|
...state.repository,
|
|
@@ -4548,7 +4626,7 @@ function applyFastMutationPatch(result) {
|
|
|
4548
4626
|
backgroundSynchronization: result.synchronization.status === "syncing" ? result.synchronization : null
|
|
4549
4627
|
};
|
|
4550
4628
|
}
|
|
4551
|
-
state.readOnly = true;
|
|
4629
|
+
if (result.synchronization?.status === "syncing") state.readOnly = true;
|
|
4552
4630
|
}
|
|
4553
4631
|
|
|
4554
4632
|
function scheduleMutationStateRefresh(delay = 0) {
|
|
@@ -4652,7 +4730,7 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
4652
4730
|
if (status) status.textContent = "";
|
|
4653
4731
|
if (busy) {
|
|
4654
4732
|
dialog._stillWorkingTimer = setTimeout(() => {
|
|
4655
|
-
if (status) status.textContent = "Still working.
|
|
4733
|
+
if (status) status.textContent = "Still working. Recalculating readiness and repository state.";
|
|
4656
4734
|
}, 1_500);
|
|
4657
4735
|
}
|
|
4658
4736
|
}
|
|
@@ -4860,7 +4938,7 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
4860
4938
|
.onboarding-git-status{display:flex;align-items:flex-start;gap:9px;margin:14px 25px 0;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.onboarding-git-status .status-dot{margin-top:4px}.onboarding-git-status strong,.onboarding-git-status small{display:block}.onboarding-git-status strong{font-size:12px}.onboarding-git-status small{color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:3px}.onboarding-git-status code{font-size:10.8px}
|
|
4861
4939
|
.badge.status-overdue,.badge.status-blocked{background:#f7dfdc;color:#873027}.badge.status-due{background:#f6e8c9;color:#79500f}.badge.status-upcoming,.badge.status-proposed{background:var(--accent-soft);color:var(--accent)}.badge.status-complete{background:#dcefe4;color:#125733}
|
|
4862
4940
|
.obligation-preview,.event-reminder-preview{display:grid;gap:8px}.obligation-preview a{display:flex;align-items:flex-start;gap:9px;text-decoration:none;padding:7px 0;border-top:1px solid var(--line)}.obligation-preview a:first-child{border-top:0;padding-top:0}.obligation-preview strong,.obligation-preview small,.event-reminder-preview strong,.event-reminder-preview small{display:block}.obligation-preview strong,.event-reminder-preview strong{font-size:12px}.obligation-preview small,.event-reminder-preview small{font-size:10.8px;color:var(--muted);margin-top:2px}.event-reminder-preview{grid-template-columns:repeat(2,minmax(0,1fr))}.event-reminder-preview a{padding:10px;border-radius:7px;background:var(--surface-soft);text-decoration:none}
|
|
4863
|
-
.obligation-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;align-items:start}.obligation-column{min-width:0}.obligation-column-head{display:flex;align-items:center;justify-content:space-between;margin:5px 1px 10px}.obligation-column-head>strong{font:500 26.4px Georgia,serif}.obligation-cards{display:grid;gap:9px}.obligation-card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--accent-light);border-radius:9px;padding:14px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.obligation-card.status-overdue,.obligation-card.status-blocked{border-left-color:var(--red)}.obligation-card.status-due{border-left-color:#d89021}.obligation-card-head{display:flex;justify-content:space-between;gap:8px;text-transform:uppercase;letter-spacing:.06em;font-size:9.6px;color:var(--muted)}.obligation-card-head strong{color:var(--ink);text-align:right}.obligation-card h3{font-size:14.4px;margin:9px 0 7px}.obligation-card h3 a{text-decoration:none}.obligation-card p{font-size:10.8px;line-height:1.5;color:var(--muted);margin:0}.obligation-links{margin-top:10px}.workflow-section{margin-top:30px}.section-head{display:flex;justify-content:space-between;margin-bottom:13px}.section-head h2{font:500 28.8px Georgia,serif;margin:6px 0}.section-head p:not(.kicker){font-size:13.2px;color:var(--muted);margin:0;max-width:720px}.policy-event-feedback{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:10px;align-items:center;margin-top:14px;padding:12px 14px;border:1px solid #9ccfb2;border-radius:8px;background:#e7f5ec}.policy-event-feedback .status-dot{align-self:start;margin-top:4px}.policy-event-feedback strong,.policy-event-feedback p{display:block}.policy-event-feedback strong{font-size:12px}.policy-event-feedback p{margin:3px 0 0;color:#315d44;font-size:10.8px}.policy-event-feedback .icon-button{width:30px;height:30px}.policy-event-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.policy-event-more{margin-top:10px}.policy-event-row{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;padding:9px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.policy-event-row[hidden]{display:none}.policy-event-name{min-width:0}.policy-event-title{display:flex;align-items:center;gap:6px;min-width:0}.policy-event-title>strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.policy-event-guide{display:inline-flex;flex:0 0 auto}.policy-event-guide .guide-trigger{width:20px;height:20px}.policy-event-guide .guide-trigger svg{width:14px;height:14px}.policy-event-name strong,.policy-event-name>small{display:block}.policy-event-name strong{font-size:12px}.policy-event-name>small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.policy-event-row>.button{flex:none;padding:7px 9px;font-size:10.8px}.policy-event-tooltip{position:absolute;z-index:8;top:calc(100% + 7px);left:0;width:min(420px,calc(100vw - 48px));padding:12px 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow);opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s,transform .12s,visibility 0s .12s;pointer-events:none}.policy-event-row:nth-child(3n) .policy-event-tooltip{right:0;left:auto}.policy-event-row:hover,.policy-event-row:focus-within{z-index:9}.policy-event-guide:hover .policy-event-tooltip,.policy-event-guide:focus-within .policy-event-tooltip{opacity:1;visibility:visible;transform:none;transition-delay:0s}.policy-event-tooltip>strong{font-size:12px}.policy-event-tooltip ol{display:grid;gap:7px;margin:9px 0 0;padding-left:20px}.policy-event-tooltip li span,.policy-event-tooltip li small{display:block}.policy-event-tooltip li span{font-size:10.8px}.policy-event-tooltip li small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.4}
|
|
4941
|
+
.obligation-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;align-items:start}.obligation-column{min-width:0}.obligation-column-head{display:flex;align-items:center;justify-content:space-between;margin:5px 1px 10px}.obligation-column-head>strong{font:500 26.4px Georgia,serif}.obligation-cards{display:grid;gap:9px}.obligation-card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--accent-light);border-radius:9px;padding:14px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.obligation-card.status-overdue,.obligation-card.status-blocked{border-left-color:var(--red)}.obligation-card.status-due{border-left-color:#d89021}.obligation-card-head{display:flex;justify-content:space-between;gap:8px;text-transform:uppercase;letter-spacing:.06em;font-size:9.6px;color:var(--muted)}.obligation-card-head strong{color:var(--ink);text-align:right}.obligation-card h3{font-size:14.4px;margin:9px 0 7px}.obligation-card h3 a{text-decoration:none}.obligation-card p{font-size:10.8px;line-height:1.5;color:var(--muted);margin:0}.obligation-links{margin-top:10px}.workflow-section{margin-top:30px}.operation-gate{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:22px;border:1px solid var(--accent-light);border-radius:12px;background:var(--accent-wash)}.operation-gate h2{margin:5px 0 7px;font:500 25px Georgia,serif}.operation-gate p:not(.kicker){margin:0;max-width:680px;color:var(--muted)}.operation-setup-preview>summary{margin-top:18px;padding:14px 16px;border:1px solid var(--line);border-radius:9px;background:var(--panel);font-weight:700;cursor:pointer}.operation-setup-preview[open]>summary{margin-bottom:0}.section-head{display:flex;justify-content:space-between;margin-bottom:13px}.section-head h2{font:500 28.8px Georgia,serif;margin:6px 0}.section-head p:not(.kicker){font-size:13.2px;color:var(--muted);margin:0;max-width:720px}.policy-event-feedback{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:10px;align-items:center;margin-top:14px;padding:12px 14px;border:1px solid #9ccfb2;border-radius:8px;background:#e7f5ec}.policy-event-feedback .status-dot{align-self:start;margin-top:4px}.policy-event-feedback strong,.policy-event-feedback p{display:block}.policy-event-feedback strong{font-size:12px}.policy-event-feedback p{margin:3px 0 0;color:#315d44;font-size:10.8px}.policy-event-feedback .icon-button{width:30px;height:30px}.policy-event-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.policy-event-more{margin-top:10px}.policy-event-row{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;padding:9px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.policy-event-row[hidden]{display:none}.policy-event-name{min-width:0}.policy-event-title{display:flex;align-items:center;gap:6px;min-width:0}.policy-event-title>strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.policy-event-guide{display:inline-flex;flex:0 0 auto}.policy-event-guide .guide-trigger{width:20px;height:20px}.policy-event-guide .guide-trigger svg{width:14px;height:14px}.policy-event-name strong,.policy-event-name>small{display:block}.policy-event-name strong{font-size:12px}.policy-event-name>small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.policy-event-row>.button{flex:none;padding:7px 9px;font-size:10.8px}.policy-event-tooltip{position:absolute;z-index:8;top:calc(100% + 7px);left:0;width:min(420px,calc(100vw - 48px));padding:12px 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow);opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s,transform .12s,visibility 0s .12s;pointer-events:none}.policy-event-row:nth-child(3n) .policy-event-tooltip{right:0;left:auto}.policy-event-row:hover,.policy-event-row:focus-within{z-index:9}.policy-event-guide:hover .policy-event-tooltip,.policy-event-guide:focus-within .policy-event-tooltip{opacity:1;visibility:visible;transform:none;transition-delay:0s}.policy-event-tooltip>strong{font-size:12px}.policy-event-tooltip ol{display:grid;gap:7px;margin:9px 0 0;padding-left:20px}.policy-event-tooltip li span,.policy-event-tooltip li small{display:block}.policy-event-tooltip li span{font-size:10.8px}.policy-event-tooltip li small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.4}
|
|
4864
4942
|
.obligation-card-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:9px;margin-top:10px}.obligation-card-foot .obligation-links{margin-top:0;min-width:0}.obligation-action{flex:0 0 auto;border:0;border-radius:6px;background:var(--accent-soft);color:var(--accent);padding:7px 9px;font-family:inherit;font-size:10.8px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer}.obligation-action:hover{filter:brightness(1.08)}.obligation-action.blocked{background:var(--surface-muted);color:var(--muted)}.obligation-more{width:100%;margin-top:9px}.workflow-section{scroll-margin-top:92px}
|
|
4865
4943
|
.event-dialog label{display:block;margin-top:13px}.event-dialog label[hidden]{display:none}.event-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select,.event-dialog textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.event-dialog textarea{resize:vertical}.commit-dialog .form-grid label.full{grid-column:1/-1}.workflow-preview{margin-top:14px;padding:0 12px;border-radius:7px;background:var(--surface-soft)}.workflow-preview:not(:empty){padding-top:10px;padding-bottom:10px}.workflow-preview strong,.workflow-preview p{display:block;margin:0}.workflow-preview p{margin-top:5px;color:var(--muted);font-size:12px;line-height:1.5}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:12px}.event-dialog-steps small{font-size:9.6px;color:var(--muted);margin-top:2px}
|
|
4866
4944
|
.applicability-dialog{width:min(980px,calc(100vw - 30px));max-height:calc(100vh - 32px);border:0;border-radius:12px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 25px 80px rgba(0,0,24,.28)}.applicability-dialog form{padding:23px}.applicability-dialog form>p{color:var(--muted);font-size:13.2px}.applicability-dialog form>.applicability-baseline-note{padding:10px 12px;border-radius:7px;background:var(--accent-soft);color:var(--ink)}.review-context label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.review-context input,.review-context select,.applicability-row input,.applicability-row select{width:100%;min-height:38px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:8px 9px;font-size:13.2px}.review-context label.full{grid-column:1/-1}.applicability-rows{display:grid;gap:7px;max-height:46vh;overflow:auto;margin-top:16px;padding-right:4px}.applicability-row{display:grid;grid-template-columns:minmax(210px,1fr) 180px minmax(240px,1.3fr);gap:9px;align-items:center;padding:9px;border:1px solid var(--line);border-radius:8px}.applicability-row strong,.applicability-row small{display:block}.applicability-row small{margin-top:3px;color:var(--muted);font-size:10.8px}.applicability-row .applicability-constraint{color:var(--accent);line-height:1.35}
|