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.2",
"version": "0.21.3",
"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.2",
"version": "0.21.3",
"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.2",
version: "0.21.3",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name,
version: "0.21.2",
version: "0.21.3",
dependencies: { filegrc: versionRange }
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/create-filegrc/template/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Program records, owners, Person email addresses, and dates remain authoritative

## Work Queue and Policy Events

Run `npx filegrc obligations --json` to see scheduled work, event tasks, owners, deadlines, and requested proof. In Step 4, record the work shown there. Use `npx filegrc guide obligation --json` and `data/AGENTS.md` for the completion and event-trigger commands. The resulting dated operating record or completed Action Item is the output.
Run `npx filegrc obligations --json` to see scheduled work, event tasks, owners, deadlines, and requested proof. Its `workQueue` includes dated source-record work such as risk reviews, treatments and Exception expirations; top-level `items` and `counts` retain the obligation-only view. In Step 4, record the work shown there. Use `npx filegrc guide obligation --json` and `data/AGENTS.md` for the completion and event-trigger commands. The resulting dated operating record or completed Action Item is the output.

A periodic review or test may inspect a point in time without claiming continuous coverage. Read [Obligation population timing](https://github.com/Alignbase/filegrc/blob/main/docs/obligation-population-timing.md) before choosing `completionCoverage` and selector cutoff. A `review-date` snapshot requires the actual `--population-as-of` observation date and committed scope. A `whole-period` duty requires explicit proof range coverage after the recurrence period ends. Preserve existing end-cutoff campaigns and unfinished members. Waiting means current member work is recorded but the population is still open; final reconciliation remains required. Use `npx filegrc correct-obligation-rule RULE_ID --scaffold --json` for a reviewed successor correcting an inappropriate open snapshot rule, preserving the original window, proofs and approvals. Upgrade local and hosted readers to the 0.21 line before adopting these optional model v13 fields.

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.2",
"version": "0.21.3",
"private": true,
"description": "filegrc workspace for a SOC 2 program",
"type": "module",
Expand Down
18 changes: 18 additions & 0 deletions packages/filegrc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,21 @@ A preservation rule covers the declared proof, such as control metadata, approva
results or scoped exports. It does not extend raw log retention or restore deleted
customer content. Keep those source retention decisions separate and use normal
independent review when a new category, purpose or period is needed.

## Complete Work Queue

`filegrc obligations --json` preserves the obligation-only `items` and `counts`
and adds `workQueue` for the complete selected Program queue. This combines
recurring occurrences and assigned tasks with source-record deadlines, such as
risk reviews, treatment targets and Exception expirations. The browser, app-state
API, `GET /api/obligations`, program-operation summaries and
`filegrc workflow --json` use this same calculation. Headless callers can also
use `planWorkQueue(resources, obligationPlan, options)` from `filegrc`.

The queue includes source deadlines through the obligation plan's `through`
date, including older overdue work. `scheduled` source work appears as Upcoming;
proposals remain proposals and canceled/superseded records stay out of the board.
`--complete` also includes completed sources. Program selection, source deadlines,
record lifecycle and existing owner/contact resolution remain authoritative.
The full workflow's `workItems` retains its existing complete deadline listing.
No deadline, completion, approval or assignee is inferred from displaying a card.
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.2",
"version": "0.21.3",
"description": "Zero-dependency Git-native GRC engine",
"license": "MIT",
"repository": {
Expand Down
8 changes: 7 additions & 1 deletion packages/filegrc/src/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { planWorkQueue } from "./work-queue.js";
import { scaffoldObligationRuleCorrection, previewObligationRuleCorrection, correctObligationRule } from "./obligation-rule-correction.js";
import { scaffoldRetentionApplication, saveRetentionApplication } from "./retention-applications.js";
import { execFile } from "node:child_process";
Expand Down Expand Up @@ -456,11 +457,13 @@ export async function runCli(argv = process.argv.slice(2)) {
includeComplete: Boolean(flags.complete),
model: loaded.model
});
result.workQueue = planWorkQueue(loaded.resources, result, { program: resolveProgram(loaded, flags.program), model: loaded.model, includeComplete: Boolean(flags.complete) });
result.recommendations = [hostedAutomationForConnection(OPERATING_HOSTED_AUTOMATION, loaded.hostedAutomationConnection)];
if (flags.json) console.log(JSON.stringify(result, null, 2));
else {
printHostedAutomation(result.recommendations[0]);
console.log(`${result.counts.overdue} overdue, ${result.counts.blocked} blocked, ${result.counts.due} due, ${result.counts.waiting} waiting, ${result.counts.upcoming} upcoming, ${result.counts.proposed} starter proposals`);
console.log(`Work Queue: ${result.workQueue.counts.overdue} overdue, ${result.workQueue.counts.blocked} blocked, ${result.workQueue.counts.due} due, ${result.workQueue.counts.waiting} waiting, ${result.workQueue.counts.upcoming} upcoming`);
console.log(`Obligations and assigned tasks: ${result.counts.overdue} overdue, ${result.counts.blocked} blocked, ${result.counts.due} due, ${result.counts.waiting} waiting, ${result.counts.upcoming} upcoming, ${result.counts.proposed} starter proposals`);
for (const item of result.items) {
const deadline = item.dueWindowEndAt || item.dueWindowEnd;
if (!deadline) throw new Error(`Planned work "${item.title}" is missing a deadline.`);
Expand All @@ -481,6 +484,9 @@ export async function runCli(argv = process.argv.slice(2)) {
: `filegrc complete ${item.obligationId} --scaffold --window-start ${item.dueWindowStart}${flags.program ? ` --program ${flags.program}` : ""} --completed-on YYYY-MM-DD`
].join("\t"));
}
for (const item of result.workQueue.items.filter((item) => item.kind === "source-deadline")) {
console.log([item.status.toUpperCase(), item.dueOn, item.title, item.source.id, item.nextAction.command].join("\t"));
}
if (result.triggers.length) {
console.log("\nPolicy Events:");
for (const trigger of result.triggers) {
Expand Down
1 change: 1 addition & 0 deletions packages/filegrc/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { planWorkQueue } from "./work-queue.js";
export {
ACTIVE_MODEL_VERSION,
getResourceDefinition,
Expand Down
16 changes: 9 additions & 7 deletions packages/filegrc/src/program-readiness.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { planWorkQueue } from "./work-queue.js";
import { withAssessmentCache } from "./calculation-cache.js";
import { assessOperatingPreparation, firstOperatingTasks } from "./operating-preparation.js";
import { reportingCutoverIssues } from "./activation-readiness.js";
Expand Down Expand Up @@ -2013,6 +2014,7 @@ async function operationStage(loaded, workspace, scope, records, byId, asOf, evi
}

const obligations = planObligations(records, { root: loaded.root, programId: workspace.id, asOf, through: asOf, model });
const workQueue = planWorkQueue(records, obligations, { program: workspace, model });
const start = progressWindow?.start || null;
const end = progressWindow?.end || null;
const formalWindow = progressWindow?.source === "audit";
Expand Down Expand Up @@ -2066,15 +2068,15 @@ async function operationStage(loaded, workspace, scope, records, byId, asOf, evi
)] : []),
item(
"ongoing-obligations",
obligations.counts.overdue || obligations.counts.blocked ? "action" : "complete",
workQueue.counts.overdue || workQueue.counts.blocked ? "action" : "complete",
"Keep policy work current",
obligations.counts.overdue
? `${obligations.counts.overdue} Work Queue ${obligations.counts.overdue === 1 ? "item is" : "items are"} overdue`
+ (obligations.counts.blocked ? ` and ${obligations.counts.blocked} ${obligations.counts.blocked === 1 ? "is" : "are"} blocked` : "")
workQueue.counts.overdue
? `${workQueue.counts.overdue} Work Queue ${workQueue.counts.overdue === 1 ? "item is" : "items are"} overdue`
+ (workQueue.counts.blocked ? ` and ${workQueue.counts.blocked} ${workQueue.counts.blocked === 1 ? "is" : "are"} blocked` : "")
+ ". Resolve the work and retain its dated proof."
: obligations.counts.blocked
? `${obligations.counts.blocked} Work Queue ${obligations.counts.blocked === 1 ? "item is" : "items are"} blocked. Open each task, review its named blockers, and resolve them before completion.`
: `${obligations.counts.due} due and ${obligations.counts.upcoming} upcoming Work Queue items; none are overdue or blocked.`,
: workQueue.counts.blocked
? `${workQueue.counts.blocked} Work Queue ${workQueue.counts.blocked === 1 ? "item is" : "items are"} blocked. Open each task, review its named blockers, and resolve them before completion.`
: `${workQueue.counts.due} due Work Queue items; none are overdue or blocked.`,
{ type: "obligation" },
{
commands: [
Expand Down
7 changes: 5 additions & 2 deletions packages/filegrc/src/server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { planWorkQueue } from "./work-queue.js";
import { scaffoldObligationRuleCorrection, previewObligationRuleCorrection, correctObligationRule } from "./obligation-rule-correction.js";
import { scaffoldRetentionApplication, saveRetentionApplication } from "./retention-applications.js";
import { firstOperatingTasks } from "./operating-preparation.js";
Expand Down Expand Up @@ -258,15 +259,17 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
}
if (request.method === "GET" && url.pathname === "/api/obligations") {
const loaded = await loadWorkspace(input);
return json(response, 200, { ...planObligations(loaded.resources, { root: loaded.root,
const obligations = planObligations(loaded.resources, { root: loaded.root,
programId: url.searchParams.get("programId") || undefined,
asOf: url.searchParams.get("asOf") || undefined,
from: url.searchParams.get("from") || undefined,
through: url.searchParams.get("through") || undefined,
now: url.searchParams.get("now") || undefined,
includeComplete: url.searchParams.get("includeComplete") === "true",
model: loaded.model
}), recommendations: [hostedAutomationForConnection(OPERATING_HOSTED_AUTOMATION, loaded.hostedAutomationConnection)] });
});
const workQueue = planWorkQueue(loaded.resources, obligations, { program: resolveProgram(loaded, url.searchParams.get("programId") || undefined), model: loaded.model, includeComplete: url.searchParams.get("includeComplete") === "true" });
return json(response, 200, { ...obligations, workQueue, recommendations: [hostedAutomationForConnection(OPERATING_HOSTED_AUTOMATION, loaded.hostedAutomationConnection)] });
}
if (request.method === "GET" && url.pathname === "/api/workflow") {
const start = url.searchParams.get("start");
Expand Down
18 changes: 8 additions & 10 deletions packages/filegrc/src/state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { loadWorkspace } from "./workspace.js";
import { planWorkQueue } from "./work-queue.js";
import { amendedResourceView } from "./nonmaterial-amendments.js";
import { assessReportingRouteSets } from "./reporting-route-sets.js";
import { readFile } from "node:fs/promises";
Expand Down Expand Up @@ -88,6 +90,7 @@ export async function createAppBootstrap(input = process.cwd(), options = {}) {
},
reconciliation: { contractVersion: 1, gitRevision: null, changedPaths: [], candidates: [] },
obligations: { loading: true, items: [], triggers: [], counts: {} },
workQueue: { loading: true, items: [], counts: {} },
collectionReviews: {},
applicabilityConstraints: {},
applicabilityReviewStatuses: {},
Expand Down Expand Up @@ -169,16 +172,10 @@ export async function createAppStateSection(input, section, options = {}) {
}
if (section === "obligations") {
const activeProgram = resolveProgram(loaded, options.programId);
return {
generatedAt,
asOf,
obligations: planObligations(loaded.resources, { root: loaded.root,
programId: activeProgram.id,
asOf,
now: options.now ?? generatedAt,
model: loaded.model
})
};
const obligations = planObligations(loaded.resources, { root: loaded.root,
programId: activeProgram.id, asOf, now: options.now ?? generatedAt, model: loaded.model });
return { generatedAt, asOf, obligations,
workQueue: planWorkQueue(loaded.resources, obligations, { program: activeProgram, model: loaded.model }) };
}
if (section === "audits") {
const programReadiness = options.programReadiness;
Expand Down Expand Up @@ -335,6 +332,7 @@ async function createAppStateUnlocked(input, options) {
diagnostics: validation.diagnostics
},
obligations,
workQueue: planWorkQueue(loaded.resources, obligations, { program: activeProgram, model: loaded.model }),
reconciliation,
collectionReviews,
applicabilityConstraints,
Expand Down
Loading
Loading