filegrc 0.7.1 → 0.8.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.
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { modelSupports } from "../model/index.js";
2
3
  import { collectionRevision } from "./collection-revision.js";
3
4
  import { scopedCollectionRecords } from "./collection-scope.js";
4
5
  import { applyResourceBatch } from "./files.js";
@@ -26,7 +27,7 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
26
27
  record.type === "collection-review"
27
28
  && record.resourceType === resourceType
28
29
  && record.status !== "retired"
29
- && (String(loaded.model.modelVersion) !== "4" || (record.scopeResourceIds || []).includes(program.id))
30
+ && (!modelSupports(loaded.model, "program-scope") || (record.scopeResourceIds || []).includes(program.id))
30
31
  ));
31
32
  const review = reviewEntry?.record || null;
32
33
  const authoritativeSourceId = review?.decision === "externally-managed"
@@ -86,7 +87,7 @@ export async function scaffoldCollectionReview(input = process.cwd(), options =
86
87
  rationale: null,
87
88
  reviewedByIds: [],
88
89
  reviewedOn: null,
89
- ...(String(loaded.model.modelVersion) === "4"
90
+ ...(modelSupports(loaded.model, "program-scope")
90
91
  ? { authoritativeComponentId: null }
91
92
  : { authoritativeSystemId: null })
92
93
  };
@@ -103,7 +104,7 @@ export async function planCollectionReview(input = process.cwd(), options = {})
103
104
  const reviewedByIds = [...new Set((options.reviewedByIds || []).map(String).filter(Boolean))];
104
105
  const reviewedOn = String(options.reviewedOn || "").trim();
105
106
  const scopeRevision = String(options.scopeRevision || getGitSummary(loaded.root).commit || "uncommitted").trim();
106
- const v4 = String(loaded.model.modelVersion) === "4";
107
+ const v4 = modelSupports(loaded.model, "program-scope");
107
108
  const authoritativeSourceId = String(v4 ? options.authoritativeComponentId : options.authoritativeSystemId || "").trim();
108
109
  if (!(configuration.decisions || ["complete"]).includes(decision)) {
109
110
  throw new Error(
@@ -1,7 +1,8 @@
1
+ import { modelSupports } from "../model/index.js";
1
2
  import { programComponents, selectedRequirementIds } from "./program.js";
2
3
 
3
4
  export function scopedCollectionRecords(loaded, resourceType, program) {
4
- if (String(loaded.model.modelVersion) !== "4") {
5
+ if (!modelSupports(loaded.model, "program-scope")) {
5
6
  return loaded.resources.filter((record) => record.type === resourceType);
6
7
  }
7
8
  if (resourceType === "vendor") {
@@ -37,7 +38,7 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
37
38
 
38
39
  export function collectionRevisionInputs(loaded, resourceType, program) {
39
40
  const reviewed = scopedCollectionRecords(loaded, resourceType, program);
40
- if (String(loaded.model.modelVersion) !== "4") {
41
+ if (!modelSupports(loaded.model, "program-scope")) {
41
42
  return reviewed.map((record) => ({ record, value: record }));
42
43
  }
43
44
  const byId = new Map(loaded.resources.map((record) => [record.id, record]));
@@ -94,7 +95,7 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
94
95
  }
95
96
 
96
97
  export function collectionScopeRevisionFacts(loaded, resourceType, program) {
97
- if (String(loaded.model.modelVersion) !== "4") {
98
+ if (!modelSupports(loaded.model, "program-scope")) {
98
99
  const common = { programId: program?.id ?? null };
99
100
  if (resourceType === "framework") {
100
101
  return {
@@ -0,0 +1,145 @@
1
+ import { applyDocumentActivationBatch, contentRevision } from "./files.js";
2
+ import { serializeWorkspaceMutation } from "./mutation.js";
3
+ import { modelSupports } from "../model/index.js";
4
+ import { assessAuditDocumentActivations } from "./audit-preparation.js";
5
+ import { assessProgramReadiness } from "./program-readiness.js";
6
+ import { personWasActiveOn } from "./soc2.js";
7
+ import { currentCalendarDate } from "./time.js";
8
+ import { loadWorkspace } from "./workspace.js";
9
+
10
+ export async function scaffoldDocumentActivation(input = process.cwd(), options = {}) {
11
+ const loaded = await loadWorkspace(input);
12
+ requireDocumentLifecycle(loaded);
13
+ const candidates = await activationCandidates(loaded, options);
14
+ const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
15
+ const today = currentCalendarDate(loaded.workspace.timezone);
16
+ return {
17
+ documentIds: candidates.map(({ documentId }) => documentId),
18
+ ...(options.auditId ? { auditId: options.auditId, workflowScope: "engagement" } : { workflowScope: "program" }),
19
+ activatedByIds: [],
20
+ activatedOn: today,
21
+ effectiveOn: today,
22
+ expectedRevisions: Object.fromEntries(candidates.map(({ documentId }) => [documentId, revisionById.get(documentId)])),
23
+ confirmed: false
24
+ };
25
+ }
26
+
27
+ export async function planDocumentActivation(input = process.cwd(), options = {}) {
28
+ const loaded = await loadWorkspace(input);
29
+ requireDocumentLifecycle(loaded);
30
+ const documentIds = [...new Set((options.documentIds || []).map(String))];
31
+ if (!documentIds.length) throw new Error("Document activation needs at least one approved governed Document.");
32
+ const activatedByIds = [...new Set((options.activatedByIds || []).map(String))];
33
+ if (!activatedByIds.length) throw new Error("Document activation needs the Person who performed the activation.");
34
+ const activatedOn = String(options.activatedOn || "").trim();
35
+ const effectiveOn = String(options.effectiveOn || "").trim();
36
+ if (!isCalendarDate(activatedOn)) throw new Error("Document activation needs the actual activation date in YYYY-MM-DD format.");
37
+ if (!isCalendarDate(effectiveOn)) throw new Error("Document activation needs a real effective date in YYYY-MM-DD format.");
38
+ const today = currentCalendarDate(loaded.workspace.timezone);
39
+ if (activatedOn !== today) {
40
+ throw new Error(`The activation date records today's lifecycle event and must be ${today}. Keep a future date in proposedEffectiveOn until activation occurs.`);
41
+ }
42
+ if (effectiveOn < activatedOn) {
43
+ throw new Error("The effective date cannot be before the separate activation date; do not backdate adoption.");
44
+ }
45
+ const expectedRevisions = options.expectedRevisions || {};
46
+ if (Array.isArray(expectedRevisions) || !expectedRevisions || typeof expectedRevisions !== "object") {
47
+ throw new Error("Document activation expected revisions must be keyed by Document ID.");
48
+ }
49
+ const workflowScope = options.auditId ? "engagement" : "program";
50
+ const candidates = await activationCandidates(loaded, options);
51
+ const assessmentById = new Map(candidates.map((item) => [item.documentId, item]));
52
+ const entryById = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
53
+ const invalidActivatorIds = activatedByIds.filter((id) => {
54
+ const person = entryById.get(id)?.record;
55
+ return !personWasActiveOn(person, activatedOn);
56
+ });
57
+ if (invalidActivatorIds.length) {
58
+ throw new Error(`Document activation needs active People as activators: ${invalidActivatorIds.join(", ")}.`);
59
+ }
60
+ const update = documentIds.map((documentId) => {
61
+ const entry = entryById.get(documentId);
62
+ if (!entry || entry.record.type !== "document") throw new Error(`Document "${documentId}" was not found.`);
63
+ if (entry.record.status !== "approved") {
64
+ throw new Error(`Document "${documentId}" must be independently approved before its separate ${workflowScope === "engagement" ? "Step 5" : "Step 3"} activation.`);
65
+ }
66
+ if (entry.record.workflowScope !== workflowScope) {
67
+ throw new Error(`Document "${documentId}" belongs to the ${entry.record.workflowScope} workflow, not ${workflowScope}.`);
68
+ }
69
+ const assessment = assessmentById.get(documentId);
70
+ if (assessment?.state !== "ready-to-activate") {
71
+ const missing = assessment?.missingImplementationControlIds || [];
72
+ throw new Error(
73
+ missing.length
74
+ ? `Document "${documentId}" cannot be activated until linked Controls are implemented: ${missing.join(", ")}.`
75
+ : `Document "${documentId}" is not ready for ${workflowScope === "engagement" ? "Step 5" : "Step 3"} activation.`
76
+ );
77
+ }
78
+ if (!/^[a-f0-9]{64}$/.test(expectedRevisions[documentId] || "")) {
79
+ throw new Error(`Document activation needs the current record revision for "${documentId}". Regenerate the activation review and try again.`);
80
+ }
81
+ const record = {
82
+ ...entry.record,
83
+ status: "active",
84
+ activationBasis: "recorded",
85
+ activatedByIds,
86
+ activatedOn,
87
+ effectiveOn,
88
+ activatedContentRevisions: structuredClone(entry.record.approvedContentRevisions)
89
+ };
90
+ delete record.proposedEffectiveOn;
91
+ return record;
92
+ });
93
+ return {
94
+ operation: "document-activation",
95
+ workflowScope,
96
+ ...(options.auditId ? { auditId: options.auditId } : {}),
97
+ documentIds,
98
+ activatedByIds,
99
+ activatedOn,
100
+ effectiveOn,
101
+ changes: {
102
+ update,
103
+ expectedRevisions: Object.fromEntries(documentIds.map((documentId) => [documentId, expectedRevisions[documentId]])),
104
+ validateWholeWorkspace: true
105
+ }
106
+ };
107
+ }
108
+
109
+ async function activationCandidates(loaded, options) {
110
+ if (options.auditId) {
111
+ return (await assessAuditDocumentActivations(loaded, {
112
+ auditId: options.auditId,
113
+ asOf: currentCalendarDate(loaded.workspace.timezone)
114
+ })).filter(({ state }) => state === "ready-to-activate");
115
+ }
116
+ const readiness = await assessProgramReadiness(loaded, { programId: options.programId });
117
+ return readiness.documentActivations.filter(({ state }) => state === "ready-to-activate");
118
+ }
119
+
120
+ export async function activateDocuments(input = process.cwd(), options = {}) {
121
+ if (options.confirmed !== true) throw new Error("Review the governed Document activation and confirm the write.");
122
+ return serializeWorkspaceMutation(input, async (root) => {
123
+ const plan = await planDocumentActivation(root, options);
124
+ const result = await applyDocumentActivationBatch(root, plan.changes);
125
+ return { ...plan, result };
126
+ });
127
+ }
128
+
129
+ function requireDocumentLifecycle(loaded) {
130
+ if (!modelSupports(loaded.model, "governed-document-activation")) {
131
+ throw new Error("Separate governed Document approval and activation requires a model v5 workspace.");
132
+ }
133
+ }
134
+
135
+ function isCalendarDate(value) {
136
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
137
+ if (!match) return false;
138
+ const [, year, month, day] = match.map(Number);
139
+ const date = new Date(0);
140
+ date.setUTCFullYear(year, month - 1, day);
141
+ date.setUTCHours(0, 0, 0, 0);
142
+ return date.getUTCFullYear() === year
143
+ && date.getUTCMonth() === month - 1
144
+ && date.getUTCDate() === day;
145
+ }