filegrc 0.9.2 → 0.11.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/model/index.js +8 -5
- package/model/v7.json +10359 -0
- package/model/v8.json +10947 -0
- package/package.json +1 -1
- package/src/applicability-scope.js +211 -0
- package/src/audit-preparation.js +156 -20
- package/src/batch-review.js +40 -24
- package/src/cli.js +85 -7
- package/src/collection-review.js +16 -3
- package/src/collection-revision.js +23 -3
- package/src/collection-scope.js +94 -7
- package/src/document-activation.js +13 -1
- package/src/git.js +4 -4
- package/src/index.js +3 -0
- package/src/model-migration.js +381 -35
- package/src/obligations.js +142 -39
- package/src/policy-activation.js +5 -0
- package/src/policy-library/data-retention-schedule-v2.md +25 -0
- package/src/policy-library.js +52 -24
- package/src/program-amendment.js +222 -0
- package/src/program-lifecycle.js +1 -1
- package/src/program-path.js +13 -8
- package/src/program-readiness.js +43 -13
- package/src/reconciliation.js +15 -3
- package/src/requirement-mapping.js +61 -0
- package/src/retention.js +261 -0
- package/src/server.js +76 -2
- package/src/setup.js +1 -1
- package/src/source-coverage.js +21 -7
- package/src/state.js +171 -2
- package/src/validate.js +106 -11
- package/src/web.js +441 -70
- package/src/workflow.js +33 -13
package/src/model-migration.js
CHANGED
|
@@ -3,7 +3,9 @@ import { applyModelMigrationBatch, applyResourceBatch, contentRevision } from ".
|
|
|
3
3
|
import { loadWorkspace } from "./workspace.js";
|
|
4
4
|
import { ACTIVE_MODEL_VERSION, loadModel } from "../model/index.js";
|
|
5
5
|
import { legacyCoverage } from "./coverage.js";
|
|
6
|
-
import {
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
7
9
|
import { resolveDataPath } from "./paths.js";
|
|
8
10
|
import { markdownEntries } from "./resource-markdown.js";
|
|
9
11
|
|
|
@@ -894,8 +896,10 @@ export async function planModelMigration(input = process.cwd(), options = {}) {
|
|
|
894
896
|
: sourceVersion === "1" ? V1_TARGET_MODEL_VERSION
|
|
895
897
|
: sourceVersion === "2" ? "3"
|
|
896
898
|
: sourceVersion === "3" ? "4"
|
|
897
|
-
|
|
898
|
-
|
|
899
|
+
: sourceVersion === "4" ? "5"
|
|
900
|
+
: sourceVersion === "5" ? "6"
|
|
901
|
+
: sourceVersion === "6" ? "7"
|
|
902
|
+
: ACTIVE_MODEL_VERSION;
|
|
899
903
|
if (sourceVersion === requestedTarget) return emptyPlan(sourceVersion, requestedTarget);
|
|
900
904
|
if (sourceVersion === "1" && requestedTarget === "2") {
|
|
901
905
|
return planV1ToV2Migration(input, options);
|
|
@@ -909,20 +913,27 @@ export async function planModelMigration(input = process.cwd(), options = {}) {
|
|
|
909
913
|
if (sourceVersion === "4" && requestedTarget === "5") {
|
|
910
914
|
return planV4ToV5Migration(loaded, options);
|
|
911
915
|
}
|
|
912
|
-
if (sourceVersion === "5" && requestedTarget ===
|
|
916
|
+
if (sourceVersion === "5" && requestedTarget === "6") {
|
|
913
917
|
return planV5ToV6Migration(loaded);
|
|
914
918
|
}
|
|
919
|
+
if (sourceVersion === "6" && requestedTarget === "7") {
|
|
920
|
+
return planV6ToV7Migration(loaded);
|
|
921
|
+
}
|
|
922
|
+
if (sourceVersion === "7" && requestedTarget === ACTIVE_MODEL_VERSION) {
|
|
923
|
+
return planV7ToV8Migration(loaded);
|
|
924
|
+
}
|
|
915
925
|
if (sourceVersion === "1" && requestedTarget === ACTIVE_MODEL_VERSION) {
|
|
916
926
|
throw new Error(
|
|
917
927
|
"Model v1 workspaces must migrate to model v2 first. "
|
|
918
|
-
+
|
|
928
|
+
+ `Preview and apply \`npx filegrc migrate --to-model 2\`, then migrate one version at a time through model v${ACTIVE_MODEL_VERSION}.`
|
|
919
929
|
);
|
|
920
930
|
}
|
|
921
931
|
throw new Error(`Model migration does not support v${sourceVersion} to v${requestedTarget}.`);
|
|
922
932
|
}
|
|
923
933
|
|
|
924
934
|
export async function migrateModel(input = process.cwd(), options = {}) {
|
|
925
|
-
const
|
|
935
|
+
const loaded = await loadWorkspace(input);
|
|
936
|
+
const plan = await planModelMigration(loaded.root, options);
|
|
926
937
|
if (plan.sourceModelVersion === plan.targetModelVersion) {
|
|
927
938
|
return { ...plan, applied: false };
|
|
928
939
|
}
|
|
@@ -932,18 +943,44 @@ export async function migrateModel(input = process.cwd(), options = {}) {
|
|
|
932
943
|
+ "and resolve every missing value, conflict, manual action, and unsupported change."
|
|
933
944
|
);
|
|
934
945
|
}
|
|
946
|
+
const migrationReportPath = await persistMigrationReport(loaded.root, plan);
|
|
935
947
|
if (plan.sourceModelVersion === "1" && plan.targetModelVersion === "2") {
|
|
936
|
-
return migrateV1ToV2(
|
|
948
|
+
return { ...await migrateV1ToV2(loaded.root, options), migrationReportPath };
|
|
937
949
|
}
|
|
938
|
-
const result = await applyModelMigrationBatch(
|
|
950
|
+
const result = await applyModelMigrationBatch(loaded.root, plan.changes);
|
|
939
951
|
return {
|
|
940
952
|
...plan,
|
|
941
953
|
applied: true,
|
|
954
|
+
migrationReportPath,
|
|
942
955
|
result,
|
|
943
|
-
postMigrationAssessment: await postMigrationAssessment(
|
|
956
|
+
postMigrationAssessment: await postMigrationAssessment(loaded.root)
|
|
944
957
|
};
|
|
945
958
|
}
|
|
946
959
|
|
|
960
|
+
async function persistMigrationReport(root, plan) {
|
|
961
|
+
const relativePath = `.filegrc/migrations/model-v${plan.sourceModelVersion}-to-v${plan.targetModelVersion}.json`;
|
|
962
|
+
const directory = join(root, ".filegrc", "migrations");
|
|
963
|
+
const path = join(root, relativePath);
|
|
964
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
965
|
+
await mkdir(directory, { recursive: true });
|
|
966
|
+
try {
|
|
967
|
+
await writeFile(temporaryPath, `${JSON.stringify({
|
|
968
|
+
schemaVersion: 1,
|
|
969
|
+
sourceModelVersion: plan.sourceModelVersion,
|
|
970
|
+
targetModelVersion: plan.targetModelVersion,
|
|
971
|
+
summary: plan.summary,
|
|
972
|
+
classifications: plan.classifications,
|
|
973
|
+
migrationReport: plan.migrationReport || null,
|
|
974
|
+
fileDiff: plan.fileDiff
|
|
975
|
+
}, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
976
|
+
await rename(temporaryPath, path);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
await rm(temporaryPath, { force: true }).catch(() => {});
|
|
979
|
+
throw error;
|
|
980
|
+
}
|
|
981
|
+
return relativePath;
|
|
982
|
+
}
|
|
983
|
+
|
|
947
984
|
async function planV2ToV3Migration(loaded) {
|
|
948
985
|
if (!loaded.workspace?.id) {
|
|
949
986
|
throw new Error("Model migration requires a valid Workspace record.");
|
|
@@ -1312,7 +1349,7 @@ async function planV3ToV4Migration(loaded, options = {}) {
|
|
|
1312
1349
|
? [{
|
|
1313
1350
|
systemId: targetIds[0],
|
|
1314
1351
|
roles: derivedComponentRoles(record, loaded.resources),
|
|
1315
|
-
rationale: `
|
|
1352
|
+
rationale: `Supports the bounded System "${byId.get(targetIds[0])?.title || targetIds[0]}".`
|
|
1316
1353
|
}]
|
|
1317
1354
|
: [];
|
|
1318
1355
|
if (!uses.length) {
|
|
@@ -1477,6 +1514,7 @@ async function planV3ToV4Migration(loaded, options = {}) {
|
|
|
1477
1514
|
componentIds: [...systemKinds].filter(([, kind]) => kind === "component").map(([id]) => id),
|
|
1478
1515
|
classificationIds: [...classifications.values()],
|
|
1479
1516
|
informationTypeIds: [...informationTypes.values()],
|
|
1517
|
+
unmappedLegacyFields: collectV4LegacyFields(loaded.resources, systemKinds),
|
|
1480
1518
|
relationshipUpdates: updates.filter((record) => record.type !== "workspace").map(({ id, type }) => ({ id, type }))
|
|
1481
1519
|
},
|
|
1482
1520
|
summary: {
|
|
@@ -1847,6 +1885,311 @@ async function planV5ToV6Migration(loaded) {
|
|
|
1847
1885
|
};
|
|
1848
1886
|
}
|
|
1849
1887
|
|
|
1888
|
+
async function planV6ToV7Migration(loaded) {
|
|
1889
|
+
if (!loaded.workspace?.id) throw new Error("Model migration requires a valid Workspace record.");
|
|
1890
|
+
const targetModel = loadModel("7");
|
|
1891
|
+
const revisions = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
1892
|
+
const automatic = [];
|
|
1893
|
+
const unsupported = [];
|
|
1894
|
+
const missing = [];
|
|
1895
|
+
const manualActions = [];
|
|
1896
|
+
const updates = [];
|
|
1897
|
+
const historicalActivationIds = [];
|
|
1898
|
+
const trainingReactivation = [];
|
|
1899
|
+
const preservedTrainingHistoryIds = [];
|
|
1900
|
+
const purifiedNarrativeIds = [];
|
|
1901
|
+
const removedMigrationExtensions = [];
|
|
1902
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
1903
|
+
|
|
1904
|
+
for (const original of loaded.resources) {
|
|
1905
|
+
const record = structuredClone(original);
|
|
1906
|
+
let changed = false;
|
|
1907
|
+
if (original.type === "workspace") {
|
|
1908
|
+
record.dataModelVersion = "7";
|
|
1909
|
+
changed = true;
|
|
1910
|
+
automatic.push(classifiedChange("automatic", original.id, "dataModelVersion", "Select model v7 and keep upgrade mechanics out of compliance entities."));
|
|
1911
|
+
}
|
|
1912
|
+
if (original.type === "document" && original.activationBasis === "legacy-v4") {
|
|
1913
|
+
record.activationBasis = "historical";
|
|
1914
|
+
changed = true;
|
|
1915
|
+
historicalActivationIds.push(original.id);
|
|
1916
|
+
automatic.push(classifiedChange(
|
|
1917
|
+
"automatic",
|
|
1918
|
+
original.id,
|
|
1919
|
+
"activationBasis",
|
|
1920
|
+
"Replace the model-version label with the equivalent historical activation basis."
|
|
1921
|
+
));
|
|
1922
|
+
}
|
|
1923
|
+
if (original.type === "training" && original.activationBasis === "legacy-v5") {
|
|
1924
|
+
delete record.activationBasis;
|
|
1925
|
+
delete record.activatedByIds;
|
|
1926
|
+
delete record.activatedOn;
|
|
1927
|
+
delete record.activatedContentRevisions;
|
|
1928
|
+
if (original.status === "active") {
|
|
1929
|
+
trainingReactivation.push({ resourceId: original.id, priorEffectiveOn: original.effectiveOn || null });
|
|
1930
|
+
record.status = "approved";
|
|
1931
|
+
delete record.effectiveOn;
|
|
1932
|
+
} else {
|
|
1933
|
+
preservedTrainingHistoryIds.push(original.id);
|
|
1934
|
+
}
|
|
1935
|
+
changed = true;
|
|
1936
|
+
automatic.push(classifiedChange(
|
|
1937
|
+
"automatic",
|
|
1938
|
+
original.id,
|
|
1939
|
+
original.status === "active" ? "status" : "activationBasis",
|
|
1940
|
+
original.status === "active"
|
|
1941
|
+
? "Keep the approved Training content and require management to record its next activation explicitly."
|
|
1942
|
+
: "Keep the closed Training lifecycle facts without a model-version activation label."
|
|
1943
|
+
));
|
|
1944
|
+
}
|
|
1945
|
+
if (
|
|
1946
|
+
original.type === "information-type"
|
|
1947
|
+
&& /^Information category migrated from the v3 dataTypes value ".+"\.$/.test(original.description || "")
|
|
1948
|
+
) {
|
|
1949
|
+
record.description = `Information handled by in-scope Systems or Components under the "${original.title}" category.`;
|
|
1950
|
+
changed = true;
|
|
1951
|
+
purifiedNarrativeIds.push(original.id);
|
|
1952
|
+
automatic.push(classifiedChange("automatic", original.id, "description", "Replace generated upgrade prose with the underlying information-handling fact."));
|
|
1953
|
+
}
|
|
1954
|
+
if (original.type === "component" && Array.isArray(original.systemUses)) {
|
|
1955
|
+
let systemUsesPurified = false;
|
|
1956
|
+
record.systemUses = original.systemUses.map((use) => {
|
|
1957
|
+
if (!/^Migrated from v3 System ".+" because it was recorded as part of or support for this bounded System\.$/.test(use.rationale || "")) return use;
|
|
1958
|
+
changed = true;
|
|
1959
|
+
systemUsesPurified = true;
|
|
1960
|
+
purifiedNarrativeIds.push(original.id);
|
|
1961
|
+
return { ...use, rationale: `Supports the bounded System "${byId.get(use.systemId)?.title || use.systemId}".` };
|
|
1962
|
+
});
|
|
1963
|
+
if (systemUsesPurified) automatic.push(classifiedChange("automatic", original.id, "systemUses", "Replace generated upgrade prose with the existing Component-to-System fact."));
|
|
1964
|
+
}
|
|
1965
|
+
if (original.type === "audit" && Array.isArray(original.subserviceTreatments)) {
|
|
1966
|
+
let treatmentsPurified = false;
|
|
1967
|
+
record.subserviceTreatments = original.subserviceTreatments.map((treatment) => {
|
|
1968
|
+
if (treatment.rationale !== "Migrated from the explicit v3 Audit subservice scope and method; management must confirm the treatment.") return treatment;
|
|
1969
|
+
changed = true;
|
|
1970
|
+
treatmentsPurified = true;
|
|
1971
|
+
purifiedNarrativeIds.push(original.id);
|
|
1972
|
+
return { ...treatment, rationale: `This Vendor is subject to the ${treatment.method} method for this engagement.` };
|
|
1973
|
+
});
|
|
1974
|
+
if (treatmentsPurified) automatic.push(classifiedChange("automatic", original.id, "subserviceTreatments", "Replace generated upgrade prose with the existing engagement treatment fact."));
|
|
1975
|
+
}
|
|
1976
|
+
if (record.extensions?.["filegrc.migration"] !== undefined) {
|
|
1977
|
+
removedMigrationExtensions.push({ resourceId: original.id, details: record.extensions["filegrc.migration"] });
|
|
1978
|
+
delete record.extensions["filegrc.migration"];
|
|
1979
|
+
if (!Object.keys(record.extensions).length) delete record.extensions;
|
|
1980
|
+
changed = true;
|
|
1981
|
+
automatic.push(classifiedChange("automatic", original.id, "extensions", "Move prior upgrade details out of the compliance record and into this migration report."));
|
|
1982
|
+
}
|
|
1983
|
+
if (changed) updates.push(record);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const updatedById = new Map(updates.map((record) => [record.id, record]));
|
|
1987
|
+
const migratedRecords = loaded.resources.map((record) => updatedById.get(record.id) || record);
|
|
1988
|
+
await collectTargetValidationActions(loaded, migratedRecords, targetModel, missing, manualActions);
|
|
1989
|
+
for (const item of [...missing, ...manualActions]) {
|
|
1990
|
+
unsupported.push(classifiedChange(
|
|
1991
|
+
"unsupported",
|
|
1992
|
+
item.resourceId,
|
|
1993
|
+
item.field,
|
|
1994
|
+
item.message || `Resolve ${item.field} before applying the model v7 migration.`
|
|
1995
|
+
));
|
|
1996
|
+
}
|
|
1997
|
+
const ready = unsupported.length === 0;
|
|
1998
|
+
return {
|
|
1999
|
+
schemaVersion: 2,
|
|
2000
|
+
sourceModelVersion: "6",
|
|
2001
|
+
targetModelVersion: "7",
|
|
2002
|
+
ready,
|
|
2003
|
+
missing,
|
|
2004
|
+
conflicts: [],
|
|
2005
|
+
manualActions,
|
|
2006
|
+
classifications: { automatic, reviewRequired: [], unsupported },
|
|
2007
|
+
notes: [],
|
|
2008
|
+
migrationReport: {
|
|
2009
|
+
historicalActivationIds,
|
|
2010
|
+
trainingReactivation,
|
|
2011
|
+
preservedTrainingHistoryIds,
|
|
2012
|
+
purifiedNarrativeIds: [...new Set(purifiedNarrativeIds)],
|
|
2013
|
+
removedMigrationExtensions
|
|
2014
|
+
},
|
|
2015
|
+
summary: {
|
|
2016
|
+
create: 0,
|
|
2017
|
+
update: updates.length,
|
|
2018
|
+
automatic: automatic.length,
|
|
2019
|
+
reviewRequired: 0,
|
|
2020
|
+
unsupported: unsupported.length
|
|
2021
|
+
},
|
|
2022
|
+
fileDiff: {
|
|
2023
|
+
create: [],
|
|
2024
|
+
update: updates.map((record) => ({
|
|
2025
|
+
type: record.type,
|
|
2026
|
+
id: record.id,
|
|
2027
|
+
before: loaded.resources.find(({ id }) => id === record.id),
|
|
2028
|
+
after: record
|
|
2029
|
+
}))
|
|
2030
|
+
},
|
|
2031
|
+
changes: {
|
|
2032
|
+
create: [],
|
|
2033
|
+
update: updates,
|
|
2034
|
+
expectedRevisions: Object.fromEntries(updates.map(({ id }) => [id, revisions.get(id)])),
|
|
2035
|
+
validateWholeWorkspace: true,
|
|
2036
|
+
targetModelVersion: "7"
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
async function planV7ToV8Migration(loaded) {
|
|
2042
|
+
if (!loaded.workspace?.id) throw new Error("Model migration requires a valid Workspace record.");
|
|
2043
|
+
const targetModel = loadModel("8");
|
|
2044
|
+
const revisions = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
2045
|
+
const automatic = [];
|
|
2046
|
+
const reviewRequired = [];
|
|
2047
|
+
const unsupported = [];
|
|
2048
|
+
const missing = [];
|
|
2049
|
+
const manualActions = [];
|
|
2050
|
+
const updates = [];
|
|
2051
|
+
|
|
2052
|
+
for (const original of loaded.resources) {
|
|
2053
|
+
const record = structuredClone(original);
|
|
2054
|
+
let changed = false;
|
|
2055
|
+
if (original.type === "workspace") {
|
|
2056
|
+
record.dataModelVersion = "8";
|
|
2057
|
+
changed = true;
|
|
2058
|
+
automatic.push(classifiedChange("automatic", original.id, "dataModelVersion", "Select model v8."));
|
|
2059
|
+
}
|
|
2060
|
+
if (original.type === "component" && Array.isArray(original.informationUses)) {
|
|
2061
|
+
record.informationUses = original.informationUses.map((use) => {
|
|
2062
|
+
if (!Array.isArray(use.activities)) return use;
|
|
2063
|
+
const migrated = { ...use, processingOperations: use.activities };
|
|
2064
|
+
delete migrated.activities;
|
|
2065
|
+
return migrated;
|
|
2066
|
+
});
|
|
2067
|
+
changed = true;
|
|
2068
|
+
automatic.push(classifiedChange(
|
|
2069
|
+
"automatic",
|
|
2070
|
+
original.id,
|
|
2071
|
+
"informationUses",
|
|
2072
|
+
"Rename information-use activities to the standard processingOperations term without changing the recorded values."
|
|
2073
|
+
));
|
|
2074
|
+
}
|
|
2075
|
+
if (original.type === "commitment" && Array.isArray(original.sourceDocumentIds)) {
|
|
2076
|
+
record.sourceResourceIds = [...new Set([
|
|
2077
|
+
...(record.sourceResourceIds || []),
|
|
2078
|
+
...original.sourceDocumentIds
|
|
2079
|
+
])];
|
|
2080
|
+
delete record.sourceDocumentIds;
|
|
2081
|
+
changed = true;
|
|
2082
|
+
automatic.push(classifiedChange(
|
|
2083
|
+
"automatic",
|
|
2084
|
+
original.id,
|
|
2085
|
+
"sourceResourceIds",
|
|
2086
|
+
"Preserve source Document links in the broader source-resource relationship."
|
|
2087
|
+
));
|
|
2088
|
+
}
|
|
2089
|
+
if (original.type === "source-coverage" && typeof original.retention === "string") {
|
|
2090
|
+
record.retentionNotes = original.retention;
|
|
2091
|
+
delete record.retention;
|
|
2092
|
+
changed = true;
|
|
2093
|
+
automatic.push(classifiedChange(
|
|
2094
|
+
"automatic",
|
|
2095
|
+
original.id,
|
|
2096
|
+
"retentionNotes",
|
|
2097
|
+
"Preserve the prior narrative retention statement as notes without treating it as an approved schedule rule."
|
|
2098
|
+
));
|
|
2099
|
+
}
|
|
2100
|
+
if (original.type === "audit" && typeof original.retentionDecision === "string") {
|
|
2101
|
+
record.retentionNotes = original.retentionDecision;
|
|
2102
|
+
delete record.retentionDecision;
|
|
2103
|
+
changed = true;
|
|
2104
|
+
automatic.push(classifiedChange(
|
|
2105
|
+
"automatic",
|
|
2106
|
+
original.id,
|
|
2107
|
+
"retentionNotes",
|
|
2108
|
+
"Preserve the prior audit retention decision as notes without inferring a structured retention rule."
|
|
2109
|
+
));
|
|
2110
|
+
}
|
|
2111
|
+
if (changed) updates.push(record);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
for (const record of loaded.resources) {
|
|
2115
|
+
if (record.type === "source-coverage" && record.status === "active") {
|
|
2116
|
+
reviewRequired.push(classifiedChange(
|
|
2117
|
+
"review-required",
|
|
2118
|
+
record.id,
|
|
2119
|
+
"retentionScheduleItemIds",
|
|
2120
|
+
"Management must select or create an approved retention schedule item for this source coverage."
|
|
2121
|
+
));
|
|
2122
|
+
}
|
|
2123
|
+
if (record.type === "component" && record.informationUses?.length) {
|
|
2124
|
+
reviewRequired.push(classifiedChange(
|
|
2125
|
+
"review-required",
|
|
2126
|
+
record.id,
|
|
2127
|
+
"informationUses",
|
|
2128
|
+
"Review every Information Type use against the structured retention schedule."
|
|
2129
|
+
));
|
|
2130
|
+
}
|
|
2131
|
+
if (["system", "vendor"].includes(record.type) && record.informationTypeIds?.length) {
|
|
2132
|
+
reviewRequired.push(classifiedChange(
|
|
2133
|
+
"review-required",
|
|
2134
|
+
record.id,
|
|
2135
|
+
"informationTypeIds",
|
|
2136
|
+
`Review every Information Type used by this ${record.type === "system" ? "System" : "Vendor"} against the structured retention schedule.`
|
|
2137
|
+
));
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
const updatedById = new Map(updates.map((record) => [record.id, record]));
|
|
2142
|
+
const migratedRecords = loaded.resources.map((record) => updatedById.get(record.id) || record);
|
|
2143
|
+
await collectTargetValidationActions(loaded, migratedRecords, targetModel, missing, manualActions);
|
|
2144
|
+
for (const item of [...missing, ...manualActions]) {
|
|
2145
|
+
unsupported.push(classifiedChange(
|
|
2146
|
+
"unsupported",
|
|
2147
|
+
item.resourceId,
|
|
2148
|
+
item.field,
|
|
2149
|
+
item.message || `Resolve ${item.field} before applying the model v8 migration.`
|
|
2150
|
+
));
|
|
2151
|
+
}
|
|
2152
|
+
const ready = unsupported.length === 0;
|
|
2153
|
+
return {
|
|
2154
|
+
schemaVersion: 2,
|
|
2155
|
+
sourceModelVersion: "7",
|
|
2156
|
+
targetModelVersion: "8",
|
|
2157
|
+
ready,
|
|
2158
|
+
missing,
|
|
2159
|
+
conflicts: [],
|
|
2160
|
+
manualActions,
|
|
2161
|
+
classifications: { automatic, reviewRequired, unsupported },
|
|
2162
|
+
notes: [
|
|
2163
|
+
"The migration never chooses a retention period, cutoff, or disposition action.",
|
|
2164
|
+
"Review-required items remain visible after migration through program readiness."
|
|
2165
|
+
],
|
|
2166
|
+
migrationReport: {},
|
|
2167
|
+
summary: {
|
|
2168
|
+
create: 0,
|
|
2169
|
+
update: updates.length,
|
|
2170
|
+
automatic: automatic.length,
|
|
2171
|
+
reviewRequired: reviewRequired.length,
|
|
2172
|
+
unsupported: unsupported.length
|
|
2173
|
+
},
|
|
2174
|
+
fileDiff: {
|
|
2175
|
+
create: [],
|
|
2176
|
+
update: updates.map((record) => ({
|
|
2177
|
+
type: record.type,
|
|
2178
|
+
id: record.id,
|
|
2179
|
+
before: loaded.resources.find(({ id }) => id === record.id),
|
|
2180
|
+
after: record
|
|
2181
|
+
}))
|
|
2182
|
+
},
|
|
2183
|
+
changes: {
|
|
2184
|
+
create: [],
|
|
2185
|
+
update: updates,
|
|
2186
|
+
expectedRevisions: Object.fromEntries(updates.map(({ id }) => [id, revisions.get(id)])),
|
|
2187
|
+
validateWholeWorkspace: true,
|
|
2188
|
+
targetModelVersion: "8"
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
|
|
1850
2193
|
function normalizeDocumentScopeDecision(value) {
|
|
1851
2194
|
const candidate = typeof value === "object" && value
|
|
1852
2195
|
? value.workflowScope || value.scope
|
|
@@ -2085,7 +2428,7 @@ function migrateV4InformationTypes(loaded, creates, automatic, reviewRequired, u
|
|
|
2085
2428
|
type: "information-type",
|
|
2086
2429
|
title: value.title,
|
|
2087
2430
|
status: classificationIds.length === 1 ? "active" : "planned",
|
|
2088
|
-
description: `Information
|
|
2431
|
+
description: `Information handled by in-scope Systems or Components under the "${value.title}" category.`,
|
|
2089
2432
|
...(classificationIds.length === 1 ? { classificationId: classificationIds[0] } : {})
|
|
2090
2433
|
});
|
|
2091
2434
|
result.set(key, id);
|
|
@@ -2113,13 +2456,7 @@ function migrateBoundedSystem(record, informationTypes, classifications) {
|
|
|
2113
2456
|
continuityObjectives: record.continuityObjectives,
|
|
2114
2457
|
statusTransition: record.statusTransition,
|
|
2115
2458
|
tags: record.tags,
|
|
2116
|
-
extensions:
|
|
2117
|
-
...(record.environment ? { environment: record.environment } : {}),
|
|
2118
|
-
...(record.vendorId ? { vendorId: record.vendorId } : {}),
|
|
2119
|
-
...(record.subserviceVendorIds ? { subserviceVendorIds: record.subserviceVendorIds } : {}),
|
|
2120
|
-
...(record.evidenceSourceKinds ? { evidenceSourceKinds: record.evidenceSourceKinds } : {}),
|
|
2121
|
-
...(record.evidenceOwnerIds ? { evidenceOwnerIds: record.evidenceOwnerIds } : {})
|
|
2122
|
-
}),
|
|
2459
|
+
extensions: complianceExtensions(record.extensions),
|
|
2123
2460
|
externalIds: record.externalIds
|
|
2124
2461
|
});
|
|
2125
2462
|
}
|
|
@@ -2149,9 +2486,7 @@ function migrateComponent(record, systemUses, informationTypes, classifications)
|
|
|
2149
2486
|
continuityObjectives: record.continuityObjectives,
|
|
2150
2487
|
statusTransition: record.statusTransition,
|
|
2151
2488
|
tags: record.tags,
|
|
2152
|
-
extensions:
|
|
2153
|
-
...(record.subserviceVendorIds ? { subserviceVendorIds: record.subserviceVendorIds } : {})
|
|
2154
|
-
}),
|
|
2489
|
+
extensions: complianceExtensions(record.extensions),
|
|
2155
2490
|
externalIds: record.externalIds
|
|
2156
2491
|
});
|
|
2157
2492
|
}
|
|
@@ -2165,10 +2500,8 @@ function componentKind(value, vendorId) {
|
|
|
2165
2500
|
|
|
2166
2501
|
function migrateV4Vendor(record, informationTypes, classifications, reviewRequired) {
|
|
2167
2502
|
const migrated = { ...record };
|
|
2168
|
-
const legacy = {};
|
|
2169
2503
|
for (const field of ["service", "subprocessor", "backupVendorId"]) {
|
|
2170
2504
|
if (!Object.hasOwn(migrated, field)) continue;
|
|
2171
|
-
legacy[field] = migrated[field];
|
|
2172
2505
|
delete migrated[field];
|
|
2173
2506
|
reviewRequired.push(classifiedChange(
|
|
2174
2507
|
"review-required",
|
|
@@ -2184,7 +2517,7 @@ function migrateV4Vendor(record, informationTypes, classifications, reviewRequir
|
|
|
2184
2517
|
migrated.informationTypeIds = normalizedInformationTypeIds(record.dataTypes, informationTypes);
|
|
2185
2518
|
delete migrated.dataTypes;
|
|
2186
2519
|
if (record.classificationId) migrated.classificationId = classifications.get(record.classificationId);
|
|
2187
|
-
migrated.extensions =
|
|
2520
|
+
migrated.extensions = complianceExtensions(record.extensions);
|
|
2188
2521
|
if (!Object.keys(migrated.extensions || {}).length) delete migrated.extensions;
|
|
2189
2522
|
return migrated;
|
|
2190
2523
|
}
|
|
@@ -2291,7 +2624,7 @@ function rewriteV4SystemRelationships(record, context) {
|
|
|
2291
2624
|
vendorId,
|
|
2292
2625
|
componentIds,
|
|
2293
2626
|
method,
|
|
2294
|
-
rationale:
|
|
2627
|
+
rationale: `This Vendor is subject to the ${method} method for this engagement.`
|
|
2295
2628
|
});
|
|
2296
2629
|
context.reviewRequired.push(classifiedChange("review-required", record.id, "subserviceTreatments", "Confirm each migrated audit-time subservice treatment and rationale."));
|
|
2297
2630
|
} else requireComponentDecision(record, "subserviceVendorIds", context, `Choose the Components and audit-time treatment for Vendor "${vendorId}".`);
|
|
@@ -2312,16 +2645,29 @@ function normalizedInformationTypeIds(values, informationTypes) {
|
|
|
2312
2645
|
return [...new Set((values || []).map((value) => informationTypes.get(String(value).trim().toLowerCase())).filter(Boolean))];
|
|
2313
2646
|
}
|
|
2314
2647
|
|
|
2315
|
-
function
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2648
|
+
function complianceExtensions(existing) {
|
|
2649
|
+
if (!existing) return undefined;
|
|
2650
|
+
const result = { ...existing };
|
|
2651
|
+
delete result["filegrc.migration"];
|
|
2652
|
+
return Object.keys(result).length ? result : undefined;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
function collectV4LegacyFields(resources, systemKinds) {
|
|
2656
|
+
return resources.flatMap((record) => {
|
|
2657
|
+
const fieldNames = record.type === "system"
|
|
2658
|
+
? systemKinds.get(record.id) === "system"
|
|
2659
|
+
? ["environment", "vendorId", "subserviceVendorIds", "evidenceSourceKinds", "evidenceOwnerIds"]
|
|
2660
|
+
: ["subserviceVendorIds"]
|
|
2661
|
+
: record.type === "vendor"
|
|
2662
|
+
? ["service", "subprocessor", "backupVendorId"]
|
|
2663
|
+
: [];
|
|
2664
|
+
const fields = Object.fromEntries(fieldNames
|
|
2665
|
+
.filter((field) => Object.hasOwn(record, field))
|
|
2666
|
+
.map((field) => [field, record[field]]));
|
|
2667
|
+
const priorMigrationDetails = record.extensions?.["filegrc.migration"];
|
|
2668
|
+
if (priorMigrationDetails !== undefined) fields.priorMigrationDetails = priorMigrationDetails;
|
|
2669
|
+
return Object.keys(fields).length ? [{ resourceId: record.id, resourceType: record.type, fields }] : [];
|
|
2670
|
+
});
|
|
2325
2671
|
}
|
|
2326
2672
|
|
|
2327
2673
|
function cleanUndefined(value) {
|