filegrc 0.16.11 → 0.16.12
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/package.json +1 -1
- package/src/applicability-scope.js +13 -4
- package/src/collection-revision.js +8 -5
- package/src/historical-workspace.js +103 -5
- package/src/retention.js +23 -5
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { CALCULATED_REVISION_FIELDS, CALCULATED_REVISION_MAP_FIELDS, calculateRevision, canonicalCalculatedRevision, revisionsMatch } from "./revisions.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { reviewCommitsChangingTypes, reviewHistoricalWorkspace, reviewHistoryCommits, reviewHistoryContext } from "./historical-workspace.js";
|
|
3
|
+
|
|
4
|
+
const scopeSourceTypes = new Set([
|
|
5
|
+
"workspace", "program", "commitment", "control", "complementary-control",
|
|
6
|
+
"system", "framework", "component", "vendor", "policy", "requirement"
|
|
7
|
+
]);
|
|
4
8
|
|
|
5
9
|
const excludedResourceFields = new Set([
|
|
6
10
|
"applicabilityReview",
|
|
@@ -243,8 +247,13 @@ export function applicabilityReviewIsCurrent(review, record, program, resources,
|
|
|
243
247
|
|
|
244
248
|
function historicalScopedReviewMatches(stored, record, program, resources, model, root) {
|
|
245
249
|
const current = applicabilityScopeRevision(record, program, resources, model);
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
const context = reviewHistoryContext(root, resources);
|
|
251
|
+
const { index } = context;
|
|
252
|
+
const commits = index?.available
|
|
253
|
+
? reviewCommitsChangingTypes(index, scopeSourceTypes)
|
|
254
|
+
: reviewHistoryCommits(context);
|
|
255
|
+
for (const commit of [...commits].reverse()) {
|
|
256
|
+
const snapshot = reviewHistoricalWorkspace(context, commit);
|
|
248
257
|
if (!snapshot) continue;
|
|
249
258
|
const historicalResources = snapshot.resources;
|
|
250
259
|
const historicalRecord = historicalResources.find(({ id }) => id === record.id);
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
collectionScopeRevisionFacts
|
|
8
8
|
} from "./collection-scope.js";
|
|
9
9
|
import { resolveDataPath } from "./paths.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { getFileAtRevision } from "./git.js";
|
|
11
|
+
import { reviewHistoricalWorkspace, reviewHistoryCommits, reviewHistoryContext } from "./historical-workspace.js";
|
|
12
12
|
import { resolveProgram } from "./program.js";
|
|
13
13
|
import { markdownEntries } from "./resource-markdown.js";
|
|
14
14
|
import { CALCULATED_REVISION_FIELDS, CALCULATED_REVISION_MAP_FIELDS, calculateRevision, canonicalCalculatedRevision, revisionsMatch } from "./revisions.js";
|
|
@@ -46,8 +46,9 @@ export function collectionRevisionMatches(loaded, resourceType, storedRevision,
|
|
|
46
46
|
|
|
47
47
|
function historicallyEquivalentCollection(loaded, resourceType, stored, options, current) {
|
|
48
48
|
if (!loaded.root) return false;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const context = reviewHistoryContext(loaded.root, loaded);
|
|
50
|
+
for (const commit of [...reviewHistoryCommits(context)].reverse()) {
|
|
51
|
+
const snapshot = reviewHistoricalWorkspace(context, commit);
|
|
51
52
|
if (!snapshot) continue;
|
|
52
53
|
const historicalOptions = {
|
|
53
54
|
...options,
|
|
@@ -103,7 +104,9 @@ function calculateCollectionRevision(loaded, resourceType, options, legacy, scop
|
|
|
103
104
|
contentRevisions: (legacy || includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
|
|
104
105
|
try {
|
|
105
106
|
const content = options.historicalCommit
|
|
106
|
-
?
|
|
107
|
+
? loaded.historicalFiles
|
|
108
|
+
? loaded.historicalFiles.get(`data/${path}`) ?? null
|
|
109
|
+
: getFileAtRevision(loaded.root, options.historicalCommit, `data/${path}`)
|
|
107
110
|
: readFileSync(resolveDataPath(loaded.root, path), "utf8");
|
|
108
111
|
if (content === null) return [];
|
|
109
112
|
return [{ path, revision: createHash("sha256").update(content).digest("hex") }];
|
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
import { loadModel } from "../model/index.js";
|
|
2
|
-
import { getDataFilesAtRevision, getFileAtRevision } from "./git.js";
|
|
2
|
+
import { getDataCommitHistory, getDataFilesAtRevision, getDataRecordHistoryIndex, getFileAtRevision } from "./git.js";
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const reviewContexts = new WeakMap();
|
|
6
|
+
|
|
7
|
+
export function reviewHistoryContext(root, owner) {
|
|
8
|
+
const key = owner.resources || owner;
|
|
9
|
+
let context = reviewContexts.get(key);
|
|
10
|
+
if (context) return context;
|
|
11
|
+
let index = null;
|
|
12
|
+
try {
|
|
13
|
+
// Leave time for the original Git recovery path if this larger index
|
|
14
|
+
// cannot be built within a browser request.
|
|
15
|
+
index = getDataRecordHistoryIndex(root, { deadline: performance.now() + 3_000 });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code !== "FILEGRC_HISTORY_DEADLINE") throw error;
|
|
18
|
+
}
|
|
19
|
+
context = { root, index, statesByCommit: new Map(), workspacesByCommit: new Map() };
|
|
20
|
+
reviewContexts.set(key, context);
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function reviewHistoryCommits(context) {
|
|
25
|
+
if (context.index?.available) return context.index.commits;
|
|
26
|
+
context.fallbackCommits ||= getDataCommitHistory(context.root);
|
|
27
|
+
return context.fallbackCommits;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function reviewCommitsChangingTypes(index, types) {
|
|
31
|
+
const paths = new Set();
|
|
32
|
+
for (const records of index.recordsByCommit.values()) {
|
|
33
|
+
for (const { record, path } of records.values()) {
|
|
34
|
+
if (types.has(record.type)) paths.add(path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return index.commits.filter((commit) => [...(index.fileChangesByCommit.get(commit)?.keys() || [])]
|
|
38
|
+
.some((path) => paths.has(path)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function reviewHistoricalWorkspace(context, commit) {
|
|
42
|
+
if (context.index?.available) return indexedHistoricalWorkspace(context, commit);
|
|
43
|
+
if (context.workspacesByCommit.has(commit)) return context.workspacesByCommit.get(commit);
|
|
44
|
+
const loaded = historicalWorkspace(context.root, commit);
|
|
45
|
+
context.workspacesByCommit.set(commit, loaded);
|
|
46
|
+
if (context.workspacesByCommit.size > 128) context.workspacesByCommit.delete(context.workspacesByCommit.keys().next().value);
|
|
47
|
+
return loaded;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Preserve recovery beyond the reconciliation index's size and build limits.
|
|
51
|
+
// It is slower, but still requires the same substantive comparison.
|
|
52
|
+
function historicalWorkspace(root, commit) {
|
|
8
53
|
const entries = [];
|
|
9
54
|
for (const path of getDataFilesAtRevision(root, commit).filter(authoritativeJsonPath)) {
|
|
10
55
|
const source = getFileAtRevision(root, commit, path);
|
|
@@ -25,6 +70,59 @@ export function historicalWorkspace(root, commit) {
|
|
|
25
70
|
}
|
|
26
71
|
}
|
|
27
72
|
|
|
73
|
+
// The history index already contains every committed JSON and Markdown source.
|
|
74
|
+
// Materialize its first-parent states once instead of asking Git to list and
|
|
75
|
+
// read the entire workspace for every legacy review binding.
|
|
76
|
+
export function indexedHistoricalWorkspace(context, commit) {
|
|
77
|
+
const { root, index } = context;
|
|
78
|
+
if (!index?.available || !index.parentsByCommit.has(commit)) return null;
|
|
79
|
+
// Reuse a full request-sized history for ordinary workspaces while bounding
|
|
80
|
+
// retained maps when a repository contains many thousands of data files.
|
|
81
|
+
const cacheLimit = Math.max(1, Math.min(512, Math.floor(200_000 / Math.max(1, index.historicalRecordPaths.size))));
|
|
82
|
+
if (context.workspacesByCommit.has(commit)) return context.workspacesByCommit.get(commit);
|
|
83
|
+
let state = context.statesByCommit.get(commit);
|
|
84
|
+
if (!state) {
|
|
85
|
+
const lineage = [];
|
|
86
|
+
let cursor = commit;
|
|
87
|
+
while (cursor && !context.statesByCommit.has(cursor)) {
|
|
88
|
+
lineage.push(cursor);
|
|
89
|
+
cursor = index.parentsByCommit.get(cursor)?.[0] || null;
|
|
90
|
+
}
|
|
91
|
+
state = cursor ? context.statesByCommit.get(cursor) : new Map();
|
|
92
|
+
for (let position = lineage.length - 1; position >= 0; position -= 1) {
|
|
93
|
+
const changedAt = lineage[position];
|
|
94
|
+
const next = new Map(state);
|
|
95
|
+
for (const [path, source] of index.fileChangesByCommit.get(changedAt) || []) {
|
|
96
|
+
if (source === null) next.delete(path);
|
|
97
|
+
else next.set(path, source);
|
|
98
|
+
}
|
|
99
|
+
context.statesByCommit.set(changedAt, next);
|
|
100
|
+
if (context.statesByCommit.size > cacheLimit) context.statesByCommit.delete(context.statesByCommit.keys().next().value);
|
|
101
|
+
state = next;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const entries = [];
|
|
105
|
+
for (const [path, source] of state) {
|
|
106
|
+
if (!authoritativeJsonPath(path)) continue;
|
|
107
|
+
try {
|
|
108
|
+
entries.push({ record: JSON.parse(source), source, relativePath: path.slice("data/".length) });
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const resources = entries.map(({ record }) => record);
|
|
114
|
+
const workspace = resources.find(({ type }) => type === "workspace");
|
|
115
|
+
if (!workspace) return null;
|
|
116
|
+
try {
|
|
117
|
+
const loaded = { root, entries, resources, workspace, model: loadModel(workspace.dataModelVersion), historicalFiles: state };
|
|
118
|
+
context.workspacesByCommit.set(commit, loaded);
|
|
119
|
+
if (context.workspacesByCommit.size > cacheLimit) context.workspacesByCommit.delete(context.workspacesByCommit.keys().next().value);
|
|
120
|
+
return loaded;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
28
126
|
function authoritativeJsonPath(path) {
|
|
29
127
|
const parts = path.split("/");
|
|
30
128
|
if (parts[0] !== "data" || !path.endsWith(".json")) return false;
|
package/src/retention.js
CHANGED
|
@@ -6,8 +6,8 @@ import { programComponents } from "./program.js";
|
|
|
6
6
|
import { markdownEntries } from "./resource-markdown.js";
|
|
7
7
|
import { modelSupports } from "../model/index.js";
|
|
8
8
|
import { canonicalCalculatedRevisionJson, revisionDigest, revisionsMatch } from "./revisions.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { getFileAtRevision } from "./git.js";
|
|
10
|
+
import { reviewHistoricalWorkspace, reviewHistoryCommits, reviewHistoryContext } from "./historical-workspace.js";
|
|
11
11
|
|
|
12
12
|
export async function assessRetentionReadiness(loaded, program, options = {}) {
|
|
13
13
|
if (!loaded.model.resources["retention-schedule-item"]) return [];
|
|
@@ -203,7 +203,9 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
203
203
|
for (const markdown of markdownEntries(loaded.model, entry.record)) {
|
|
204
204
|
try {
|
|
205
205
|
const source = historicalCommit
|
|
206
|
-
?
|
|
206
|
+
? loaded.historicalFiles
|
|
207
|
+
? loaded.historicalFiles.get(`data/${markdown.path}`) ?? null
|
|
208
|
+
: getFileAtRevision(loaded.root, historicalCommit, `data/${markdown.path}`)
|
|
207
209
|
: readFileSync(resolveDataPath(loaded.root, markdown.path), "utf8");
|
|
208
210
|
if (source !== null) parts.push(source);
|
|
209
211
|
} catch (error) {
|
|
@@ -241,8 +243,24 @@ export function resourceReviewRevisionMatches(loaded, revisions, id, stored) {
|
|
|
241
243
|
// records and Markdown, before the old approval is treated as current.
|
|
242
244
|
function historicallyEquivalentReviewSource(loaded, id, stored, current) {
|
|
243
245
|
if (!loaded.root) return false;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
const context = reviewHistoryContext(loaded.root, loaded);
|
|
247
|
+
const { index } = context;
|
|
248
|
+
const relevantPaths = index?.available ? new Set(["data/workspace.json"]) : null;
|
|
249
|
+
const graphIds = new Set();
|
|
250
|
+
const visit = (resourceId) => {
|
|
251
|
+
if (graphIds.has(resourceId)) return;
|
|
252
|
+
graphIds.add(resourceId);
|
|
253
|
+
const entry = loaded.entries.find(({ record }) => record.id === resourceId);
|
|
254
|
+
if (!entry) return;
|
|
255
|
+
for (const path of (index.historiesById.get(resourceId) || []).map(({ path }) => path)) relevantPaths.add(path);
|
|
256
|
+
for (const markdown of markdownEntries(loaded.model, entry.record)) relevantPaths.add(`data/${markdown.path}`);
|
|
257
|
+
for (const sourceId of entry.record.sourceResourceIds || []) visit(sourceId);
|
|
258
|
+
};
|
|
259
|
+
if (relevantPaths) visit(id);
|
|
260
|
+
for (const commit of [...reviewHistoryCommits(context)].reverse()) {
|
|
261
|
+
const changed = index?.available ? index.fileChangesByCommit.get(commit) : null;
|
|
262
|
+
if (relevantPaths && ![...changed.keys()].some((path) => relevantPaths.has(path) || path.endsWith(".md"))) continue;
|
|
263
|
+
const snapshot = reviewHistoricalWorkspace(context, commit);
|
|
246
264
|
if (!snapshot?.entries.some(({ record }) => record.id === id)) continue;
|
|
247
265
|
const semantic = resourceReviewRevisionsSync(snapshot, [id], "legacy", false, commit).get(id);
|
|
248
266
|
if (!revisionsMatch("content", semantic, current)) continue;
|