Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "filegrc-monorepo",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"workspaces": [
"packages/filegrc",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-filegrc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-filegrc",
"version": "0.21.1",
"version": "0.21.2",
"description": "Create a filegrc workspace for a SOC 2 program",
"license": "MIT",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions packages/create-filegrc/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,13 +524,13 @@ async function runCombinedSetup(target, input) {
async function writeMinimalLockfile(target, name, versionRange) {
const lock = {
name,
version: "0.21.1",
version: "0.21.2",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name,
version: "0.21.1",
version: "0.21.2",
dependencies: { filegrc: versionRange }
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/create-filegrc/template/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "{{project_name}}",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"description": "filegrc workspace for a SOC 2 program",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/filegrc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "filegrc",
"version": "0.21.1",
"version": "0.21.2",
"description": "Zero-dependency Git-native GRC engine",
"license": "MIT",
"repository": {
Expand Down
6 changes: 5 additions & 1 deletion packages/filegrc/src/schedule-set.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ export function scheduleSetIssues(loaded, program, review, options = {}) {
const rule = options.at ? obligationRule(obligation, byId, { now: options.at }) : byId.get(obligation.activeRuleId);
if (obligation.status !== "active" || (obligation.scheduleMode === "rule"
&& (!rule || rule.obligationId !== obligation.id || !["active", "retired"].includes(rule.status) || decision.ruleId !== obligation.activeRuleId || !reviewedRuleChainContains(obligation, decision.ruleId, rule.id, byId) || !rule.approvedByIds?.length || !rule.approvedOn))) {
add(obligation.id, `${obligation.title} needs its adopted, approved rule effective at cutover.`);
add(obligation.id, decision.ruleId !== obligation.activeRuleId && obligation.scheduleMode === "rule"
? `${obligation.title}: the schedule review must name the currently adopted rule. Independently review the current Program schedule set.`
: options.at
? `${obligation.title} needs its adopted, approved rule effective at cutover.`
: `${obligation.title} needs its currently adopted, approved rule. Review the current Program schedule set.`);
} else if (rule && (rule.approvedOn > currentCalendarDate(rule.timezone || loaded.workspace?.timezone || "UTC")
|| !Array.isArray(rule.approvedByIds) || rule.approvedByIds.some((id) => !personWasActiveOn(byId.get(id), rule.approvedOn)))) {
add(obligation.id, `${obligation.title} needs an actual rule approval by People active on its approval date.`);
Expand Down
10 changes: 7 additions & 3 deletions packages/filegrc/src/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -2544,7 +2544,7 @@ function obligationCard(item, collapsed = false) {
: "");
const kind = item.kind === "event" ? "Policy Event Task" : item.kind === "action" ? "Assigned Follow-up" : properCase(item.activityType || "Recurring");
const population = item.ruleId
? '<p><strong>' + esc(String(item.completedCount || 0)) + ' of ' + esc(String(item.expectedCount || 0)) + '</strong> expected members passed' + (item.awaitingPeriodEnd ? '; work recorded, awaiting full period end ' + esc(item.requiredCoverage.endsOn) : item.membershipFinal ? '' : (item.awaitingPopulationCutoff ? '; work recorded, awaiting population cutoff ' : '; population remains open through ') + esc(item.membershipCutoffAt)) + '.</p>'
? '<p><strong>' + esc(String(item.completedCount || 0)) + ' of ' + esc(String(item.expectedCount || 0)) + '</strong> expected members passed' + (item.awaitingPeriodEnd ? item.requiredCoverage?.endsOn ? '; work recorded, awaiting full period end ' + esc(item.requiredCoverage.endsOn) : '; review the required period end before reconciliation' : item.membershipFinal ? '' : item.membershipCutoffAt ? (item.awaitingPopulationCutoff ? '; work recorded, awaiting population cutoff ' : '; population remains open through ') + esc(item.membershipCutoffAt) : '; record the actual population observation date with the work') + '.</p>'
: '';
return '<article class="obligation-card status-' + esc(item.status) + '" data-work-source="' + esc(type + ":" + id) + '"' + (collapsed ? ' data-collapsed hidden' : "") + '><div class="obligation-card-head"><span>' + esc(kind) + '</span><strong>' + esc(timingText(item)) + '</strong></div><h3><a href="#/resource/' + type + '/' + encodeURIComponent(id) + '">' + esc(titleCase(item.title)) + '</a></h3><p>' + esc(windowText(item)) + '</p>' + population + (item.timingIssue ? '<p>' + esc(item.timingIssue) + '</p>' : '') + '<div class="obligation-card-foot"><div class="obligation-links">' + (item.policyIds || []).map(formatReference).join("") + '</div>' + action + '</div></article>';
}
Expand Down Expand Up @@ -5433,10 +5433,14 @@ function openEditor(type, entry = null, options = {}) {
function occurrenceMemberReview(record, membershipFinal = true, periodFinal = true) {
const members = record.members || [];
const pending = !periodFinal
? '<p class="occurrence-pending">The full period remains open through ' + esc(record.requiredCoverage?.endsOn) + '. Reconcile after its inclusive final day, even for zero or non-applicable populations.</p>'
? record.requiredCoverage?.endsOn
? '<p class="occurrence-pending">The full period remains open through ' + esc(record.requiredCoverage.endsOn) + '. Reconcile after its inclusive final day, even for zero or non-applicable populations.</p>'
: '<p class="occurrence-pending">Review the required period end before reconciling this occurrence.</p>'
: membershipFinal
? ""
: '<p class="occurrence-pending">Population remains open through ' + esc(record.membershipCutoffAt) + '. Review now, then reconcile after the cutoff.</p>';
: record.membershipCutoffAt
? '<p class="occurrence-pending">Population remains open through ' + esc(record.membershipCutoffAt) + '. Review now, then reconcile after the cutoff.</p>'
: '<p class="occurrence-pending">Record the date you actually observed this population. Reconcile after reviewing its members and proof.</p>';
if (!members.length) {
return '<section class="occurrence-review"><div class="occurrence-review-head"><strong>Population</strong><span>0 members</span></div>' + pending + '<p class="occurrence-empty">Confirm the zero population, then set the record to reconciled.</p></section>';
}
Expand Down
29 changes: 29 additions & 0 deletions packages/filegrc/test/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3039,3 +3039,32 @@ test("waiting card timing never claims completion or an actionable deadline", ()
assert.equal(timing.timingText(item), "Awaiting population cutoff");
assert.equal(timing.windowText(item), "Recorded work window: 2026-10-01 through 2026-10-31.");
});


test("snapshot population copy requests an actual observation date without inventing a cutoff", () => {
const card = APP_SCRIPT.slice(APP_SCRIPT.indexOf("function obligationCard"), APP_SCRIPT.indexOf("function actionCompletionPlan"));
const review = APP_SCRIPT.slice(APP_SCRIPT.indexOf("function occurrenceMemberReview"), APP_SCRIPT.indexOf("function applyOccurrenceMemberReview"));
const context = vm.createContext({ state: { readOnly: true }, esc: String, titleCase: String, properCase: String, formatReference: String, timingText: () => "Upcoming", windowText: () => "Allowed window", obligationCompletionPlan: () => null });
vm.runInContext(card + review, context);
context.item = { obligationId: "review", ruleId: "rule-review", title: "Periodic review", status: "upcoming", kind: "calendar", expectedCount: 1, completedCount: 0, membershipFinal: false, membershipCutoffAt: null };
let html = vm.runInContext("obligationCard(item)", context);
assert.match(html, /record the actual population observation date/);
assert.doesNotMatch(html, /through \.|null|undefined/);
context.record = { members: [], membershipCutoffAt: null };
html = vm.runInContext("occurrenceMemberReview(record, false)", context);
assert.match(html, /date you actually observed this population/);
assert.doesNotMatch(html, /through \.|null|undefined/);
context.item.membershipCutoffAt = "2026-10-31";
context.record.membershipCutoffAt = "2026-10-31";
assert.match(vm.runInContext("obligationCard(item)", context), /population remains open through 2026-10-31/);
assert.match(vm.runInContext("occurrenceMemberReview(record, false)", context), /Population remains open through 2026-10-31/);
context.item.awaitingPeriodEnd = true;
context.item.requiredCoverage = { endsOn: "2026-12-31" };
context.record.requiredCoverage = { endsOn: "2026-12-31" };
assert.match(vm.runInContext("obligationCard(item)", context), /awaiting full period end 2026-12-31/);
assert.match(vm.runInContext("occurrenceMemberReview(record, true, false)", context), /inclusive final day/);
delete context.item.requiredCoverage;
delete context.record.requiredCoverage;
assert.match(vm.runInContext("obligationCard(item)", context), /review the required period end/);
assert.match(vm.runInContext("occurrenceMemberReview(record, true, false)", context), /Review the required period end/);
});
18 changes: 18 additions & 0 deletions packages/filegrc/test/schedule-set.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,21 @@ test("replacement event coverage uses the rule effective at cutover", () => {
assert.ok(scheduleSetIssues(loaded, program, review, { at: "2026-10-13T00:00:00Z" }).some(({ message }) => /required incident-declared event/.test(message)));
assert.deepEqual(scheduleSetIssues(loaded, program, review, { at: "2099-12-01T00:00:00Z" }), []);
});


test("a successor awaiting schedule oversight does not imply missing historical cutover approval", () => {
const { loaded, program } = fixture();
const obligation = loaded.resources.find(({ id }) => id === "calendar");
const predecessor = loaded.resources.find(({ id }) => id === "rule-calendar");
predecessor.status = "retired";
loaded.resources.push({ ...predecessor, id: "rule-current", status: "active", supersedesId: predecessor.id, effectiveAt: "2026-10-25T00:00:00Z" });
obligation.activeRuleId = "rule-current";
const review = { scopeResourceIds: [program.id], scheduleDecisions: [adopted, exclusion] };
const issues = scheduleSetIssues(loaded, program, review, { at: "2026-10-12T00:00:00Z" });
assert.ok(issues.some(({ message }) => /schedule review must name the currently adopted rule/.test(message)));
assert.ok(issues.every(({ message }) => !/effective at cutover/.test(message)));
review.scheduleDecisions[0] = { ...adopted, ruleId: "rule-current" };
assert.deepEqual(scheduleSetIssues(loaded, program, review, { at: "2026-10-12T00:00:00Z" }), []);
predecessor.approvedByIds = [];
assert.ok(scheduleSetIssues(loaded, program, review, { at: "2026-10-12T00:00:00Z" }).some(({ message }) => /effective at cutover/.test(message)));
});
Loading