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.3",
"version": "0.21.4",
"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.3",
"version": "0.21.4",
"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.3",
version: "0.21.4",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name,
version: "0.21.3",
version: "0.21.4",
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.3",
"version": "0.21.4",
"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.3",
"version": "0.21.4",
"description": "Zero-dependency Git-native GRC engine",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/filegrc/src/workflow-history-integrity.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ function canonicalJson(value, field = null, revisionMap = false) {
async function compareMarkdownAtRevision(loaded, entry, historical, diagnostics, occurrenceId = null) {
for (const markdown of markdownEntries(loaded.model, entry.record)) {
const historicalPath = markdownEntries(loaded.model, historical.record)
.find(({ slot }) => slot === markdown.slot)?.path;
.find(({ name }) => name === markdown.name)?.path;
const committed = historicalPath
? getFileAtRevision(loaded.root, historical.commit, `data/${historicalPath}`)
: null;
Expand Down
80 changes: 80 additions & 0 deletions packages/filegrc/test/workflow-proof.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { join } from "node:path";
import test from "node:test";
import { loadWorkspace, updateResource, validateWorkspace } from "../src/index.js";
import { validateWorkflowHistoryIntegrity } from "../src/workflow-history-integrity.js";
import { markdownEntries } from "../src/resource-markdown.js";
import { loadModel, SUPPORTED_MODEL_VERSIONS } from "../model/index.js";
import { workflowProofIds } from "../src/workflow-proof.js";
import { makeComprehensiveWorkspace } from "./fixtures.js";
import { commitWorkspaceFiles, initializeGitWorkspace, writeJson } from "./helpers.js";
Expand Down Expand Up @@ -68,9 +70,87 @@ test("mutable source context evolves while finalized proof stays bound to Git",
await writeJson(evidence.path, { ...evidence.record, sourceResourceIds: [] });
assert.ok((await historyDiagnostics()).some(({ message }) => message.includes(`Proof record "${evidence.record.id}" differs`)));
await writeJson(evidence.path, evidence.record);
const companion = markdownEntries(loaded.model, completion.record)[0];
const companionPath = join(root, "data", companion.path);
const originalCompanion = await readFile(companionPath, "utf8").catch(() => null);
await writeFile(companionPath, "# Changed single-slot proof\n");
assert.ok((await historyDiagnostics()).some(({ code, path }) => code === "rewritten-finalized-content" && path === `data/${companion.path}`));
if (originalCompanion === null) await rm(companionPath);
else await writeFile(companionPath, originalCompanion);
const attachment = evidence.record.filePaths?.[0];
assert.ok(attachment);
const original = await readFile(join(root, "data", attachment));
await writeFile(join(root, "data", attachment), Buffer.concat([original, Buffer.from("rewritten")]));
assert.ok((await historyDiagnostics()).some(({ message }) => /attachment/i.test(message)));
});


test("supported models identify every meeting companion by name", () => {
for (const version of SUPPORTED_MODEL_VERSIONS) {
const model = loadModel(version);
const multiSlotTypes = Object.entries(model.resources)
.filter(([, definition]) => Object.keys(definition.markdown || {}).length > 1)
.map(([type]) => type);
assert.deepEqual(multiSlotTypes, ["meeting"], `model v${version}`);
const entries = markdownEntries(model, { type: "meeting", id: "meeting-example" });
assert.deepEqual(entries.map(({ name }) => name), ["agenda", "minutes"]);
assert.equal(entries.find(({ primary }) => primary).name, "minutes");
assert.match(entries[0].path, /-agenda\.md$/);
assert.match(entries[1].path, /meeting-example\.md$/);
}
});

for (const { version, extended = false } of [{ version: "9" }, { version: "13" }, { version: "13", extended: true }]) {
test(`committed meeting proof preserves each Markdown companion in model v${version}${extended ? " with extended slots" : ""}`, async (t) => {
const root = await mkdtemp(join(tmpdir(), "filegrc-meeting-proof-"));
t.after(() => rm(root, { recursive: true, force: true }));
await makeComprehensiveWorkspace(root, version);
const extendModel = (loaded) => {
if (extended) loaded.model.resources.meeting.markdown = {
followUpPath: { label: "Follow-up", primary: true },
minutes: { label: "Minutes" },
agenda: { label: "Agenda" }
};
return loaded;
};
const loaded = extendModel(await loadWorkspace(root));
const meeting = loaded.entries.find(({ record }) => record.type === "meeting");
const occurrence = loaded.entries.find(({ record }) => record.type === "obligation-occurrence");
const obligation = loaded.entries.find(({ record }) => record.type === "obligation");
obligation.record.activityType = "meeting";
await writeJson(obligation.path, obligation.record);
occurrence.record.members[0].resourceId = loaded.workspace.id;
occurrence.record.members[0].completionResourceIds = [meeting.record.id];
await writeJson(occurrence.path, occurrence.record);
const companions = markdownEntries(loaded.model, meeting.record);
const originals = new Map(companions.map(({ path, name }) => [path, `# ${name}\nDistinct ${name} proof.\n`]));
for (const [path, content] of originals) await writeFile(join(root, "data", path), content);
await initializeGitWorkspace(root, { message: "Finalize meeting and occurrence with distinct companions" });
const diagnostics = async () => {
const result = [];
await validateWorkflowHistoryIntegrity(extendModel(await loadWorkspace(root)), result);
return result;
};
assert.deepEqual(await diagnostics(), []);
if (version === "9") assert.deepEqual((await validateWorkspace(root)).diagnostics, []);
for (const { path } of companions) {
const absolute = join(root, "data", path);
await writeFile(absolute, "# Changed proof\n");
assert.ok((await diagnostics()).some((item) => item.code === "rewritten-finalized-content" && item.path === `data/${path}`));
await writeFile(absolute, originals.get(path));
await rm(absolute);
assert.ok((await diagnostics()).some((item) => item.code === "rewritten-finalized-content" && item.path === `data/${path}`));
await writeFile(absolute, originals.get(path));
}
assert.deepEqual(await diagnostics(), []);
// An optional companion absent at finalization cannot be added afterward.
const agenda = companions.find(({ name }) => name === "agenda");
await rm(join(root, "data", agenda.path));
// Start a separate history so the absence is part of the first final version.
await rm(join(root, ".git"), { recursive: true, force: true });
await initializeGitWorkspace(root, { message: "Finalize meeting without optional agenda" });
assert.deepEqual(await diagnostics(), []);
await writeFile(join(root, "data", agenda.path), originals.get(agenda.path));
assert.ok((await diagnostics()).some((item) => item.code === "rewritten-finalized-content" && item.path === `data/${agenda.path}`));
});
}
Loading