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
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ Historical JSON classification uses the complete owning Evidence state at each G

Rule-based operating completion saves its dated result and occurrence links in one validated batch. Fixed scopes without a selector need no temporal Collection Review. Dynamic populations retain their cutoff and committed population requirements. Automatic fixed-scope reconciliation uses the actual reviewer named on a completion that covers every expected member. Separate member completions and dynamic populations keep the normal population reconciliation workflow. Partial work stays open. Initial windows use the later rule or governing-content cutover, and finalized proof is evaluated at reconciliation rather than against later evidence expiration. Browser and CLI completions bind the scaffolded rule and occurrence revisions and preserve identical retries.

Open selector occurrences retain members proven eligible in committed snapshots of the same Program, rule, window and cutoff. A later service retirement or Vendor termination preserves that in-window work. Shared population calculations verify the original eligibility through Git instead of trusting arbitrary recorded IDs. New eligible inventory enters the derived queue immediately; source edits remain writable while the owner refreshes the open occurrence, but previously recorded eligible members cannot be deleted. Cutoff reconciliation combines the exact temporal Collection Review population with proven prior members, bounded by that review's scope revision. Finalized proof keeps its existing immutable binding.



Program Overview renders its scope, navigation, and program path from bootstrap while verified readiness and scheduled work load. Pending and failed checks remain explicit. Overview summaries use the shared Program Readiness stages; detailed workflow and Audit preparation load on the pages that need them. Audit status is not inferred from an unloaded assessment.
Expand Down
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.20.4",
"version": "0.20.5",
"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.20.4",
"version": "0.20.5",
"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.20.4",
version: "0.20.5",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name,
version: "0.20.4",
version: "0.20.5",
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.20.4",
"version": "0.20.5",
"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.20.4",
"version": "0.20.5",
"description": "Zero-dependency Git-native GRC engine",
"license": "MIT",
"repository": {
Expand Down
68 changes: 68 additions & 0 deletions packages/filegrc/src/obligation-population.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { currentCalendarDate } from "./time.js";
import { coverageStart } from "./coverage.js";
import { calculationMemo } from "./calculation-cache.js";
import { selectScopedCollectionRecords } from "./collection-scope.js";
import { getGitSummary, getRecordIdentityHistory, isDataHistoryAncestor } from "./git.js";
import { reviewHistoricalWorkspace, reviewHistoryCommits, reviewHistoryContext } from "./historical-workspace.js";

const IDENTITY = ["programId", "obligationId", "ruleId", "occurrenceKey", "membershipCutoffAt"];
const sameIds = (left, right) => JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
const sameWindow = (left, right) => left?.type === "obligation-occurrence"
&& IDENTITY.every((key) => left[key] === right[key])
&& JSON.stringify(left.coverage) === JSON.stringify(right.coverage);
const selectorKey = (selector) => JSON.stringify({ ...selector,
...(selector?.statuses ? { statuses: [...selector.statuses].sort() } : {}),
...(selector?.criticalities ? { criticalities: [...selector.criticalities].sort() } : {}) });

// Open windows accumulate actual reviewed membership. A later retirement must
// not erase that work, but merely writing an unrelated ID into a JSON file
// does not establish eligibility. Recover only valid same-window snapshots.
export function committedOccurrencePopulation(loaded, occurrence, selector, throughCommit = null) {
if (!loaded.root || !occurrence || !selector) return { memberIds: [], established: false };
return calculationMemo(loaded.resources, "committed-occurrence-population", [occurrence.id,
JSON.stringify(IDENTITY.map((key) => occurrence[key])), JSON.stringify(occurrence.coverage), selectorKey(selector), throughCommit], () => {
const bound = throughCommit || getGitSummary(loaded.root).commit;
const context = reviewHistoryContext(loaded.root, loaded);
const commits = reviewHistoryCommits(context);
const identities = new Set();
let basis = occurrence;
while (sameWindow(basis, occurrence) && !identities.has(basis.id)) {
identities.add(basis.id);
basis = loaded.resources.find(({ id }) => id === basis.supersedesId);
}
const changed = new Map([...identities].flatMap((id) => (
context.index?.available ? context.index.historiesById.get(id) || [] : getRecordIdentityHistory(loaded.root, id)
)).map((summary) => [summary.commit, summary]));
const members = new Set();
let established = false;
for (const commit of commits) {
if (!changed.has(commit) || !isDataHistoryAncestor(loaded, commit, bound)) continue;
const snapshot = reviewHistoricalWorkspace(context, commit);
if (!snapshot) continue;
const program = snapshot.resources.find(({ id }) => id === occurrence.programId);
const rule = snapshot.resources.find(({ id }) => id === occurrence.ruleId);
if (selectorKey(rule?.selector) !== selectorKey(selector)) continue;
const recordedAt = new Date(changed.get(commit).timestamp);
if (Number.isNaN(recordedAt.getTime())) continue;
const recordedOn = currentCalendarDate(rule?.timezone || snapshot.workspace.timezone || "UTC", recordedAt);
if (recordedOn < coverageStart(occurrence.coverage) || recordedOn > occurrence.membershipCutoffAt) continue;
const eligible = new Set(selectScopedCollectionRecords(snapshot, selector, program).map(({ id }) => id));
for (const prior of snapshot.resources.filter((item) => identities.has(item.id) && item.status === "open" && sameWindow(item, occurrence))) {
const ids = (prior.members || []).map(({ resourceId }) => resourceId);
if (new Set(ids).size !== ids.length || [...members].some((id) => !ids.includes(id))
|| ids.some((id) => !eligible.has(id) && !members.has(id))
|| !established && !sameIds(ids, eligible)) continue;
established = true;
ids.forEach((id) => members.add(id));
}
}
return { memberIds: [...members].sort(), established };
});
}

export function occurrenceSelectorPopulation(loaded, occurrence, selector, program, { selectedIds = null, throughCommit = null } = {}) {
const currentIds = selectedIds || selectScopedCollectionRecords(loaded, selector, program).map(({ id }) => id);
const committed = committedOccurrencePopulation(loaded, occurrence, selector, throughCommit);
return { ...committed, currentIds: [...currentIds].sort(),
expectedIds: [...new Set([...currentIds, ...committed.memberIds])].sort() };
}
19 changes: 12 additions & 7 deletions packages/filegrc/src/obligations.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { occurrenceSelectorPopulation } from "./obligation-population.js";
import { occurrenceCompletionContext } from "./obligation-completion-context.js";
import { assessProgramAccessReview, providerAccessControls } from "./provider-access-completion.js";
import { readFile } from "node:fs/promises";
Expand Down Expand Up @@ -244,7 +245,7 @@ export function planObligations(resources, options = {}) {
const recordedMemberIds = (reconciliation?.members || []).map(({ resourceId }) => resourceId);
const expectedMemberIds = reconciliation
? reconciliation.status === "open" && !membershipFinal
? [...new Set([...recordedMemberIds, ...selectedMemberIds])].sort()
? occurrenceSelectorPopulation({ resources: records, model, root: options.root }, reconciliation, schedule.selector, program, { selectedIds: selectedMemberIds }).expectedIds
: recordedMemberIds
: selectedMemberIds;
const completedMemberIds = new Set((reconciliation?.members || [])
Expand Down Expand Up @@ -343,7 +344,7 @@ export function planObligations(resources, options = {}) {
const membershipFinal = membershipIsFinal(rule.selector, window, asOf);
const recordedMemberIds = (occurrence.members || []).map(({ resourceId }) => resourceId);
const expectedMemberIds = occurrence.status === "open" && !membershipFinal && rule.selector
? [...new Set([...recordedMemberIds, ...selectScopedCollectionRecords({ resources: records, model }, rule.selector, program).map(({ id }) => id)])].sort()
? occurrenceSelectorPopulation({ resources: records, model, root: options.root }, occurrence, rule.selector, program).expectedIds
: recordedMemberIds;
const completedMemberIds = (occurrence.members || [])
.filter(({ disposition, result }) => disposition === "expected" && result === "passed")
Expand Down Expand Up @@ -866,7 +867,11 @@ export async function scaffoldObligationOccurrence(input, options = {}) {
rule?.selector,
entry.relativePath
);
return snapshot ? [{ entry, snapshot }] : [];
return snapshot ? [{ entry, snapshot: { ...snapshot,
populationIds: occurrenceSelectorPopulation(loaded, current?.record, rule.selector, program, {
selectedIds: snapshot.selectedIds, throughCommit: entry.record.scopeRevision
}).expectedIds
} }] : [];
});
const supersededIds = new Set(candidates.map(({ entry }) => entry.record.supersedesId).filter(Boolean));
candidates.sort((left, right) => (
Expand All @@ -888,16 +893,16 @@ export async function scaffoldObligationOccurrence(input, options = {}) {
const activity = obligationActivity(loaded.model, obligation);
const completionMemberField = activity.aggregate?.completionMemberField;
const populationIds = existing
? item.expectedMemberIds
? historicalSnapshot?.populationIds || item.expectedMemberIds
: predecessor
? predecessor.record.members.map(({ resourceId }) => resourceId)
: historicalReview
? historicalSnapshot.selectedIds
? historicalSnapshot.populationIds
: item.expectedMemberIds;
if (existing && historicalSnapshot) {
const frozenIds = [...existingMembers.keys()].sort();
const reviewedIds = [...historicalSnapshot.selectedIds].sort();
if (JSON.stringify(frozenIds) !== JSON.stringify(reviewedIds)) {
const reviewedIds = [...historicalSnapshot.populationIds].sort();
if (frozenIds.some((id) => !reviewedIds.includes(id))) {
throw new Error(
`Open occurrence "${existing.record.id}" does not match the committed ${cutoff} Collection Review population. `
+ "Correct the open occurrence before reconciliation."
Expand Down
19 changes: 11 additions & 8 deletions packages/filegrc/src/validate.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { occurrenceSelectorPopulation } from "./obligation-population.js";
import { occurrenceCompletionContext } from "./obligation-completion-context.js";
import { attestationBindingIssue } from "./attestation-integrity.js";
import { withAssessmentCache } from "./calculation-cache.js";
Expand Down Expand Up @@ -596,7 +597,11 @@ function validateObligationOccurrence(record, model, resources, entries, root, w
record.collectionReviewCommit
);
const memberIds = [...(record.members || []).map(({ resourceId }) => resourceId)].sort();
const reviewedIds = [...(historicalSnapshot?.selectedIds || [])].sort();
const reviewedIds = historicalSnapshot ? occurrenceSelectorPopulation(
{ root, resources, model }, record, rule.selector, byId.get(record.programId), {
selectedIds: historicalSnapshot.selectedIds, throughCommit: review.scopeRevision
}
).expectedIds : [];
const reviewCoversCutoff = review?.coverage?.kind === "as-of"
? review.coverage.on === record.membershipCutoffAt
: review?.coverage?.kind === "range"
Expand Down Expand Up @@ -626,16 +631,14 @@ function validateObligationOccurrence(record, model, resources, entries, root, w
"A historical occurrence must bind an immutable Collection Review for its exact population cutoff."
));
} else if (rule?.selector) {
const selectedIds = selectScopedCollectionRecords(
{ resources, model },
rule.selector,
byId.get(record.programId)
).map(({ id }) => id).sort();
if (JSON.stringify([...memberIds].sort()) !== JSON.stringify(selectedIds)) {
const population = occurrenceSelectorPopulation({ root, resources, model }, record, rule.selector, byId.get(record.programId));
const missingRecorded = population.memberIds.some((id) => !memberIds.includes(id));
const unsupported = memberIds.some((id) => !population.expectedIds.includes(id));
if (missingRecorded || unsupported || !population.established && JSON.stringify([...memberIds].sort()) !== JSON.stringify(population.currentIds)) {
diagnostics.push(error(
"invalid-obligation-occurrence-population",
path,
"An occurrence without a historical Collection Review must contain the exact current selector population."
"An open occurrence must preserve its committed eligible members and contain no unsupported population members. Reconcile newly eligible members before finalizing the window."
));
}
}
Expand Down
Loading
Loading