filegrc 0.16.2 → 0.16.3
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 +1 -1
- package/package.json +1 -1
- package/src/obligations.js +6 -2
- package/src/web.js +35 -45
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ filegrc is a zero-dependency Node.js engine for Git-native GRC workspaces. It va
|
|
|
4
4
|
|
|
5
5
|
Program Readiness checks management-owned scope, policy adoption, control implementation, and authoritative evidence mapping without requiring an audit record. Audit Readiness starts after CPA engagement and checks the firm-agreed date or period, engagement-specific management documents, operating evidence, and Type 2 population completeness.
|
|
6
6
|
|
|
7
|
-
Controls
|
|
7
|
+
The Controls list focuses on implementation and its next action. The Obligations list shows schedule state separately, including proposals, approved schedules, and Policies awaiting activation or their effective date. Enabled work starts when its governing content is effective and a linked Control is implemented.
|
|
8
8
|
|
|
9
9
|
Most users should create a complete workspace:
|
|
10
10
|
|
package/package.json
CHANGED
package/src/obligations.js
CHANGED
|
@@ -101,6 +101,9 @@ export function planObligations(resources, options = {}) {
|
|
|
101
101
|
&& ["active", "proposed"].includes(record.status)
|
|
102
102
|
&& obligationBelongsToProgram(record, obligationProgram, model)
|
|
103
103
|
));
|
|
104
|
+
const programStatusByObligationId = new Map(obligations.map((obligation) => [
|
|
105
|
+
obligation.id, obligationProgramStatus(obligation, byId, asOf, model)
|
|
106
|
+
]));
|
|
104
107
|
const obligationIds = new Set(obligations.map(({ id }) => id));
|
|
105
108
|
if (obligations.length > MAX_PLANNED_ITEMS) {
|
|
106
109
|
throw new Error(`The obligation query must be narrowed; it includes more than ${MAX_PLANNED_ITEMS.toLocaleString("en-US")} active obligations.`);
|
|
@@ -123,7 +126,7 @@ export function planObligations(resources, options = {}) {
|
|
|
123
126
|
const schedule = rule || obligation;
|
|
124
127
|
const activity = obligationActivity(model, obligation);
|
|
125
128
|
const expectedCompletionTypes = activity.completionResourceTypes;
|
|
126
|
-
const programStatus =
|
|
129
|
+
const programStatus = programStatusByObligationId.get(obligation.id);
|
|
127
130
|
const programBlocker = programStatus === "proposed" ? obligationProgramBlocker(obligation, byId, asOf) : null;
|
|
128
131
|
if (schedule.recurrence?.mode === "event" && schedule.recurrence.eventType) {
|
|
129
132
|
const eventType = schedule.recurrence.eventType;
|
|
@@ -294,7 +297,7 @@ export function planObligations(resources, options = {}) {
|
|
|
294
297
|
if (!dueWindowStart || !dueWindowEnd || dueWindowStart > through) continue;
|
|
295
298
|
const overdueOn = addCalendarDays(dueWindowEnd, 1);
|
|
296
299
|
const window = { dueWindowStart, dueWindowEnd, overdueOn };
|
|
297
|
-
const programStatus =
|
|
300
|
+
const programStatus = programStatusByObligationId.get(obligation.id);
|
|
298
301
|
const programBlocker = programStatus === "proposed" ? obligationProgramBlocker(obligation, byId, asOf) : null;
|
|
299
302
|
const activity = obligationActivity(model, obligation);
|
|
300
303
|
const completedMemberIds = (occurrence.members || [])
|
|
@@ -402,6 +405,7 @@ export function planObligations(resources, options = {}) {
|
|
|
402
405
|
asOf,
|
|
403
406
|
through,
|
|
404
407
|
from: requestedFrom,
|
|
408
|
+
programStatuses: Object.fromEntries(programStatusByObligationId),
|
|
405
409
|
counts,
|
|
406
410
|
items,
|
|
407
411
|
calendarItems,
|
package/src/web.js
CHANGED
|
@@ -246,6 +246,7 @@ function normalizeAppState(next) {
|
|
|
246
246
|
function blockingStateSections(route) {
|
|
247
247
|
if (route.name === "home") return ["program", "obligations", "workflow"];
|
|
248
248
|
if (route.name === "repository") return ["repository"];
|
|
249
|
+
if (route.name === "list" && route.type === "obligation") return ["obligations"];
|
|
249
250
|
if (route.name === "obligations" || route.name === "stage" && route.stageId === "run") return ["program", "obligations"];
|
|
250
251
|
if (route.name === "audit-packet") return ["repository", "program", "obligations", "audits"];
|
|
251
252
|
if (route.name === "evidence-sources") return ["program", "workflow"];
|
|
@@ -3088,7 +3089,6 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
3088
3089
|
const fields = [...new Set([
|
|
3089
3090
|
"title",
|
|
3090
3091
|
...(definition.listFields || []),
|
|
3091
|
-
...(type === "control" ? ["$operationTracking"] : []),
|
|
3092
3092
|
...(type === "obligation" ? ["$workQueueStatus"] : [])
|
|
3093
3093
|
])].filter((name) => name !== "title");
|
|
3094
3094
|
const modelFields = { ...state.model.commonFields, ...definition.fields };
|
|
@@ -3131,7 +3131,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
3131
3131
|
const start = (pageNumber - 1) * LIST_PAGE_SIZE;
|
|
3132
3132
|
const visible = filtered.slice(start, start + LIST_PAGE_SIZE);
|
|
3133
3133
|
main.querySelector("#result-count").textContent = filtered.length + (filtered.length === 1 ? " record" : " records");
|
|
3134
|
-
main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + detailContext + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$
|
|
3134
|
+
main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + detailContext + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? displayStatus(entry.record) : entry.record[name], name, type, true)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." : collectionEmptyState(type, definition)) + '</td></tr>';
|
|
3135
3135
|
pagination.hidden = totalPages === 1;
|
|
3136
3136
|
previous.disabled = pageNumber === 1;
|
|
3137
3137
|
next.disabled = pageNumber === totalPages;
|
|
@@ -5972,7 +5972,6 @@ function setNavigationGroupOpen(group, open) {
|
|
|
5972
5972
|
function groupTitle(id) { return state.model.groups.find((group) => group.id === id)?.title || "Program"; }
|
|
5973
5973
|
function fieldDefinition(type, name) { return state.model.resources[type]?.fields?.[name] || state.model.commonFields[name]; }
|
|
5974
5974
|
function fieldLabel(type, name) {
|
|
5975
|
-
if (name === "$operationTracking") return "Operation tracking";
|
|
5976
5975
|
if (name === "$workQueueStatus") return "Work Queue";
|
|
5977
5976
|
if (type === "obligation" && name === "status") return "Configuration";
|
|
5978
5977
|
if (name === "title") return state.model.resources[type]?.titleLabel || state.model.commonFields.title.label;
|
|
@@ -6083,58 +6082,49 @@ function formatObjectArray(items, objectType, compact = false) {
|
|
|
6083
6082
|
return '<div class="object-value"><span class="object-value-facts">' + facts.join("") + '</span>' + (!compact && notes.length ? '<small>' + esc(notes.join(" ")) + '</small>' : "") + '</div>';
|
|
6084
6083
|
}).join("") + '</div>';
|
|
6085
6084
|
}
|
|
6086
|
-
function
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
return
|
|
6092
|
-
}
|
|
6093
|
-
const counts = obligations.reduce((result, obligation) => {
|
|
6094
|
-
const status = workQueueScheduleStatus(obligation, control);
|
|
6095
|
-
result[status] = (result[status] || 0) + 1;
|
|
6096
|
-
return result;
|
|
6097
|
-
}, {});
|
|
6098
|
-
const scheduleCount = obligations.length + " " + pluralize("schedule", obligations.length);
|
|
6099
|
-
if (counts.running === obligations.length) {
|
|
6100
|
-
return '<a class="operation-tracking running" href="#/stage/run"><strong>Running in Work Queue</strong><small>' + esc(scheduleCount) + '</small></a>';
|
|
6101
|
-
}
|
|
6102
|
-
if (counts["waiting-policy"] === obligations.length) {
|
|
6103
|
-
return '<a class="operation-tracking waiting" href="#/stage/run"><strong>Waiting for policy approval</strong><small>' + esc(scheduleCount) + ' enabled</small></a>';
|
|
6104
|
-
}
|
|
6105
|
-
if (counts["waiting-control"] === obligations.length) {
|
|
6106
|
-
return '<a class="operation-tracking waiting" href="#/stage/run"><strong>Ready when implemented</strong><small>' + esc(scheduleCount) + ' enabled</small></a>';
|
|
6107
|
-
}
|
|
6108
|
-
if (counts.paused === obligations.length) {
|
|
6109
|
-
return '<a class="operation-tracking paused" href="#/stage/run"><strong>Work Queue paused</strong><small>' + esc(scheduleCount) + '</small></a>';
|
|
6110
|
-
}
|
|
6111
|
-
const summary = [
|
|
6112
|
-
counts.running ? counts.running + " running" : "",
|
|
6113
|
-
counts["waiting-policy"] ? counts["waiting-policy"] + " waiting for policy" : "",
|
|
6114
|
-
counts["waiting-control"] ? counts["waiting-control"] + " waiting for implementation" : "",
|
|
6115
|
-
counts.paused ? counts.paused + " paused" : ""
|
|
6116
|
-
].filter(Boolean).join(" · ");
|
|
6117
|
-
return '<a class="operation-tracking mixed" href="#/stage/run"><strong>Work Queue needs attention</strong><small>' + esc(summary) + '</small></a>';
|
|
6118
|
-
}
|
|
6119
|
-
function workQueueScheduleStatus(obligation, control = null) {
|
|
6085
|
+
function workQueueScheduleStatus(obligation) {
|
|
6086
|
+
if (obligation.status === "proposed") {
|
|
6087
|
+
const pendingRules = resourcesOfType("obligation-rule")
|
|
6088
|
+
.map(({ record }) => record)
|
|
6089
|
+
.filter((record) => record.obligationId === obligation.id && ["proposed", "approved"].includes(record.status));
|
|
6090
|
+
return pendingRules.length === 1 && pendingRules[0].status === "approved" ? "approved" : "proposed";
|
|
6091
|
+
}
|
|
6120
6092
|
if (obligation.status === "paused") return "paused";
|
|
6121
6093
|
if (obligation.status !== "active") return obligation.status;
|
|
6094
|
+
const programStatus = state.obligations?.programStatuses?.[obligation.id];
|
|
6095
|
+
if (!programStatus) return "not-evaluated";
|
|
6096
|
+
const rule = state.resources.find(({ record }) => record.id === obligation.activeRuleId)?.record;
|
|
6097
|
+
if (obligation.scheduleMode === "rule" && rule?.status !== "active") {
|
|
6098
|
+
return "proposed";
|
|
6099
|
+
}
|
|
6100
|
+
if (rule?.effectiveAt && new Date(rule.effectiveAt) > new Date()) {
|
|
6101
|
+
const prior = state.resources.find(({ record }) => record.id === rule.supersedesId)?.record;
|
|
6102
|
+
if (prior?.obligationId !== obligation.id || !["active", "retired"].includes(prior.status)) {
|
|
6103
|
+
return "waiting-schedule-effective";
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6122
6106
|
const asOf = state.obligations?.asOf || new Date().toISOString().slice(0, 10);
|
|
6123
6107
|
const policies = (obligation.policyIds || []).map((id) => state.resources.find(({ record }) => record.id === id)?.record);
|
|
6124
|
-
if (
|
|
6125
|
-
|
|
6126
|
-
|
|
6108
|
+
if (policies.some((policy) => policy?.type !== "policy" || !["approved", "active"].includes(policy.status))) return "waiting-policy-approval";
|
|
6109
|
+
if (policies.some((policy) => policy.status !== "active")) return "waiting-policy-activation";
|
|
6110
|
+
if (policies.some((policy) => !policy.effectiveOn || policy.effectiveOn > asOf)) return "waiting-policy-effective";
|
|
6127
6111
|
const linkedControls = (obligation.controlIds || []).map((id) => state.resources.find(({ record }) => record.id === id)?.record);
|
|
6128
|
-
const implemented = control
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
return implemented ? "running" : "waiting-control";
|
|
6112
|
+
const implemented = !linkedControls.length || linkedControls.some((record) => record?.type === "control" && record.status === "implemented");
|
|
6113
|
+
if (!implemented) return "waiting-control";
|
|
6114
|
+
return programStatus === "accepted" ? "running" : "waiting-prerequisites";
|
|
6132
6115
|
}
|
|
6133
6116
|
function obligationWorkQueueStatus(obligation) {
|
|
6134
6117
|
const status = workQueueScheduleStatus(obligation);
|
|
6118
|
+
if (status === "proposed") return '<span class="operation-tracking"><strong>Proposed</strong><small>Review schedule before activation</small></span>';
|
|
6119
|
+
if (status === "approved") return '<span class="operation-tracking"><strong>Approved</strong><small>Activate schedule before operation</small></span>';
|
|
6135
6120
|
if (status === "running") return '<span class="operation-tracking running"><strong>Running</strong><small>Enabled and policy effective</small></span>';
|
|
6136
|
-
if (status === "waiting-policy") return '<span class="operation-tracking waiting"><strong>Waiting for policy</strong><small>Schedule enabled</small></span>';
|
|
6121
|
+
if (status === "waiting-policy-approval") return '<span class="operation-tracking waiting"><strong>Waiting for policy approval</strong><small>Schedule enabled</small></span>';
|
|
6122
|
+
if (status === "waiting-policy-activation") return '<span class="operation-tracking waiting"><strong>Waiting for policy activation</strong><small>Policy approved</small></span>';
|
|
6123
|
+
if (status === "waiting-policy-effective") return '<span class="operation-tracking waiting"><strong>Waiting for effective date</strong><small>Policy active</small></span>';
|
|
6124
|
+
if (status === "waiting-schedule-effective") return '<span class="operation-tracking waiting"><strong>Waiting for schedule start</strong><small>Approved rule activates at its effective time</small></span>';
|
|
6137
6125
|
if (status === "waiting-control") return '<span class="operation-tracking waiting"><strong>Waiting for control</strong><small>Implement a linked control</small></span>';
|
|
6126
|
+
if (status === "waiting-prerequisites") return '<span class="operation-tracking waiting"><strong>Waiting for prerequisites</strong><small>Check owner and governed content</small></span>';
|
|
6127
|
+
if (status === "not-evaluated") return '<span class="operation-tracking"><strong>Not assessed here</strong><small>Outside the selected program</small></span>';
|
|
6138
6128
|
if (status === "paused") return '<span class="operation-tracking paused"><strong>Paused</strong><small>Schedule disabled</small></span>';
|
|
6139
6129
|
return '<span class="operation-tracking"><strong>' + esc(properCase(status)) + '</strong></span>';
|
|
6140
6130
|
}
|