From e5829de1691cbb9c22b95e3176c8d127a727ef1c Mon Sep 17 00:00:00 2001 From: Abe Wheeler Date: Fri, 23 Oct 2026 08:53:31 -0500 Subject: [PATCH] Preserve closed partial occurrence windows across later activation --- package-lock.json | 8 +++---- package.json | 2 +- packages/create-filegrc/package.json | 2 +- packages/create-filegrc/src/index.js | 4 ++-- packages/create-filegrc/template/package.json | 2 +- packages/filegrc/package.json | 2 +- packages/filegrc/src/validate.js | 11 +++++---- .../test/completion-reconciliation.test.js | 23 +++++++++++++++++++ 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index df6a568..74d2721 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "filegrc-monorepo", - "version": "0.18.14", + "version": "0.18.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "filegrc-monorepo", - "version": "0.18.14", + "version": "0.18.15", "workspaces": [ "packages/filegrc", "packages/create-filegrc" @@ -24,7 +24,7 @@ "link": true }, "packages/create-filegrc": { - "version": "0.18.14", + "version": "0.18.15", "license": "MIT", "bin": { "create-filegrc": "bin/create-filegrc.js" @@ -34,7 +34,7 @@ } }, "packages/filegrc": { - "version": "0.18.14", + "version": "0.18.15", "license": "MIT", "bin": { "filegrc": "bin/filegrc.js" diff --git a/package.json b/package.json index 7ab167b..23781e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "filegrc-monorepo", - "version": "0.18.14", + "version": "0.18.15", "private": true, "workspaces": [ "packages/filegrc", diff --git a/packages/create-filegrc/package.json b/packages/create-filegrc/package.json index 9b91fe2..6d40f17 100644 --- a/packages/create-filegrc/package.json +++ b/packages/create-filegrc/package.json @@ -1,6 +1,6 @@ { "name": "create-filegrc", - "version": "0.18.14", + "version": "0.18.15", "description": "Create a filegrc workspace for a SOC 2 program", "license": "MIT", "repository": { diff --git a/packages/create-filegrc/src/index.js b/packages/create-filegrc/src/index.js index 56b1f79..a26de6d 100644 --- a/packages/create-filegrc/src/index.js +++ b/packages/create-filegrc/src/index.js @@ -524,13 +524,13 @@ async function runCombinedSetup(target, input) { async function writeMinimalLockfile(target, name, versionRange) { const lock = { name, - version: "0.18.14", + version: "0.18.15", lockfileVersion: 3, requires: true, packages: { "": { name, - version: "0.18.14", + version: "0.18.15", dependencies: { filegrc: versionRange } } } diff --git a/packages/create-filegrc/template/package.json b/packages/create-filegrc/template/package.json index 1fd8343..44b5da1 100644 --- a/packages/create-filegrc/template/package.json +++ b/packages/create-filegrc/template/package.json @@ -1,6 +1,6 @@ { "name": "{{project_name}}", - "version": "0.18.14", + "version": "0.18.15", "private": true, "description": "filegrc workspace for a SOC 2 program", "type": "module", diff --git a/packages/filegrc/package.json b/packages/filegrc/package.json index e794f11..ba1c872 100644 --- a/packages/filegrc/package.json +++ b/packages/filegrc/package.json @@ -1,6 +1,6 @@ { "name": "filegrc", - "version": "0.18.14", + "version": "0.18.15", "description": "Zero-dependency Git-native GRC engine", "license": "MIT", "repository": { diff --git a/packages/filegrc/src/validate.js b/packages/filegrc/src/validate.js index 7ef4438..925ffb0 100644 --- a/packages/filegrc/src/validate.js +++ b/packages/filegrc/src/validate.js @@ -782,12 +782,13 @@ function validateOccurrenceScheduleBinding(record, obligation, rule, resources, const byId = new Map(resources.map((candidate) => [candidate.id, candidate])); const cutover = [obligationActivationDate(obligation, byId, rule, rule.timezone || "UTC"), obligationActivationDate(obligation, byId, model, rule.timezone || "UTC")].filter(Boolean).sort().at(-1); - if (modelSupports(model, "operating-preparation") && cutover && expectedStart < cutover && expectedEnd >= cutover + if (modelSupports(model, "operating-preparation") && cutover && expectedStart < cutover && start !== expectedStart) { - // Original full windows retain their published binding. A previously - // committed partial window also survives later governed-content cutovers. - expectedStart = start > expectedStart && start < cutover - && committedOccurrenceWindowMatches(root, record, resources) ? start : cutover; + // A committed partial window keeps its binding even when a later content + // cutover falls after its end. New partial windows still require overlap. + if (start > expectedStart && start < cutover + && committedOccurrenceWindowMatches(root, record, resources)) expectedStart = start; + else if (expectedEnd >= cutover) expectedStart = cutover; } const expectedCutoff = rule.selector?.cutoff === "window-end" ? expectedEnd : expectedStart; const program = resources.find(({ type, id }) => type === "program" && id === record.programId); diff --git a/packages/filegrc/test/completion-reconciliation.test.js b/packages/filegrc/test/completion-reconciliation.test.js index 3507a5c..60fab2a 100644 --- a/packages/filegrc/test/completion-reconciliation.test.js +++ b/packages/filegrc/test/completion-reconciliation.test.js @@ -249,6 +249,29 @@ test("committed partial windows survive later content activation without duplica assert.equal(items[0].dueWindowStart, "2026-07-05"); }); +test("closed committed partial windows survive later content cutovers without accepting edited dates", async (context) => { + const f = await fixture(context); + const result = await completeObligationOccurrence(f.root, { obligationId: f.obligation.id, + expectedRevision: f.revision, record: f.record }); + assert.deepEqual(result.linked.coverage, { kind: "range", startsOn: "2026-07-05", endsOn: "2026-07-21" }); + const loaded = await loadWorkspace(f.root); + const policy = loaded.entries.find(({ record }) => record.id === f.obligation.policyIds[0]); + await writeJson(policy.path, { ...policy.record, effectiveOn: "2026-08-10" }); + let validation = await validateWorkspace(f.root); + assert.ok(validation.diagnostics.some(({ code }) => code === "invalid-obligation-occurrence-schedule-binding"), "Uncommitted dates cannot establish a historical window"); + await writeJson(policy.path, policy.record); + await initializeGitWorkspace(f.root, "Retain the completed initial partial window"); + await writeJson(policy.path, { ...policy.record, effectiveOn: "2026-08-10" }); + validation = await validateWorkspace(f.root); + assert.equal(validation.diagnostics.some(({ code }) => code === "invalid-obligation-occurrence-schedule-binding"), false); + const current = await loadWorkspace(f.root); + const occurrence = current.entries.find(({ record }) => record.id === result.linked.id); + assert.deepEqual(occurrence.record, result.linked, "Activation preserves all frozen occurrence facts"); + await writeJson(occurrence.path, { ...occurrence.record, coverage: { ...occurrence.record.coverage, startsOn: "2026-07-06" } }); + validation = await validateWorkspace(f.root); + assert.ok(validation.diagnostics.some(({ code }) => code === "invalid-obligation-occurrence-schedule-binding"), "An edited historical start remains invalid"); +}); + test("CLI and browser retain the selected Program when multiple Programs are active", async (context) => { const cli = await fixture(context); const browser = await fixture(context);