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.14",
"version": "0.21.15",
"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.14",
"version": "0.21.15",
"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.14",
version: "0.21.15",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name,
version: "0.21.14",
version: "0.21.15",
dependencies: { filegrc: versionRange }
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/create-filegrc/template/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ Do not loosen validation to make a bad record pass. Fix the record or update the

Model v12 requires a complete Program Obligation schedule-set review before activation. Use `npx filegrc review-collection obligation --program PROGRAM_ID --scaffold`, record adopted rules, replacements, retired duplicates, and intentionally inapplicable proposals, then preview and save the exact revision with an independent reviewer. Reporting Channel Sets need independent approval in Step 1; Step 3 verifies their effective coverage and usability at cutover. Existing model v11 workspaces preview `npx filegrc migrate --to-model 12 --preview --json`. See the [model v12 upgrade guide](https://github.com/Alignbase/filegrc/blob/main/docs/upgrading-to-model-v12.md).

Schedule-review scaffolds keep the new reviewer, date and conclusion blank. Prior reviews remain historical context. For Obligations, inspect `scheduleReview` for changed rules, governing sources and before/after configured windows as of its stated date. The CLI prints only rows requiring a fresh decision. After reviewing the unchanged dispositions, explicitly set `confirmUnchangedSchedules` to `true` and retain `expectedPriorReviewRevision` and `expectedCollectionRevision`; FileGRC reconstructs and validates the complete decision set at preview and save. Changed rules and excluded proposals still need explicit decisions. Missing historical proof cannot be carried forward. Add `--include-operating-preview` to the scaffold, preview or save command when detailed first eligible work and proof are needed. Generated commands create a private review payload outside the checkout with `--output`, keeping the reviewed Git revision clean. Existing drafts are never overwritten. Edit that payload, then use the generated `--summary --json` preview and save commands for concise counts, bound revisions, review history and governed-content effects. Omit `--summary` for the complete JSON result. Existing complete decision arrays remain supported.


Model v13 checks completed full-program risk assessment, current workforce training and policy acknowledgements, and actual provider-access reconciliation when selected Controls require them. Retrieval rehearsals prove collection access; they do not complete baseline work. Operating ownership reuses substantive Control procedures and unchanged approved program Documents without a separate Document or duplicate approval. Run `npx filegrc program-readiness --json` for exact gaps, candidates and normal mutation commands shared with the browser. Existing v13 repositories update the engine within the 0.18 release line; no new model migration is needed. Existing v12 repositories preview `npx filegrc migrate --to-model 13 --preview --json` before applying the upgrade. Active programs repair current gaps with actual dates and preserve historical activation and reviews. Material content, schedule and retention changes keep their normal reviews. See the [model v13 upgrade guide](https://github.com/Alignbase/filegrc/blob/main/docs/upgrading-to-model-v13.md).

## Nonmaterial administrative amendments
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.14",
"version": "0.21.15",
"private": true,
"description": "filegrc workspace for a SOC 2 program",
"type": "module",
Expand Down
3 changes: 3 additions & 0 deletions packages/filegrc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ Track this file in Git. Existing repositories that ignore `.filegrc/` should use

Model v12 requires a complete Program Obligation schedule-set review before activation. Use `npx filegrc review-collection obligation --program PROGRAM_ID --scaffold`, record adopted rules, replacements, retired duplicates, and intentionally inapplicable proposals, then preview and save the exact revision with an independent reviewer. Reporting Channel Sets need independent approval in Step 1; Step 3 verifies their effective coverage and usability at cutover. Existing model v11 workspaces preview `npx filegrc migrate --to-model 12 --preview --json`. See the [model v12 upgrade guide](https://github.com/Alignbase/filegrc/blob/main/docs/upgrading-to-model-v12.md).

Schedule-review scaffolds keep the new reviewer, date and conclusion blank. Prior reviews remain historical context. For Obligations, inspect `scheduleReview` for changed rules, governing sources and before/after configured windows as of its stated date. The CLI prints only rows requiring a fresh decision. After reviewing the unchanged dispositions, explicitly set `confirmUnchangedSchedules` to `true` and retain `expectedPriorReviewRevision` and `expectedCollectionRevision`; FileGRC reconstructs and validates the complete decision set at preview and save. Changed rules and excluded proposals still need explicit decisions. Missing historical proof cannot be carried forward. Add `--include-operating-preview` to the scaffold, preview or save command when detailed first eligible work and proof are needed. Generated commands create a private review payload outside the checkout with `--output`, keeping the reviewed Git revision clean. Existing drafts are never overwritten. Edit that payload, then use the generated `--summary --json` preview and save commands for concise counts, bound revisions, review history and governed-content effects. Omit `--summary` for the complete JSON result. Existing complete decision arrays remain supported.


## Hosted runtime compatibility

A hosted application can run one tested stable patch per supported compatibility line. Before 1.0, the line is the minor version, such as `0.17.x`. From 1.0 onward, it is the major version, such as `1.x`. Core accepts stable repository versions in its own line, including both older and newer patches, while retaining workspace model, record validation, and API contract checks. The 0.20 runtime also reads unchanged repositories in the supported 0.18 and 0.19 lines; older runtimes reject repositories that adopt 0.20. Prereleases, malformed versions, and other lines are rejected. Build metadata does not change the line. Diagnostic views remain available for model errors, with failed validation; ordinary mutations stay blocked.
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.14",
"version": "0.21.15",
"description": "Zero-dependency Git-native GRC engine",
"license": "MIT",
"repository": {
Expand Down
5 changes: 4 additions & 1 deletion packages/filegrc/src/agent.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { collectionReviewCommands } from "./collection-review-handoff.js";
import { modelSupports } from "../model/index.js";
import { createResourceId } from "./id.js";
import { markdownEntries } from "./resource-markdown.js";
Expand Down Expand Up @@ -119,7 +120,9 @@ export function buildAgentGuide(loaded, type, options = {}) {
status: collectionReview.status,
recordCount: collectionReview.recordCount,
...(collectionReview.changesSinceReview ? { changesSinceReview: collectionReview.changesSinceReview } : {}),
command: `npx filegrc review-collection ${type} --scaffold`
...(type === "obligation" ? { reviewInstructions: "The scaffold includes a compact scheduleReview packet with changed rules and before/after timing. Fill only decisions requiring review, explicitly confirm unchanged dispositions against the bound prior review, and supply fresh reviewer, date and conclusion. Use --output to create the private payload outside the checkout; generated preview/save commands use --summary for compact results. Add --include-operating-preview for detailed first-work proof." } : {}),
command: collectionReviewCommands(collectionReview)[0],
commands: collectionReviewCommands(collectionReview)
}
: null
},
Expand Down
17 changes: 13 additions & 4 deletions packages/filegrc/src/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { writeReviewPayload, summarizeCollectionReview } from "./collection-review-handoff.js";
import { scaffoldPeriodImpactReview, previewPeriodImpactReview, recordPeriodImpactReview } from "./period-impact.js";
import { scaffoldObligationRuleWithdrawal, previewObligationRuleWithdrawal, withdrawObligationRule } from "./obligation-rule-withdrawal.js";
import { planWorkQueue, sourceDeadlineValidation } from "./work-queue.js";
Expand Down Expand Up @@ -98,6 +99,7 @@ import {
} from "./workflow.js";

const BOOLEAN_FLAGS = new Set([
"include-operating-preview",
"allow-non-authoritative-writes",
"apply",
"check-docs",
Expand Down Expand Up @@ -836,21 +838,28 @@ export async function runCli(argv = process.argv.slice(2)) {
if (command === "review-collection") {
const resourceType = positionals[0] || flags.type;
if (flags.scaffold) {
const result = await scaffoldCollectionReview(root, { resourceType, programId: flags.program });
console.log(JSON.stringify(result, null, 2));
const result = await scaffoldCollectionReview(root, { resourceType, programId: flags.program, includeOperatingPreview: flags["include-operating-preview"] === true });
if (resourceType === "obligation" && result.scheduleReview) {
const unchanged = new Set(result.scheduleReview.rows.filter(({ state }) => state === "unchanged").map(({ obligationId }) => obligationId));
result.scheduleDecisions = result.scheduleDecisions.filter(({ obligationId }) => !unchanged.has(obligationId));
}
const outputPath = flags.output ? await writeReviewPayload(root, flags.output, result) : null;
console.log(JSON.stringify(outputPath ? { outputPath, ...result } : result, null, 2));
return result;
}
const payload = await readSetupPayload(positionals[1]);
const options = {
...payload,
resourceType: resourceType || payload.resourceType,
includeOperatingPreview: flags["include-operating-preview"] === true,
programId: flags.program || payload.programId,
confirmed: flags.yes === true
};
const result = flags.preview
? await planCollectionReview(root, options)
: await withWorkflowDelta(root, () => applyCollectionReview(root, options));
if (flags.json) console.log(JSON.stringify(result, null, 2));
const output = flags.summary ? summarizeCollectionReview(result, flags.preview === true) : result;
if (flags.json || flags.summary) console.log(JSON.stringify(output, null, 2));
else if (flags.preview) { console.log(`Collection review preview: ${result.assessment.configuration.title}.`); printOperatingPreview(result.firstOperatingTasks); }
else console.log(`Confirmed ${result.assessment.configuration.title}.`);
return result;
Expand Down Expand Up @@ -1515,7 +1524,7 @@ Usage:
filegrc external-reviewer-setup <reviewer.json|-> [--preview|--yes] [--json]
filegrc next-audit-cycle <prior-audit-id> [cycle.json|-] --start YYYY-MM-DD --end YYYY-MM-DD [--preview|--yes] [--json]
filegrc review-applicability [--scaffold --type requirement|control|commitment|complementary-control] [decisions.json|-] [--preview|--yes] [--json]
filegrc review-collection <resource-type> [--scaffold | review.json|-] [--preview|--yes] [--json]
filegrc review-collection <resource-type> [--scaffold | review.json|-] [--preview|--yes] [--include-operating-preview] [--output path] [--summary] [--json]
filegrc activate-policies [--scaffold | activation.json|-] [--effective-on YYYY-MM-DD] [--preview|--yes] [--json]
filegrc activate-content [--scaffold | activation.json|-] [--program id] [--activated-by person-id] [--activated-on YYYY-MM-DD] [--effective-on YYYY-MM-DD] [--preview|--yes] [--json]
filegrc activate-documents [--scaffold | activation.json|-] [--program id | --audit id] [--activated-by person-id] [--activated-on YYYY-MM-DD] [--effective-on YYYY-MM-DD] [--preview|--yes] [--json]
Expand Down
3 changes: 3 additions & 0 deletions packages/filegrc/src/collection-changes.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export function collectionChangesSinceReview(loaded, resourceType, review, progr
title: record.title || id,
kind: !prior ? "added" : !current ? "removed" : "changed",
fields,
...(prior && current ? { fieldChanges: [...new Set([...Object.keys(prior.value || {}), ...Object.keys(current.value || {})])]
.filter((field) => !sameValue(prior.value?.[field], current.value?.[field]))
.map((field) => ({ field, before: prior.value?.[field] ?? null, after: current.value?.[field] ?? null })) } : {}),
...(contentChanged ? { contentChanged: true } : {}),
...(membershipChanged ? { membershipChanged: true } : {})
});
Expand Down
53 changes: 53 additions & 0 deletions packages/filegrc/src/collection-review-handoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createHash } from "node:crypto";
import { realpath, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { getGitSummary } from "./git.js";
import { isWithin, resolveWorkspaceRoot } from "./paths.js";

export function reviewPayloadPath(root, programId, resourceType, collectionRevision, priorRevision) {
const slug = (value) => String(value || "workspace").replace(/[^a-z0-9-]/gi, "-").slice(0, 55);
const revision = createHash("sha256").update([root, programId, resourceType, collectionRevision, priorRevision].join("\0")).digest("hex").slice(0, 12);
return join(tmpdir(), `filegrc-${slug(programId)}-${slug(resourceType)}-${revision}-review.json`);
}

const quote = (value) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(String(value)) ? String(value) : `'${String(value).replaceAll("'", "'\\''")}'`;

export function collectionReviewCommands(assessment, shellArgument = quote) {
const path = shellArgument(assessment.reviewPayloadPath || reviewPayloadPath("", assessment.programId, assessment.resourceType));
const program = assessment.programId ? ` --program ${shellArgument(assessment.programId)}` : "";
const command = `npx filegrc review-collection ${assessment.resourceType}${program}`;
return [`${command} --scaffold --output ${path}`, `${command} ${path} --preview --summary --json`, `${command} ${path} --yes --summary --json`];
}

export async function writeReviewPayload(input, destination, payload) {
const root = resolveWorkspaceRoot(input);
const path = resolve(String(destination));
// Resolve the parent as well as the workspace so directory symlinks cannot
// turn a temporary draft into a source write inside the checkout.
const target = join(await realpath(dirname(path)), basename(path));
const checkout = getGitSummary(root).root || root;
if (isWithin(await realpath(checkout), target)) throw new Error("Write review payloads outside the checkout so the reviewed Git revision remains clean.");
try { await writeFile(target, `${JSON.stringify(payload, null, 2)}\n`, { flag: "wx", mode: 0o600 }); }
catch (error) {
if (error.code === "EEXIST") throw new Error(`Review payload already exists at ${target}. Inspect and reuse it, or choose a new --output path; the existing draft was not overwritten.`);
throw error;
}
return target;
}

export function summarizeCollectionReview(result, preview) {
const records = [...(result.changes?.create || []), ...(result.changes?.update || [])];
const review = records.find((record) => record.type === "collection-review" && record.status === "active");
const deltaCounts = (changes) => Object.fromEntries(["added", "removed", "changed"].map((key) => [key, changes?.[key]?.length || 0]));
return {
operation: "collection-review", status: preview ? "preview" : "recorded", resourceType: result.resourceType,
...result.reviewSummary,
review: review ? Object.fromEntries(["id", "decision", "rationale", "reviewedByIds", "reviewedOn", "scopeRevision", "collectionRevision", "knowledgeCutoffAt", "supersedesId"].filter((key) => review[key] !== undefined).map((key) => [key, review[key]])) : null,
retiredReviewIds: records.filter((record) => record.type === "collection-review" && record.status === "retired").map(({ id }) => id),
governedContentChanges: records.filter((record) => ["policy", "document", "training"].includes(record.type)).map(({ id, type, status, effectiveOn, activatedOn }) => ({ id, type, status, effectiveOn: effectiveOn || null, activatedOn: activatedOn || null })),
...(!preview ? { collectionReviewCurrent: result.assessment.complete } : {}),
...(result.firstOperatingTasks ? { firstOperatingTasks: result.firstOperatingTasks } : {}),
...(result.workflowDelta ? { workflowDelta: { assessments: result.workflowDelta.assessments, findings: deltaCounts(result.workflowDelta.findings), workItems: deltaCounts(result.workflowDelta.workItems), recommendedAfter: result.workflowDelta.recommendedAfter } } : {})
};
}
Loading
Loading