filegrc 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/model/index.js +22 -4
- package/model/v5.json +10233 -0
- package/model/v6.json +10358 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +182 -45
- package/src/audit-transition.js +3 -2
- package/src/batch-review.js +7 -6
- package/src/cli.js +143 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +181 -0
- package/src/evidence-packet.js +199 -78
- package/src/external-reviewer.js +5 -4
- package/src/files.js +173 -35
- package/src/git.js +71 -7
- package/src/index.js +11 -1
- package/src/model-migration.js +363 -7
- package/src/obligations.js +14 -11
- package/src/policy-library.js +7 -3
- package/src/program-lifecycle.js +131 -3
- package/src/program-path.js +19 -13
- package/src/program-readiness.js +338 -71
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +36 -7
- package/src/setup.js +8 -5
- package/src/soc2.js +3 -2
- package/src/validate.js +178 -16
- package/src/web.js +264 -17
- package/src/workflow.js +38 -7
- package/src/workspace.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "filegrc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Zero-dependency Git-native GRC engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"src"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node
|
|
24
|
+
"test": "node ../../scripts/run-filegrc-tests.mjs",
|
|
25
|
+
"test:fast": "node ../../scripts/run-filegrc-tests.mjs --fast",
|
|
26
|
+
"test:full": "npm test"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
27
29
|
"node": ">=20"
|
package/src/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modelSupports } from "../model/index.js";
|
|
1
2
|
import { createResourceId } from "./id.js";
|
|
2
3
|
import { markdownEntries } from "./resource-markdown.js";
|
|
3
4
|
import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
|
|
@@ -116,7 +117,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
116
117
|
recommendedMarkdown.length
|
|
117
118
|
? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
|
|
118
119
|
: "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
|
|
119
|
-
...(type === "program" &&
|
|
120
|
+
...(type === "program" && modelSupports(loaded.model, "program-scope")
|
|
120
121
|
? ["Review Requirement applicability with npx filegrc review-applicability --type requirement --scaffold, then preview and apply the reviewed decisions as one validated batch."]
|
|
121
122
|
: []),
|
|
122
123
|
"Run npx filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
|
|
@@ -125,7 +126,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
125
126
|
"Required and status-dependent fields are complete, and the lifecycle status matches the facts.",
|
|
126
127
|
"Every relationship resolves to the intended existing record.",
|
|
127
128
|
"Dates describe the business event in the workspace time zone, not the file edit time.",
|
|
128
|
-
...(type === "program" &&
|
|
129
|
+
...(type === "program" && modelSupports(loaded.model, "program-scope")
|
|
129
130
|
? ["Every selected Requirement has an applicable or not-applicable decision reviewed against the current Program scope."]
|
|
130
131
|
: []),
|
|
131
132
|
...(recommendedMarkdown.length
|
|
@@ -217,7 +218,7 @@ function applyModelScaffoldDefaults(record, loaded, options = {}) {
|
|
|
217
218
|
}[program?.assuranceGoal];
|
|
218
219
|
if (kind) record.auditKind = kind;
|
|
219
220
|
for (const field of ["frameworkIds", "systemIds", "requirementIds", "controlIds"]) {
|
|
220
|
-
if (field === "requirementIds" &&
|
|
221
|
+
if (field === "requirementIds" && modelSupports(loaded.model, "program-scope")) {
|
|
221
222
|
record[field] = (program.requirementApplicability || []).filter(({ decision }) => decision === "applicable").map(({ requirementId }) => requirementId);
|
|
222
223
|
} else if (program?.[field]?.length) record[field] = [...program[field]];
|
|
223
224
|
}
|
package/src/audit-preparation.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { modelSupports } from "../model/index.js";
|
|
4
|
+
import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
|
|
3
5
|
import {
|
|
4
6
|
coverageContains,
|
|
5
7
|
coverageEnd,
|
|
@@ -15,6 +17,7 @@ import { currentPartyPeople, partiesIndependent } from "./parties.js";
|
|
|
15
17
|
import { resolveDataPath } from "./paths.js";
|
|
16
18
|
import { assessProgramReadiness } from "./program-readiness.js";
|
|
17
19
|
import { markdownEntries } from "./resource-markdown.js";
|
|
20
|
+
import { contentRevisionBindingsMatch, governedDocumentIsOperating } from "./program-lifecycle.js";
|
|
18
21
|
import {
|
|
19
22
|
auditorWasEngaged,
|
|
20
23
|
missingSoc2References,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
signatoryAppointmentIssue,
|
|
26
29
|
subsequentEventsReviewIssue
|
|
27
30
|
} from "./soc2.js";
|
|
31
|
+
import { currentCalendarDate } from "./time.js";
|
|
28
32
|
import { loadWorkspace } from "./workspace.js";
|
|
29
33
|
|
|
30
34
|
const NON_EVIDENCE_RECORD_TYPES = new Set([
|
|
@@ -65,9 +69,13 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
65
69
|
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
66
70
|
|
|
67
71
|
const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
|
|
72
|
+
asOf: options.asOf,
|
|
68
73
|
generatedAt: options.generatedAt,
|
|
69
74
|
programId: audit?.programId
|
|
70
75
|
});
|
|
76
|
+
const documentActivations = audit && modelSupports(loaded.model, "governed-document-activation")
|
|
77
|
+
? await auditDocumentActivationAssessments(loaded, audit, byId, programReadiness.asOf)
|
|
78
|
+
: [];
|
|
71
79
|
const stages = [
|
|
72
80
|
programFoundationStage(programReadiness, loaded.workspace),
|
|
73
81
|
engagementStage(audit, byId, programReadiness),
|
|
@@ -75,7 +83,7 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
75
83
|
];
|
|
76
84
|
const fieldworkSections = audit
|
|
77
85
|
? [
|
|
78
|
-
await documentsStage(loaded, audit, byId),
|
|
86
|
+
await documentsStage(loaded, audit, byId, programReadiness.asOf),
|
|
79
87
|
evidenceStage(audit, records, byId, loaded.model),
|
|
80
88
|
populationsStage(audit, records, byId, loaded.model)
|
|
81
89
|
]
|
|
@@ -107,10 +115,23 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
107
115
|
&& coverageStart(audit.coverage)
|
|
108
116
|
&& coverageEnd(audit.coverage)
|
|
109
117
|
&& initializationNeeded(audit, records, loaded.model)),
|
|
118
|
+
documentActivations,
|
|
110
119
|
stages
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
export async function assessAuditDocumentActivations(input = process.cwd(), options = {}) {
|
|
124
|
+
const loaded = input?.resources && input?.model && input?.entries
|
|
125
|
+
? input
|
|
126
|
+
: await loadWorkspace(input);
|
|
127
|
+
if (!modelSupports(loaded.model, "governed-document-activation")) return [];
|
|
128
|
+
const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
|
|
129
|
+
if (!audit) throw new Error(`Audit "${options.auditId || ""}" was not found.`);
|
|
130
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
131
|
+
const asOf = options.asOf || currentCalendarDate(loaded.workspace.timezone);
|
|
132
|
+
return auditDocumentActivationAssessments(loaded, audit, byId, asOf);
|
|
133
|
+
}
|
|
134
|
+
|
|
114
135
|
export async function prepareAuditWorkspace(input, options = {}) {
|
|
115
136
|
const loaded = await loadWorkspace(input);
|
|
116
137
|
const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
|
|
@@ -168,7 +189,7 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
168
189
|
const selectedControls = (audit.controlIds || [])
|
|
169
190
|
.map((id) => loaded.resources.find((record) => record.id === id))
|
|
170
191
|
.filter(Boolean);
|
|
171
|
-
const v4 =
|
|
192
|
+
const v4 = modelSupports(loaded.model, "program-scope");
|
|
172
193
|
const sourceSystems = loaded.resources.filter((record) => record.type === (v4 ? "component" : "system"));
|
|
173
194
|
const populations = (audit.auditKind === "soc-2-type-2" ? model.populationTemplates || [] : [])
|
|
174
195
|
.filter((template) => !existingKinds.has(template.kind))
|
|
@@ -269,7 +290,7 @@ function scopeStage(loaded, audit, records, byId, programReadiness) {
|
|
|
269
290
|
));
|
|
270
291
|
}
|
|
271
292
|
|
|
272
|
-
const v4 =
|
|
293
|
+
const v4 = modelSupports(programReadiness.dataModelVersion, "program-scope");
|
|
273
294
|
const engagementStart = coverageStart(audit.coverage);
|
|
274
295
|
const engagementEnd = coverageEnd(audit.coverage);
|
|
275
296
|
const scopeRevision = assessScopeRevision(loaded, audit, v4);
|
|
@@ -744,9 +765,12 @@ function engagementStage(audit, byId, programReadiness) {
|
|
|
744
765
|
const engagementTermsComplete = Boolean(
|
|
745
766
|
engagementTerms?.type === "document"
|
|
746
767
|
&& engagementTerms.documentKind === "soc2-engagement-terms"
|
|
747
|
-
&&
|
|
768
|
+
&& governedDocumentIsOperating(
|
|
769
|
+
engagementTerms,
|
|
770
|
+
programReadiness.asOf,
|
|
771
|
+
{ modelVersion: programReadiness.dataModelVersion }
|
|
772
|
+
)
|
|
748
773
|
&& engagementTerms.approvedOn
|
|
749
|
-
&& engagementTerms.effectiveOn
|
|
750
774
|
);
|
|
751
775
|
const acknowledgementPeople = (audit.managementAcknowledgedByIds || [])
|
|
752
776
|
.map((id) => byId.get(id))
|
|
@@ -805,7 +829,7 @@ function engagementStage(audit, byId, programReadiness) {
|
|
|
805
829
|
}
|
|
806
830
|
)
|
|
807
831
|
];
|
|
808
|
-
if (
|
|
832
|
+
if (modelSupports(programReadiness.dataModelVersion, "guided-workflow")) {
|
|
809
833
|
items.push(
|
|
810
834
|
item(
|
|
811
835
|
"engagement-terms",
|
|
@@ -853,51 +877,160 @@ function fieldworkStage(audit, sections) {
|
|
|
853
877
|
);
|
|
854
878
|
}
|
|
855
879
|
|
|
856
|
-
async function
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
880
|
+
async function auditDocumentActivationAssessments(loaded, audit, byId, asOf) {
|
|
881
|
+
const links = new Map();
|
|
882
|
+
const addLink = (documentId, role, definition = null) => {
|
|
883
|
+
if (!documentId) return;
|
|
884
|
+
const current = links.get(documentId) || { documentId, roles: [], definitions: [] };
|
|
885
|
+
current.roles.push(role);
|
|
886
|
+
if (definition) current.definitions.push(definition);
|
|
887
|
+
links.set(documentId, current);
|
|
888
|
+
};
|
|
889
|
+
addLink(audit.engagementTermsDocumentId, "engagement-terms");
|
|
890
|
+
for (const definition of applicableManagementDocuments(audit, loaded.model.auditReadiness || {})) {
|
|
891
|
+
addLink(audit[definition.field], definition.field, definition);
|
|
892
|
+
}
|
|
893
|
+
for (const documentId of audit.supplementalDocumentIds || []) addLink(documentId, "supplemental");
|
|
894
|
+
|
|
895
|
+
const assessments = [];
|
|
896
|
+
for (const link of links.values()) {
|
|
897
|
+
const document = byId.get(link.documentId);
|
|
898
|
+
if (document?.type !== "document") continue;
|
|
899
|
+
if (link.roles.every((role) => role === "supplemental") && document.workflowScope !== "engagement") continue;
|
|
900
|
+
const source = await primaryMarkdown(loaded, document);
|
|
901
|
+
const issues = [];
|
|
902
|
+
if (document.workflowScope !== "engagement") issues.push("Set workflowScope to engagement.");
|
|
903
|
+
if (document.template === true) issues.push("Replace the starter template with the completed engagement Document.");
|
|
904
|
+
if (!substantiveMarkdown(source)) issues.push("Complete the Document Markdown.");
|
|
905
|
+
const placeholders = openPlaceholderCount(source);
|
|
906
|
+
if (placeholders) issues.push(`Resolve ${placeholders} open ${placeholders === 1 ? "placeholder" : "placeholders"}.`);
|
|
907
|
+
if (link.roles.includes("engagement-terms") && document.documentKind !== "soc2-engagement-terms") {
|
|
908
|
+
issues.push("Use documentKind soc2-engagement-terms for the accepted engagement terms.");
|
|
866
909
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
870
|
-
.map((record) => record.reconciledOn)
|
|
871
|
-
.filter(Boolean)
|
|
872
|
-
.sort()
|
|
873
|
-
.at(-1);
|
|
874
|
-
if (latestReconciliation && document.approvedOn < latestReconciliation) {
|
|
875
|
-
contentIssues.push("Approve the period completeness statement after the last population reconciliation.");
|
|
876
|
-
}
|
|
910
|
+
for (const definition of link.definitions) {
|
|
911
|
+
issues.push(...await managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId));
|
|
877
912
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
913
|
+
const approvalComplete = Boolean(
|
|
914
|
+
["approved", "active"].includes(document.status)
|
|
915
|
+
&& document.approvedOn
|
|
916
|
+
&& document.approvedContentRevisions
|
|
917
|
+
&& (document.activationBasis === "legacy-v4"
|
|
918
|
+
? (document.ownerIds || []).length
|
|
919
|
+
: currentPartyPeople(document.ownerIds || [], byId).size)
|
|
920
|
+
&& (document.approverIds || []).length
|
|
921
|
+
&& partiesIndependent(document.ownerIds, document.approverIds, byId)
|
|
922
|
+
);
|
|
923
|
+
if (!approvalComplete) issues.push("Complete the independent approval and bind the approved Markdown revision first.");
|
|
924
|
+
if (document.status === "active" && document.activationBasis !== "legacy-v4") {
|
|
925
|
+
if (document.activationBasis !== "recorded") issues.push("Record the Step 5 activation basis.");
|
|
926
|
+
if (!document.activatedOn) issues.push("Record the separate Step 5 activation date.");
|
|
927
|
+
else if (document.activatedOn > asOf) issues.push(`The activation date ${document.activatedOn} is after ${asOf}.`);
|
|
928
|
+
if (!(document.activatedByIds || []).length) issues.push("Name the active Person who performed activation.");
|
|
889
929
|
else {
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
930
|
+
const invalidActorIds = document.activatedByIds.filter((id) => !personWasActiveOn(byId.get(id), document.activatedOn));
|
|
931
|
+
if (invalidActorIds.length) issues.push(`Activation actors were not active on ${document.activatedOn}: ${invalidActorIds.join(", ")}.`);
|
|
892
932
|
}
|
|
933
|
+
if (!document.activatedContentRevisions) issues.push("Bind activation to the exact Document Markdown revision.");
|
|
934
|
+
else if (!contentRevisionBindingsMatch(document.approvedContentRevisions, document.activatedContentRevisions)) {
|
|
935
|
+
issues.push("The activated revision must match the unchanged approved revision.");
|
|
936
|
+
}
|
|
937
|
+
if (!document.effectiveOn) issues.push("Record the effective date.");
|
|
938
|
+
else if (document.effectiveOn > asOf) issues.push(`The Document does not become effective until ${document.effectiveOn}.`);
|
|
939
|
+
}
|
|
940
|
+
const operating = governedDocumentIsOperating(document, asOf, loaded.model) && issues.length === 0;
|
|
941
|
+
const state = operating
|
|
942
|
+
? "active-and-operating"
|
|
943
|
+
: document.status === "active"
|
|
944
|
+
? "active-with-gaps"
|
|
945
|
+
: document.status === "approved" && issues.length === 0
|
|
946
|
+
? "ready-to-activate"
|
|
947
|
+
: document.status === "approved"
|
|
948
|
+
? "approved-not-ready"
|
|
949
|
+
: "approval-pending";
|
|
950
|
+
assessments.push({
|
|
951
|
+
auditId: audit.id,
|
|
952
|
+
documentId: document.id,
|
|
953
|
+
title: document.title,
|
|
954
|
+
roles: [...new Set(link.roles)],
|
|
955
|
+
state,
|
|
956
|
+
label: auditDocumentActivationLabel(state),
|
|
957
|
+
issues: [...new Set(issues)],
|
|
958
|
+
approvedOn: document.approvedOn || null,
|
|
959
|
+
activatedOn: document.activatedOn || null,
|
|
960
|
+
activatedByIds: document.activatedByIds || [],
|
|
961
|
+
effectiveOn: document.effectiveOn || null,
|
|
962
|
+
approvalRevisionBound: Boolean(document.approvedContentRevisions),
|
|
963
|
+
activationRevisionBound: Boolean(document.activatedContentRevisions),
|
|
964
|
+
gapCount: [...new Set(issues)].length
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return assessments.sort((left, right) => left.title.localeCompare(right.title));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function auditDocumentActivationLabel(state) {
|
|
971
|
+
return ({
|
|
972
|
+
"approval-pending": "Approval pending in Step 5",
|
|
973
|
+
"approved-not-ready": "Approved, engagement facts incomplete",
|
|
974
|
+
"ready-to-activate": "Ready to activate in Step 5",
|
|
975
|
+
"active-with-gaps": "Active with lifecycle or engagement gaps",
|
|
976
|
+
"active-and-operating": "Active and ready for the engagement"
|
|
977
|
+
})[state] || state;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId) {
|
|
981
|
+
const issues = managementDocumentContentIssues(source, definition, audit);
|
|
982
|
+
if (document.documentKind !== definition.kind) {
|
|
983
|
+
issues.push(`Use documentKind ${definition.kind} for this Audit field.`);
|
|
984
|
+
}
|
|
985
|
+
const engagementEnd = coverageEnd(audit?.coverage);
|
|
986
|
+
if (document.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
|
|
987
|
+
issues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
|
|
988
|
+
}
|
|
989
|
+
if (definition.kind === "soc2-period-completeness" && document.approvedOn) {
|
|
990
|
+
const latestReconciliation = loaded.resources
|
|
991
|
+
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
992
|
+
.map((record) => record.reconciledOn)
|
|
993
|
+
.filter(Boolean)
|
|
994
|
+
.sort()
|
|
995
|
+
.at(-1);
|
|
996
|
+
if (latestReconciliation && document.approvedOn < latestReconciliation) {
|
|
997
|
+
issues.push("Approve the period completeness statement after the last population reconciliation.");
|
|
893
998
|
}
|
|
999
|
+
}
|
|
1000
|
+
if (definition.kind === "soc2-management-representation") {
|
|
1001
|
+
const signedEvidence = (document.evidenceIds || [])
|
|
1002
|
+
.map((id) => byId.get(id))
|
|
1003
|
+
.find((record) => (
|
|
1004
|
+
record?.type === "evidence"
|
|
1005
|
+
&& record.status === "verified"
|
|
1006
|
+
&& record.artifactKind === "signed-record"
|
|
1007
|
+
&& record.artifactSubtype === "signed-management-representation"
|
|
1008
|
+
&& (record.filePaths || []).length
|
|
1009
|
+
));
|
|
1010
|
+
if (!signedEvidence) issues.push("Link verified signed-record Evidence with subtype signed-management-representation and a fixed-format copy of the signed letter.");
|
|
1011
|
+
else {
|
|
1012
|
+
const dateIssue = signedRepresentationDateIssue(signedEvidence, audit, loaded.model.modelVersion);
|
|
1013
|
+
if (dateIssue) issues.push(dateIssue);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return issues;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function documentsStage(loaded, audit, byId, asOf) {
|
|
1020
|
+
const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
|
|
1021
|
+
const items = [];
|
|
1022
|
+
for (const definition of definitions) {
|
|
1023
|
+
const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
|
|
1024
|
+
const source = document ? await primaryMarkdown(loaded, document) : "";
|
|
1025
|
+
const contentIssues = document
|
|
1026
|
+
? await managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId)
|
|
1027
|
+
: [];
|
|
894
1028
|
const complete = Boolean(
|
|
895
1029
|
document
|
|
896
1030
|
&& document.type === "document"
|
|
897
1031
|
&& document.template !== true
|
|
898
|
-
&& document
|
|
1032
|
+
&& governedDocumentIsOperating(document, asOf, loaded.model)
|
|
899
1033
|
&& document.approvedOn
|
|
900
|
-
&& document.effectiveOn
|
|
901
1034
|
&& (document.ownerIds || []).length
|
|
902
1035
|
&& (document.approverIds || []).length
|
|
903
1036
|
&& partiesIndependent(document.ownerIds, document.approverIds, byId)
|
|
@@ -912,7 +1045,11 @@ async function documentsStage(loaded, audit, byId) {
|
|
|
912
1045
|
complete ? "complete" : representationLater ? "later" : "action",
|
|
913
1046
|
definition.title,
|
|
914
1047
|
complete
|
|
915
|
-
?
|
|
1048
|
+
? document.activationBasis === "legacy-v4"
|
|
1049
|
+
? "Linked Markdown is complete and effective. Its active state is preserved from model v4, which did not record approval and activation as separate events."
|
|
1050
|
+
: modelSupports(loaded.model, "governed-document-activation")
|
|
1051
|
+
? "Linked Markdown is complete, independently approved, separately activated by a named Person, revision-bound at both events, and effective."
|
|
1052
|
+
: "Linked Markdown is complete, active, approved, and effective."
|
|
916
1053
|
: document
|
|
917
1054
|
? `${definition.timing} ${contentIssues[0] || "Complete and approve the engagement-specific document."}`
|
|
918
1055
|
: `Link the starter ${definition.title.toLowerCase()} to this audit. ${definition.timing}`,
|
|
@@ -925,7 +1062,7 @@ async function documentsStage(loaded, audit, byId) {
|
|
|
925
1062
|
export function signedRepresentationDateIssue(evidence, audit, modelVersion) {
|
|
926
1063
|
const engagementEnd = coverageEnd(audit?.coverage);
|
|
927
1064
|
const timingMessage = `The signed representation must be dated on or after the engagement ${audit?.auditKind === "soc-2-type-1" ? "date" : "period end"}.`;
|
|
928
|
-
if (
|
|
1065
|
+
if (!modelSupports(modelVersion, "program-scope")) {
|
|
929
1066
|
return !evidence?.collectedOn || (engagementEnd && evidence.collectedOn < engagementEnd)
|
|
930
1067
|
? timingMessage
|
|
931
1068
|
: null;
|
|
@@ -999,7 +1136,7 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
999
1136
|
externalEvidence[0] || { type: "evidence" }
|
|
1000
1137
|
)
|
|
1001
1138
|
];
|
|
1002
|
-
const v4 =
|
|
1139
|
+
const v4 = modelSupports(model, "program-scope");
|
|
1003
1140
|
const systems = records.filter((record) => record.type === (v4 ? "component" : "system") && record.status === "active");
|
|
1004
1141
|
const sourceId = (record) => v4 ? record.sourceComponentId : record.sourceSystemId;
|
|
1005
1142
|
for (const source of model.evidenceSourceFamilies || []) {
|
|
@@ -1103,7 +1240,7 @@ function auditorStage(audit, byId, modelVersion) {
|
|
|
1103
1240
|
const signatoryStatus = !audit || ["planned", "in-progress", "fieldwork"].includes(audit.status)
|
|
1104
1241
|
? "later"
|
|
1105
1242
|
: signatoryIssue ? "action" : "complete";
|
|
1106
|
-
const managementItems =
|
|
1243
|
+
const managementItems = modelSupports(modelVersion, "program-scope") ? [
|
|
1107
1244
|
item(
|
|
1108
1245
|
"subsequent-events",
|
|
1109
1246
|
subsequentEventsStatus,
|
package/src/audit-transition.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { modelSupports } from "../model/index.js";
|
|
1
2
|
import { createResource } from "./files.js";
|
|
2
3
|
import { createResourceId } from "./id.js";
|
|
3
4
|
import { loadWorkspace } from "./workspace.js";
|
|
4
5
|
|
|
5
6
|
export async function planNextAuditCycle(input = process.cwd(), options = {}) {
|
|
6
7
|
const loaded = await loadWorkspace(input);
|
|
7
|
-
if (!
|
|
8
|
-
throw new Error("Audit-cycle carry-forward requires a model v3 or
|
|
8
|
+
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
9
|
+
throw new Error("Audit-cycle carry-forward requires a model v3 or newer workspace.");
|
|
9
10
|
}
|
|
10
11
|
const prior = loaded.resources.find((record) => (
|
|
11
12
|
record.type === "audit" && record.id === options.priorAuditId
|
package/src/batch-review.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { modelSupports } from "../model/index.js";
|
|
3
4
|
import { applyResourceBatch } from "./files.js";
|
|
4
5
|
import { getWorkspaceRevisionSnapshot } from "./git.js";
|
|
5
6
|
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
@@ -20,8 +21,8 @@ const REVIEWABLE_TYPES = new Set([
|
|
|
20
21
|
export async function scaffoldApplicabilityReview(input = process.cwd(), options = {}) {
|
|
21
22
|
const context = await applicabilityReviewContext(input);
|
|
22
23
|
const { loaded } = context;
|
|
23
|
-
if (!
|
|
24
|
-
throw new Error("Batch applicability review requires a model v3 or
|
|
24
|
+
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
25
|
+
throw new Error("Batch applicability review requires a model v3 or newer workspace.");
|
|
25
26
|
}
|
|
26
27
|
const requestedType = options.type ? String(options.type) : null;
|
|
27
28
|
if (requestedType && !REVIEWABLE_TYPES.has(requestedType)) {
|
|
@@ -34,7 +35,7 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
|
|
|
34
35
|
const records = loaded.resources.filter((record) => (
|
|
35
36
|
REVIEWABLE_TYPES.has(record.type)
|
|
36
37
|
&& (!requestedType || record.type === requestedType)
|
|
37
|
-
&& (record.type === "requirement" &&
|
|
38
|
+
&& (record.type === "requirement" && modelSupports(loaded.model, "program-scope")
|
|
38
39
|
? !reviewedRequirementIds.has(record.id)
|
|
39
40
|
: !record.applicabilityReview)
|
|
40
41
|
&& !["retired", "superseded"].includes(record.status)
|
|
@@ -64,8 +65,8 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
|
|
|
64
65
|
|
|
65
66
|
function planApplicabilityReviewWithContext(context, options) {
|
|
66
67
|
const { basis, loaded } = context;
|
|
67
|
-
if (!
|
|
68
|
-
throw new Error("Batch applicability review requires a model v3 or
|
|
68
|
+
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
69
|
+
throw new Error("Batch applicability review requires a model v3 or newer workspace.");
|
|
69
70
|
}
|
|
70
71
|
if (!Array.isArray(options.decisions) || !options.decisions.length) {
|
|
71
72
|
throw new Error("Applicability review needs at least one decision.");
|
|
@@ -112,7 +113,7 @@ function planApplicabilityReviewWithContext(context, options) {
|
|
|
112
113
|
if (!["applicable", "not-applicable"].includes(result)) {
|
|
113
114
|
throw new Error(`Requirement "${record.id}" must be applicable or not-applicable.`);
|
|
114
115
|
}
|
|
115
|
-
if (
|
|
116
|
+
if (modelSupports(loaded.model, "program-scope")) {
|
|
116
117
|
v4RequirementDecisions.push({
|
|
117
118
|
requirementId: record.id,
|
|
118
119
|
decision: result,
|