filegrc 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/model/index.js +6 -5
- package/model/v6.json +10358 -0
- package/package.json +1 -1
- package/src/cli.js +75 -6
- package/src/document-activation.js +61 -25
- package/src/files.js +54 -25
- package/src/index.js +8 -1
- package/src/model-migration.js +148 -3
- package/src/obligations.js +5 -5
- package/src/policy-library/information-security-policy-v2.md +2 -2
- package/src/policy-library.js +42 -10
- package/src/program-lifecycle.js +41 -3
- package/src/program-path.js +19 -18
- package/src/program-readiness.js +131 -16
- package/src/server.js +8 -1
- package/src/setup.js +1 -1
- package/src/validate.js +33 -5
- package/src/web.js +128 -26
- package/src/workflow.js +21 -4
package/src/web.js
CHANGED
|
@@ -176,7 +176,8 @@ function buildNavigation(route) {
|
|
|
176
176
|
const current = route.type === type && (!contextualStageId || contextualStageId === stage.id);
|
|
177
177
|
return '<a class="' + (direct ? "nav-direct " : "") + (current ? "current" : "") + '" href="#/resources/' + encodeURIComponent(type) + '"><span>' + esc(titleCase(definition.pluralTitle)) + '</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
|
|
178
178
|
}).join("") + (section.relatedLinks || []).map((link) => {
|
|
179
|
-
const current = route.type === link.type && contextualStageId === stage.id
|
|
179
|
+
const current = route.type === link.type && contextualStageId === stage.id
|
|
180
|
+
|| link.href === "#/stage/" + route.stageId;
|
|
180
181
|
return '<a class="' + (direct ? "nav-direct " : "") + (current ? "current" : "") + '" href="' + esc(link.href) + '"><span>' + esc(link.label) + '</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
|
|
181
182
|
}).join("") + renderSidebarUtility(section.utility, route, direct);
|
|
182
183
|
if (direct) return links;
|
|
@@ -358,7 +359,7 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
358
359
|
const progress = stageProgress(stage);
|
|
359
360
|
main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
360
361
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
|
|
361
|
-
(stage.id === "policies" ? renderPolicyApprovalGuidance()
|
|
362
|
+
(stage.id === "policies" ? renderPolicyApprovalGuidance() + renderPoliciesTable() : renderStagePageIndex(stage)) + (stage.id === "controls" ? renderDocumentActivationAssessments() + renderPolicyActivationAssessments() + renderEvidenceReadiness() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
|
|
362
363
|
main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
|
|
363
364
|
main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
|
|
364
365
|
event.currentTarget.remove();
|
|
@@ -379,11 +380,92 @@ function renderPolicyApprovalGuidance() {
|
|
|
379
380
|
: "";
|
|
380
381
|
return '<article><strong>' + esc(proposal.title) + '</strong><p>' + esc(proposal.message) + '</p>' + review + '<div class="evidence-map-references">' + proposal.policyIds.map((id) => formatReference(id)).join("") + '</div></article>';
|
|
381
382
|
}).join("");
|
|
382
|
-
return '<section class="policy-lifecycle-note panel ' + (proposalRows ? "" : "single") + '"><div><p class="kicker">Step 2 approval</p><h3>Approve the
|
|
383
|
+
return '<section class="policy-lifecycle-note panel ' + (proposalRows ? "" : "single") + '"><div><p class="kicker">Step 2 approval</p><h3>Approve the governed content</h3><p>Approval means your company reviewed and accepted the requirements and intended values in each Policy, program Document, and Training record. It does not mean the linked Controls are implemented yet.</p><p>Bind each approval to the exact Markdown revision here. Implement Controls and configure Obligations in Step 3, then activate each unchanged approved revision at implementation cutover.</p></div>' + (proposalRows ? '<div class="policy-library-proposals">' + proposalRows + '</div>' : "") + '</section>';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function renderPoliciesTable() {
|
|
387
|
+
const entries = state.resources.filter(({ record }) => (
|
|
388
|
+
record.type === "policy"
|
|
389
|
+
|| record.type === "training"
|
|
390
|
+
|| record.type === "document" && !auditSpecificDocument(record)
|
|
391
|
+
)).sort((left, right) => (
|
|
392
|
+
["policy", "document", "training"].indexOf(left.record.type) - ["policy", "document", "training"].indexOf(right.record.type)
|
|
393
|
+
|| left.record.title.localeCompare(right.record.title)
|
|
394
|
+
));
|
|
395
|
+
const ownerNames = (record) => (record.ownerIds || []).map((id) => (
|
|
396
|
+
state.resources.find(({ record: candidate }) => candidate.id === id)?.record.title || id
|
|
397
|
+
));
|
|
398
|
+
const approverNames = (record) => (record.approverIds || record.approvedByIds || []).map((id) => (
|
|
399
|
+
state.resources.find(({ record: candidate }) => candidate.id === id)?.record.title || id
|
|
400
|
+
));
|
|
401
|
+
const typeLabel = (record) => record.type === "document" && record.documentKind
|
|
402
|
+
? '<span>' + esc(state.model.resources.document.title) + '<small>' + esc(properCase(record.documentKind)) + '</small></span>'
|
|
403
|
+
: esc(state.model.resources[record.type].title);
|
|
404
|
+
const approval = (record) => {
|
|
405
|
+
const approvers = approverNames(record);
|
|
406
|
+
const bound = Boolean(record.approvedContentRevisions || record.effectiveContentRevisions);
|
|
407
|
+
if (!record.approvedOn || !approvers.length) return '<span class="muted">Not approved</span>';
|
|
408
|
+
return '<span>' + esc(record.approvedOn) + '<small>' + esc(approvers.join(", ")) + (bound ? ' · Revision bound' : '') + '</small></span>';
|
|
409
|
+
};
|
|
410
|
+
const row = (entry) => {
|
|
411
|
+
const record = entry.record;
|
|
412
|
+
const detail = '#/resource/' + encodeURIComponent(record.type) + '/' + encodeURIComponent(record.id) + '?stage=policies' + (record.type === "document" ? '&documentScope=program' : '');
|
|
413
|
+
const owners = ownerNames(record);
|
|
414
|
+
return '<tr data-policy-content-row data-history="' + (["superseded", "retired"].includes(record.status) ? 'true' : 'false') + '"><td data-label="Name" data-primary-field><a class="record-title" href="' + detail + '">' + esc(record.title) + '</a><small><code>' + esc(record.id) + '</code></small></td><td data-label="Type" class="policy-content-type">' + typeLabel(record) + '</td><td data-label="Owner">' + (owners.length ? esc(owners.join(", ")) : '<span class="muted">Missing</span>') + '</td><td data-label="Status">' + formatValue(displayStatus(record), "status", record.type, true) + '</td><td data-label="Approval" class="policy-content-approval">' + approval(record) + '</td><td data-label="Next action">' + policyContentWorkflowCell(entry) + '</td></tr>';
|
|
415
|
+
};
|
|
416
|
+
const add = !state.readOnly
|
|
417
|
+
? '<details class="policy-content-add"><summary class="button primary">Add</summary><div><button type="button" data-add-policy-content="policy">Policy</button><button type="button" data-add-policy-content="document">Document</button><button type="button" data-add-policy-content="training">Training</button></div></details>'
|
|
418
|
+
: '';
|
|
419
|
+
const emptyRow = '<tr><td colspan="6">' + empty('No governed content has been created.') + '</td></tr>';
|
|
420
|
+
const html = '<section class="policy-content-table"><div class="section-head"><div><p class="kicker">Governed content</p><h2>Policies</h2><p>Review every program artifact in one place. Each row keeps its own type-specific fields and Git file.</p></div><div class="policy-content-tools"><label><span class="sr-only">Filter policies</span><input type="search" data-policy-content-search placeholder="Filter policies"></label><label class="history-toggle"><input type="checkbox" data-policy-content-history> Show retired</label>' + add + '</div></div><div class="record-table-wrap"><table class="record-table"><thead><tr><th>Name</th><th>Type</th><th>Owner</th><th>Status</th><th>Approval</th><th>Next action</th></tr></thead><tbody data-policy-content-body>' + (entries.length ? entries.map(row).join('') : emptyRow) + '</tbody></table></div><p class="policy-content-count" data-policy-content-count></p></section>';
|
|
421
|
+
queueMicrotask(() => wirePoliciesTable(entries.length));
|
|
422
|
+
return html;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function wirePoliciesTable(total) {
|
|
426
|
+
const table = root.querySelector('.policy-content-table');
|
|
427
|
+
if (!table) return;
|
|
428
|
+
const search = table.querySelector('[data-policy-content-search]');
|
|
429
|
+
const history = table.querySelector('[data-policy-content-history]');
|
|
430
|
+
const count = table.querySelector('[data-policy-content-count]');
|
|
431
|
+
const filter = () => {
|
|
432
|
+
const query = search.value.trim().toLowerCase();
|
|
433
|
+
let visible = 0;
|
|
434
|
+
table.querySelectorAll('[data-policy-content-row]').forEach((row) => {
|
|
435
|
+
const show = (history.checked || row.dataset.history !== 'true')
|
|
436
|
+
&& (!query || row.textContent.toLowerCase().includes(query));
|
|
437
|
+
row.hidden = !show;
|
|
438
|
+
if (show) visible += 1;
|
|
439
|
+
});
|
|
440
|
+
count.textContent = visible + ' of ' + total + ' ' + pluralize('artifact', total);
|
|
441
|
+
};
|
|
442
|
+
search.addEventListener('input', filter);
|
|
443
|
+
history.addEventListener('change', filter);
|
|
444
|
+
table.querySelectorAll('[data-add-policy-content]').forEach((button) => {
|
|
445
|
+
button.addEventListener('click', () => openEditor(button.dataset.addPolicyContent));
|
|
446
|
+
});
|
|
447
|
+
filter();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function policyContentWorkflowCell(entry) {
|
|
451
|
+
const record = entry.record;
|
|
452
|
+
const items = recordWorkflowItems(record.type, record.id);
|
|
453
|
+
if (!items.length) return '<span class="record-workflow-clear">No calculated action</span>';
|
|
454
|
+
const item = items[0];
|
|
455
|
+
const href = workflowItemHref(item) || '#/resource/' + encodeURIComponent(record.type) + '/' + encodeURIComponent(record.id) + '?stage=policies';
|
|
456
|
+
const title = ["draft", "in-review"].includes(record.status)
|
|
457
|
+
? "Complete and approve"
|
|
458
|
+
: record.status === "approved"
|
|
459
|
+
? "Implement and activate"
|
|
460
|
+
: "Resolve readiness gaps";
|
|
461
|
+
return '<a class="record-workflow-action policy-content-next-action" href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(title) + '</strong><small>' + items.length + ' ' + pluralize('check', items.length) + '</small></span></a>';
|
|
383
462
|
}
|
|
384
463
|
|
|
385
464
|
function renderDocumentActivationAssessments() {
|
|
386
|
-
const assessments =
|
|
465
|
+
const assessments = [
|
|
466
|
+
...(state.programReadiness?.documentActivations || []).map((item) => ({ ...item, resourceType: "document", resourceId: item.documentId })),
|
|
467
|
+
...(state.programReadiness?.trainingActivations || []).map((item) => ({ ...item, resourceType: "training", resourceId: item.trainingId }))
|
|
468
|
+
];
|
|
387
469
|
if (!assessments.length) return "";
|
|
388
470
|
const candidates = assessments.filter(({ state }) => state === "ready-to-activate");
|
|
389
471
|
const cards = assessments.map((assessment) => {
|
|
@@ -391,12 +473,16 @@ function renderDocumentActivationAssessments() {
|
|
|
391
473
|
const controls = assessment.missingImplementationControlIds.length
|
|
392
474
|
? '<div class="policy-activation-gaps"><div><small>Controls still being implemented</small><div class="evidence-map-references">' + assessment.missingImplementationControlIds.map((id) => formatReference(id, "control")).join("") + '</div></div></div>'
|
|
393
475
|
: "";
|
|
394
|
-
|
|
476
|
+
const schedule = assessment.resourceType === "training" && !assessment.assignmentScheduled
|
|
477
|
+
? '<p class="policy-activation-warning">Enable at least one assignment Obligation before activating this Training.</p>'
|
|
478
|
+
: '';
|
|
479
|
+
const detail = '#/resource/' + assessment.resourceType + '/' + encodeURIComponent(assessment.resourceId) + '?stage=controls' + (assessment.resourceType === "document" ? '&documentScope=program' : '');
|
|
480
|
+
return '<article class="policy-activation-card ' + esc(assessment.state) + '"><div class="evidence-map-card-head"><div><span class="badge ' + (operating ? "good" : "warn") + '">' + esc(assessment.label) + '</span><h3><a href="' + detail + '">' + esc(assessment.title) + '</a></h3></div><small>' + assessment.gapCount + ' ' + pluralize("gap", assessment.gapCount) + '</small></div>' + controls + schedule + '<div class="policy-activation-actions"><a class="button" href="' + detail + '">View ' + esc(properCase(assessment.resourceType)) + '</a></div></article>';
|
|
395
481
|
}).join("");
|
|
396
482
|
const action = candidates.length && !state.readOnly
|
|
397
|
-
? '<div class="evidence-map-actions"><button class="button primary" type="button" data-review-document-activation>Review
|
|
483
|
+
? '<div class="evidence-map-actions"><button class="button primary" type="button" data-review-document-activation>Review content activation</button></div>'
|
|
398
484
|
: "";
|
|
399
|
-
return '<section class="policy-activation"><div class="evidence-map-head"><div><p class="kicker">
|
|
485
|
+
return '<section class="policy-activation"><div class="evidence-map-head"><div><p class="kicker">Program content cutover</p><h2>Activate approved program content</h2><p>After linked requirements are implemented and Training assignment Obligations are ready, activate each unchanged approved Document and Training record. FileGRC records a separate activation date and binds the exact activated revision.</p></div>' + action + '</div><div class="policy-activation-grid">' + cards + '</div></section>';
|
|
400
486
|
}
|
|
401
487
|
|
|
402
488
|
function renderAuditDocumentActivationAssessments() {
|
|
@@ -422,11 +508,16 @@ function renderAuditDocumentActivationAssessments() {
|
|
|
422
508
|
function openDocumentActivationDialog(auditId = null) {
|
|
423
509
|
const candidates = (auditId
|
|
424
510
|
? state.auditPreparations?.[auditId]?.documentActivations || []
|
|
425
|
-
:
|
|
511
|
+
: [
|
|
512
|
+
...(state.programReadiness?.documentActivations || []).map((item) => ({ ...item, resourceType: "document", resourceId: item.documentId })),
|
|
513
|
+
...(state.programReadiness?.trainingActivations || []).map((item) => ({ ...item, resourceType: "training", resourceId: item.trainingId }))
|
|
514
|
+
]).filter(({ state }) => state === "ready-to-activate");
|
|
426
515
|
const entryById = new Map(state.resources
|
|
427
|
-
.filter(({ record }) => record.type === "document" && record.status === "approved")
|
|
516
|
+
.filter(({ record }) => (auditId ? record.type === "document" : ["document", "training"].includes(record.type)) && record.status === "approved")
|
|
428
517
|
.map((entry) => [entry.record.id, entry]));
|
|
429
|
-
const ready = candidates.
|
|
518
|
+
const ready = candidates.map((item) => auditId
|
|
519
|
+
? { ...item, resourceType: "document", resourceId: item.documentId }
|
|
520
|
+
: item).filter(({ resourceId }) => entryById.has(resourceId));
|
|
430
521
|
if (!ready.length) return;
|
|
431
522
|
const activators = state.resources
|
|
432
523
|
.filter(({ record }) => record.type === "person" && record.status === "active")
|
|
@@ -436,13 +527,13 @@ function openDocumentActivationDialog(auditId = null) {
|
|
|
436
527
|
dialog.className = "commit-dialog event-dialog policy-activation-dialog";
|
|
437
528
|
dialog.setAttribute("aria-labelledby", "document-activation-dialog-title");
|
|
438
529
|
const step = auditId ? "Step 5" : "Step 3";
|
|
439
|
-
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + step + ' activation</p><h2 id="document-activation-dialog-title">Review governed
|
|
440
|
-
'<p>' + (auditId ? 'These
|
|
441
|
-
'<div class="policy-activation-selections">' + ready.map((assessment) => '<label class="policy-activation-selection"><input type="checkbox" name="
|
|
530
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + step + ' activation</p><h2 id="document-activation-dialog-title">Review governed content activation</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div>' +
|
|
531
|
+
'<p>' + (auditId ? 'These Audit Documents are complete and independently approved for the selected Audit.' : 'These program Documents and Training records already have independent Step 2 approvals, implemented linked requirements, and any required schedules.') + ' Activation records the Person who puts each unchanged approved revision into use.</p>' +
|
|
532
|
+
'<div class="policy-activation-selections">' + ready.map((assessment) => '<label class="policy-activation-selection"><input type="checkbox" name="resourceId" value="' + esc(assessment.resourceId) + '" checked><span><strong>' + esc(assessment.title) + '</strong><small>' + esc(properCase(assessment.resourceType)) + ' · Approved ' + esc(assessment.approvedOn) + (auditId ? ' · Engagement facts complete' : ' · Implementation ready') + '</small></span></label>').join("") + '</div>' +
|
|
442
533
|
'<label><span>Activated by</span><select name="activatedById" required><option value="">Select the Person who performs this activation</option>' + activators.map((person) => '<option value="' + esc(person.id) + '">' + esc(person.title) + '</option>').join("") + '</select></label>' +
|
|
443
534
|
'<label><span>Activation date</span><input name="activatedOn" type="date" value="' + esc(today) + '" readonly required></label>' +
|
|
444
535
|
'<label><span>Effective date</span><input name="effectiveOn" type="date" min="' + esc(today) + '" value="' + esc(today) + '" required></label>' +
|
|
445
|
-
'<div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status" role="status" aria-live="polite"></span><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary">Activate selected
|
|
536
|
+
'<div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status" role="status" aria-live="polite"></span><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary">Activate selected content</button></div></form>';
|
|
446
537
|
document.body.append(dialog);
|
|
447
538
|
const close = () => dialog.close();
|
|
448
539
|
dialog.querySelector(".icon-button").addEventListener("click", close);
|
|
@@ -451,23 +542,23 @@ function openDocumentActivationDialog(auditId = null) {
|
|
|
451
542
|
dialog.querySelector("form").addEventListener("submit", async (event) => {
|
|
452
543
|
event.preventDefault();
|
|
453
544
|
if (!event.currentTarget.reportValidity() || dialog.dataset.mutationBusy === "true") return;
|
|
454
|
-
const
|
|
455
|
-
if (!
|
|
456
|
-
dialog.querySelector(".dialog-error").textContent = "Select at least one approved
|
|
545
|
+
const resourceIds = [...event.currentTarget.querySelectorAll('[name="resourceId"]:checked')].map(({ value }) => value);
|
|
546
|
+
if (!resourceIds.length) {
|
|
547
|
+
dialog.querySelector(".dialog-error").textContent = "Select at least one approved artifact to activate.";
|
|
457
548
|
return;
|
|
458
549
|
}
|
|
459
|
-
setMutationBusy(dialog, true, "Activating…", "Activate selected
|
|
550
|
+
setMutationBusy(dialog, true, "Activating…", "Activate selected content");
|
|
460
551
|
try {
|
|
461
|
-
const response = await localFetch("/api/document-activations", {
|
|
552
|
+
const response = await localFetch(auditId ? "/api/document-activations" : "/api/governed-content-activations", {
|
|
462
553
|
method: "POST",
|
|
463
554
|
headers: { "content-type": "application/json" },
|
|
464
555
|
body: JSON.stringify({
|
|
465
|
-
documentIds,
|
|
556
|
+
...(auditId ? { documentIds: resourceIds } : { resourceIds }),
|
|
466
557
|
...(auditId ? { auditId } : {}),
|
|
467
558
|
activatedByIds: [event.currentTarget.elements.activatedById.value],
|
|
468
559
|
activatedOn: event.currentTarget.elements.activatedOn.value,
|
|
469
560
|
effectiveOn: event.currentTarget.elements.effectiveOn.value,
|
|
470
|
-
expectedRevisions: Object.fromEntries(
|
|
561
|
+
expectedRevisions: Object.fromEntries(resourceIds.map((resourceId) => [resourceId, entryById.get(resourceId).revision]))
|
|
471
562
|
})
|
|
472
563
|
});
|
|
473
564
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
@@ -475,7 +566,7 @@ function openDocumentActivationDialog(auditId = null) {
|
|
|
475
566
|
dialog.close();
|
|
476
567
|
render();
|
|
477
568
|
} catch (error) {
|
|
478
|
-
setMutationBusy(dialog, false, "", "Activate selected
|
|
569
|
+
setMutationBusy(dialog, false, "", "Activate selected content");
|
|
479
570
|
dialog.querySelector(".dialog-error").textContent = error.message;
|
|
480
571
|
}
|
|
481
572
|
});
|
|
@@ -499,7 +590,7 @@ function renderPolicyActivationAssessments() {
|
|
|
499
590
|
gapGroup("Planned or partial Controls", assessment.plannedOrPartialControlIds, "control") +
|
|
500
591
|
gapGroup("Missing active Components", assessment.missingComponentControlIds, "control") +
|
|
501
592
|
gapGroup("Missing ready evidence sources", assessment.missingEvidenceSourceControlIds, "control") +
|
|
502
|
-
gapGroup("Missing enabled
|
|
593
|
+
gapGroup("Missing enabled Obligations", assessment.missingScheduleControlIds, "control") +
|
|
503
594
|
gapGroup("Inactive governed Documents", assessment.missingGovernedDocumentIds || [], "document") +
|
|
504
595
|
gapGroup("Unresolved Exceptions", assessment.unresolvedExceptionIds, "exception") +
|
|
505
596
|
gapGroup("Documented Exceptions", assessment.documentedExceptionIds, "exception") + '</div>' +
|
|
@@ -1031,6 +1122,13 @@ function stagePageItems(stage, destination) {
|
|
|
1031
1122
|
&& item.code === "governance.appointment.independent-policy-reviewer"
|
|
1032
1123
|
)
|
|
1033
1124
|
));
|
|
1125
|
+
if (stage.id === "policies" && destination.href === "#/stage/policies") {
|
|
1126
|
+
return items.sort((left, right) => (
|
|
1127
|
+
workflowItemStatePriority(left) - workflowItemStatePriority(right)
|
|
1128
|
+
|| (left.priority ?? workflowItemPriority(left)) - (right.priority ?? workflowItemPriority(right))
|
|
1129
|
+
|| left.key.localeCompare(right.key)
|
|
1130
|
+
));
|
|
1131
|
+
}
|
|
1034
1132
|
return items.filter((item) => {
|
|
1035
1133
|
if (
|
|
1036
1134
|
destination.type === "requirement"
|
|
@@ -2147,8 +2245,8 @@ function auditSpecificDocument(record) {
|
|
|
2147
2245
|
}
|
|
2148
2246
|
|
|
2149
2247
|
function documentListTitle(params = new URLSearchParams()) {
|
|
2150
|
-
if (params.get("documentScope") === "audit") return "Audit
|
|
2151
|
-
if (params.get("documentScope") === "program") return "
|
|
2248
|
+
if (params.get("documentScope") === "audit") return "Audit Documents";
|
|
2249
|
+
if (params.get("documentScope") === "program") return "Program Documents";
|
|
2152
2250
|
return "Documents";
|
|
2153
2251
|
}
|
|
2154
2252
|
|
|
@@ -3535,7 +3633,8 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
|
|
|
3535
3633
|
}
|
|
3536
3634
|
if (field.type === "enum" || field.type === "rating" || field.type === "outcome") {
|
|
3537
3635
|
const values = field.values || (field.type === "rating" ? state.model.primitives.rating : state.model.primitives.outcome) || [];
|
|
3538
|
-
const activationManagedType = ["policy", "document"].includes(type)
|
|
3636
|
+
const activationManagedType = ["policy", "document"].includes(type)
|
|
3637
|
+
|| (type === "training" && Boolean(state.model.resources.training?.fields?.activatedContentRevisions));
|
|
3539
3638
|
const availableValues = activationManagedType && name === "status" && value !== "active"
|
|
3540
3639
|
? values.filter((item) => item !== "active")
|
|
3541
3640
|
: values;
|
|
@@ -3543,6 +3642,8 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
|
|
|
3543
3642
|
const enumHelp = activationManagedType && name === "status"
|
|
3544
3643
|
? type === "document"
|
|
3545
3644
|
? "Approval ends at Approved. Activate program Documents from Step 3 after implementation, and engagement Documents from Step 5 after their Audit facts are complete."
|
|
3645
|
+
: type === "training"
|
|
3646
|
+
? "Step 2 ends at Approved. Activate Training from Step 3 after its Controls and assignment Obligation are ready."
|
|
3546
3647
|
: "Step 2 ends at Approved. Activate approved Policies from the Step 3 Controls-page cutover."
|
|
3547
3648
|
: help;
|
|
3548
3649
|
return fieldWrap(name, "string", label, requiredMark, control, enumHelp, required);
|
|
@@ -4648,6 +4749,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
4648
4749
|
.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}
|
|
4649
4750
|
.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)}
|
|
4650
4751
|
.policy-lifecycle-note.single{grid-template-columns:1fr}
|
|
4752
|
+
.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}
|
|
4651
4753
|
.policy-library-proposals{min-width:0}.policy-library-proposals article,.policy-library-proposals details{min-width:0}.policy-library-proposals pre{box-sizing:border-box;max-width:100%;overflow:auto;padding:9px;border:1px solid var(--line);border-radius:6px;background:var(--paper);font-size:9px;line-height:1.45}
|
|
4652
4754
|
.policy-activation-actions{display:flex;justify-content:flex-end;margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
|
|
4653
4755
|
.policy-activation-review-warning{margin-top:14px;padding:12px 14px;border:1px solid #d8bd78;border-radius:8px;background:#fff8e8}.policy-activation-review-warning>strong{font-size:12px}.policy-activation-review-warning ul{display:grid;gap:5px;margin:9px 0;padding-left:20px;font-size:10.8px}.policy-activation-review-warning p{margin:9px 0 0;color:#6d4917;font-size:11px;line-height:1.5}
|
package/src/workflow.js
CHANGED
|
@@ -540,7 +540,11 @@ function finalizationFields(record, model) {
|
|
|
540
540
|
["reviewDueOn", record.status === "active"],
|
|
541
541
|
["nonExpiringRationale", record.status === "active" && !record.expiresOn]
|
|
542
542
|
],
|
|
543
|
-
training: [
|
|
543
|
+
training: modelSupports(model, "governed-training-activation") ? [
|
|
544
|
+
["approvedContentRevisions", ["approved", "active"].includes(record.status)],
|
|
545
|
+
["activatedContentRevisions", record.status === "active" && record.activationBasis !== "legacy-v5"],
|
|
546
|
+
["effectiveOn", record.status === "active"]
|
|
547
|
+
] : [
|
|
544
548
|
["effectiveContentRevisions", record.status === "active"],
|
|
545
549
|
["effectiveOn", record.status === "active"]
|
|
546
550
|
],
|
|
@@ -1241,6 +1245,7 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
|
|
|
1241
1245
|
const deliveryFindingKeys = findingKeys(findings, "delivery-readiness");
|
|
1242
1246
|
const policyActivations = program.policyActivations || [];
|
|
1243
1247
|
const documentActivations = program.documentActivations || [];
|
|
1248
|
+
const trainingActivations = program.trainingActivations || [];
|
|
1244
1249
|
const policiesOperating = policyActivations.length > 0
|
|
1245
1250
|
&& policyActivations.every(({ state }) => state === "active-and-operating");
|
|
1246
1251
|
return {
|
|
@@ -1278,11 +1283,23 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
|
|
|
1278
1283
|
message: documentActivations.length && documentActivations.every(({ state }) => state === "active-and-operating")
|
|
1279
1284
|
? "Every required governed Document is separately approved, activated, effective, and operating."
|
|
1280
1285
|
: documentActivations.length
|
|
1281
|
-
? "Approve required
|
|
1286
|
+
? "Approve required program Documents in Step 2, implement their linked requirements, then activate their exact revisions in Step 3."
|
|
1282
1287
|
: "No required governed Document activation is configured.",
|
|
1283
1288
|
findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".document-")),
|
|
1284
1289
|
documents: documentActivations
|
|
1285
1290
|
},
|
|
1291
|
+
trainingActivation: {
|
|
1292
|
+
status: trainingActivations.length && trainingActivations.every(({ state }) => state === "active-and-operating")
|
|
1293
|
+
? "complete"
|
|
1294
|
+
: trainingActivations.length ? "needs-work" : "not-started",
|
|
1295
|
+
message: trainingActivations.length && trainingActivations.every(({ state }) => state === "active-and-operating")
|
|
1296
|
+
? "Every required Training record is separately approved, activated, effective, scheduled, and operating."
|
|
1297
|
+
: trainingActivations.length
|
|
1298
|
+
? "Approve Training in Step 2, implement its Controls and assignment Obligation, then activate its exact revision in Step 3."
|
|
1299
|
+
: "No required Training activation is configured.",
|
|
1300
|
+
findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".training-")),
|
|
1301
|
+
training: trainingActivations
|
|
1302
|
+
},
|
|
1286
1303
|
policyLibraryReview: {
|
|
1287
1304
|
status: program.policyLibraryProposals?.length ? "review" : "current",
|
|
1288
1305
|
message: program.policyLibraryProposals?.length
|
|
@@ -1648,7 +1665,7 @@ function shellArgument(value) {
|
|
|
1648
1665
|
|
|
1649
1666
|
function recordAssessment(type) {
|
|
1650
1667
|
if (["audit", "audit-request", "audit-population"].includes(type)) return "audit-readiness";
|
|
1651
|
-
if (["action-item", "obligation
|
|
1668
|
+
if (["action-item", "obligation-event", "control-activity"].includes(type)) return "period-health";
|
|
1652
1669
|
return "program-configuration";
|
|
1653
1670
|
}
|
|
1654
1671
|
|
|
@@ -1657,7 +1674,7 @@ function recordStage(type) {
|
|
|
1657
1674
|
return "scope";
|
|
1658
1675
|
}
|
|
1659
1676
|
if (["policy", "document", "training"].includes(type)) return "policies";
|
|
1660
|
-
if (["control", "complementary-control", "source-coverage"].includes(type)) return "controls";
|
|
1677
|
+
if (["control", "complementary-control", "source-coverage", "obligation"].includes(type)) return "controls";
|
|
1661
1678
|
if (["audit", "audit-request", "audit-population"].includes(type)) return "audit";
|
|
1662
1679
|
return "operate";
|
|
1663
1680
|
}
|