filegrc 0.12.3 → 0.12.4
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/git.js +284 -46
- package/src/reconciliation.js +9 -4
- package/src/server.js +356 -52
- package/src/state.js +18 -10
- package/src/validate.js +73 -8
- package/src/web.js +75 -9
- package/src/workflow-history-integrity.js +19 -9
- package/src/workflow.js +7 -2
- package/src/workspace.js +3 -0
package/src/validate.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
2
3
|
import { readFile, stat } from "node:fs/promises";
|
|
3
4
|
import { performance } from "node:perf_hooks";
|
|
4
5
|
import { getResourceDefinition, modelSupports } from "../model/index.js";
|
|
@@ -36,6 +37,17 @@ import {
|
|
|
36
37
|
} from "./reporting-route-integrity.js";
|
|
37
38
|
import { validateWorkflowHistoryIntegrity } from "./workflow-history-integrity.js";
|
|
38
39
|
|
|
40
|
+
let fingerprintFileReadObserver = null;
|
|
41
|
+
|
|
42
|
+
export function setFingerprintFileReadObserverForTests(observer) {
|
|
43
|
+
if (observer !== null && typeof observer !== "function") {
|
|
44
|
+
throw new TypeError("The fingerprint file-read observer must be a function or null.");
|
|
45
|
+
}
|
|
46
|
+
const previous = fingerprintFileReadObserver;
|
|
47
|
+
fingerprintFileReadObserver = observer;
|
|
48
|
+
return () => { fingerprintFileReadObserver = previous; };
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
40
52
|
const NAMESPACE_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
|
|
41
53
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -229,6 +241,7 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
229
241
|
));
|
|
230
242
|
}
|
|
231
243
|
} catch (cause) {
|
|
244
|
+
if (cause?.code === "FILEGRC_GIT_DEADLINE") throw cause;
|
|
232
245
|
diagnostics.push(error(
|
|
233
246
|
"reconciliation-history-unavailable",
|
|
234
247
|
"data/workspace.json",
|
|
@@ -1224,19 +1237,27 @@ function validateCollectionReviewSet(resources, pathById, diagnostics) {
|
|
|
1224
1237
|
}
|
|
1225
1238
|
}
|
|
1226
1239
|
|
|
1227
|
-
export async function fingerprintWorkspace(input = process.cwd()) {
|
|
1240
|
+
export async function fingerprintWorkspace(input = process.cwd(), options = {}) {
|
|
1228
1241
|
const loaded = typeof input === "object" && input.entries ? input : await loadWorkspace(input);
|
|
1229
1242
|
const hash = createHash("sha256");
|
|
1230
1243
|
hash.update(`model\0${loaded.model.modelVersion}\0`);
|
|
1231
|
-
|
|
1244
|
+
const sourceEntries = loaded.sourceEntries || loaded.entries;
|
|
1245
|
+
const fingerprintBudget = { uncachedBytes: 0 };
|
|
1246
|
+
for (const entry of [...sourceEntries].sort((a, b) => a.relativePath.localeCompare(b.relativePath))) {
|
|
1232
1247
|
hash.update(`record\0${entry.relativePath}\0${entry.source.length}\0${entry.source}`);
|
|
1248
|
+
}
|
|
1249
|
+
for (const entry of [...loaded.entries].sort((a, b) => a.relativePath.localeCompare(b.relativePath))) {
|
|
1233
1250
|
const definition = loaded.model.resources[entry.record?.type];
|
|
1234
1251
|
if (!definition) continue;
|
|
1235
1252
|
for (const item of markdownEntries(loaded.model, entry.record).sort((a, b) => a.path.localeCompare(b.path))) {
|
|
1236
1253
|
try {
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1254
|
+
const resolvedPath = resolveDataPath(loaded.root, item.path);
|
|
1255
|
+
const file = await stat(resolvedPath);
|
|
1256
|
+
if (!file.isFile()) throw new Error("Markdown path is not a file.");
|
|
1257
|
+
const content = await fingerprintFileContent(resolvedPath, file, options, fingerprintBudget);
|
|
1258
|
+
hash.update(`markdown\0${item.path}\0${content.size}\0${content.digest}\0`);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (error?.code === "FILEGRC_FINGERPRINT_BUDGET") throw error;
|
|
1240
1261
|
hash.update(`markdown-missing\0${item.path}\0`);
|
|
1241
1262
|
}
|
|
1242
1263
|
}
|
|
@@ -1247,9 +1268,18 @@ export async function fingerprintWorkspace(input = process.cwd()) {
|
|
|
1247
1268
|
const paths = Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
|
|
1248
1269
|
for (const path of [...paths].sort()) {
|
|
1249
1270
|
try {
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1271
|
+
const resolvedPath = resolveDataPath(loaded.root, path);
|
|
1272
|
+
const file = await stat(resolvedPath);
|
|
1273
|
+
if (file.isFile()) {
|
|
1274
|
+
const identity = `${file.dev}:${file.ino}:${file.mode}:${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
|
|
1275
|
+
let content = options.fileDigestCache?.get(resolvedPath);
|
|
1276
|
+
if (!content || content.identity !== identity) {
|
|
1277
|
+
content = await fingerprintFileContent(resolvedPath, file, options, fingerprintBudget);
|
|
1278
|
+
}
|
|
1279
|
+
hash.update(`data-path\0${path}\0file\0${content.size}\0${content.digest}\0`);
|
|
1280
|
+
} else hash.update(`data-path\0${path}\0other\0`);
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
if (error?.code === "FILEGRC_FINGERPRINT_BUDGET") throw error;
|
|
1253
1283
|
hash.update(`data-path-missing\0${path}\0`);
|
|
1254
1284
|
}
|
|
1255
1285
|
}
|
|
@@ -1258,6 +1288,41 @@ export async function fingerprintWorkspace(input = process.cwd()) {
|
|
|
1258
1288
|
return { fingerprint: hash.digest("hex"), loaded };
|
|
1259
1289
|
}
|
|
1260
1290
|
|
|
1291
|
+
async function fingerprintFileContent(path, file, options, budget) {
|
|
1292
|
+
const identity = `${file.dev}:${file.ino}:${file.mode}:${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
|
|
1293
|
+
const cached = options.fileDigestCache?.get(path);
|
|
1294
|
+
if (cached?.identity === identity) return cached;
|
|
1295
|
+
assertFingerprintBudget(options, budget.uncachedBytes + file.size);
|
|
1296
|
+
budget.uncachedBytes += file.size;
|
|
1297
|
+
const contentHash = createHash("sha256");
|
|
1298
|
+
let size = 0;
|
|
1299
|
+
await fingerprintFileReadObserver?.(path);
|
|
1300
|
+
for await (const chunk of createReadStream(path)) {
|
|
1301
|
+
assertFingerprintBudget(options, budget.uncachedBytes - file.size + size + chunk.length);
|
|
1302
|
+
size += chunk.length;
|
|
1303
|
+
contentHash.update(chunk);
|
|
1304
|
+
}
|
|
1305
|
+
const content = { identity, size, digest: contentHash.digest("hex") };
|
|
1306
|
+
if (options.fileDigestCache?.size < 20_000 || options.fileDigestCache?.has(path)) {
|
|
1307
|
+
options.fileDigestCache.set(path, content);
|
|
1308
|
+
}
|
|
1309
|
+
return content;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function assertFingerprintBudget(options, uncachedBytes) {
|
|
1313
|
+
if (options.deadlineAt !== undefined && performance.now() >= options.deadlineAt) {
|
|
1314
|
+
const error = new Error("Workspace attachment verification exceeded its time limit. Reload and try again.");
|
|
1315
|
+
error.code = "FILEGRC_FINGERPRINT_BUDGET";
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
if (options.maxUncachedFileBytes !== undefined
|
|
1319
|
+
&& uncachedBytes > options.maxUncachedFileBytes) {
|
|
1320
|
+
const error = new Error("Workspace file verification exceeded its byte limit. Reload and try again.");
|
|
1321
|
+
error.code = "FILEGRC_FINGERPRINT_BUDGET";
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1261
1326
|
function validateImplementedControlSchedules(record, obligationsByControl, path, diagnostics) {
|
|
1262
1327
|
if (record.type !== "control" || record.status !== "implemented") return;
|
|
1263
1328
|
const schedules = obligationsByControl.get(record.id) || [];
|
package/src/web.js
CHANGED
|
@@ -95,6 +95,7 @@ let repositorySyncPollInFlight = false;
|
|
|
95
95
|
let mutationStateRefreshInFlight = false;
|
|
96
96
|
let mutationStateRefreshTimer = null;
|
|
97
97
|
let programSelectionGeneration = 0;
|
|
98
|
+
let expiredStateRefresh = null;
|
|
98
99
|
|
|
99
100
|
start().catch((error) => {
|
|
100
101
|
root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
|
|
@@ -193,14 +194,22 @@ function loadStateForRoute() {
|
|
|
193
194
|
for (const section of desiredStateSections(route)) loadStateSection(section);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
function loadStateSection(section) {
|
|
197
|
+
function loadStateSection(section, refreshExpired = true) {
|
|
197
198
|
if (!state.stateToken || state.sections?.[section] === "complete") return Promise.resolve();
|
|
198
199
|
if (stateSectionRequests.has(section)) return stateSectionRequests.get(section);
|
|
199
200
|
state.sections[section] = "loading";
|
|
200
201
|
const token = state.stateToken;
|
|
201
202
|
const programQuery = state.selectedProgramId ? "&programId=" + encodeURIComponent(state.selectedProgramId) : "";
|
|
202
|
-
const request =
|
|
203
|
-
.then((
|
|
203
|
+
const request = localFetch("/api/state/" + encodeURIComponent(section) + "?token=" + encodeURIComponent(token) + programQuery)
|
|
204
|
+
.then(async (response) => {
|
|
205
|
+
if (response.status === 409 && refreshExpired && state.stateToken === token) {
|
|
206
|
+
await refreshExpiredAppState(token);
|
|
207
|
+
render();
|
|
208
|
+
if (stateSectionRequests.get(section) === request) stateSectionRequests.delete(section);
|
|
209
|
+
return loadStateSection(section, false);
|
|
210
|
+
}
|
|
211
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
212
|
+
const result = await response.json();
|
|
204
213
|
if (state.stateToken !== result.stateToken) return;
|
|
205
214
|
Object.assign(state, result.state);
|
|
206
215
|
state.sections[section] = "complete";
|
|
@@ -2909,17 +2918,35 @@ function evidenceAttachmentPanel(entry) {
|
|
|
2909
2918
|
}
|
|
2910
2919
|
|
|
2911
2920
|
async function loadResourceDetail(type, id) {
|
|
2912
|
-
const key = type + "\0" + id;
|
|
2921
|
+
const key = (state.stateToken || "live") + "\0" + type + "\0" + id;
|
|
2913
2922
|
if (resourceDetailRequests.has(key)) return resourceDetailRequests.get(key);
|
|
2914
2923
|
const request = (async () => {
|
|
2915
2924
|
try {
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2925
|
+
let token = state.stateToken;
|
|
2926
|
+
let detail = null;
|
|
2927
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2928
|
+
const tokenQuery = token ? "?token=" + encodeURIComponent(token) : "";
|
|
2929
|
+
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
|
|
2930
|
+
if (response.status === 409 && token) {
|
|
2931
|
+
await refreshExpiredAppState(token);
|
|
2932
|
+
token = state.stateToken;
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2935
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
2936
|
+
detail = await response.json();
|
|
2937
|
+
if (!token || (detail.stateToken === token && state.stateToken === token)) break;
|
|
2938
|
+
token = state.stateToken;
|
|
2939
|
+
detail = null;
|
|
2940
|
+
}
|
|
2941
|
+
if (!detail) throw new Error("The workspace state changed while loading this record. Reload and try again.");
|
|
2942
|
+
delete detail.stateToken;
|
|
2919
2943
|
const index = state.resources.findIndex(({ record }) => record.type === type && record.id === id);
|
|
2920
2944
|
if (index >= 0) state.resources[index] = detail;
|
|
2921
2945
|
const route = parseRoute();
|
|
2922
|
-
if (route.name === "detail" && route.type === type && route.id === id)
|
|
2946
|
+
if (route.name === "detail" && route.type === type && route.id === id) {
|
|
2947
|
+
render();
|
|
2948
|
+
loadStateForRoute();
|
|
2949
|
+
}
|
|
2923
2950
|
} catch (error) {
|
|
2924
2951
|
const route = parseRoute();
|
|
2925
2952
|
if (route.name === "detail" && route.type === type && route.id === id) {
|
|
@@ -2927,13 +2954,52 @@ async function loadResourceDetail(type, id) {
|
|
|
2927
2954
|
if (main) main.innerHTML = '<div class="page"><section class="panel"><div class="dialog-error" role="alert">' + esc(error.message) + '</div></section></div>';
|
|
2928
2955
|
}
|
|
2929
2956
|
} finally {
|
|
2930
|
-
resourceDetailRequests
|
|
2957
|
+
for (const [requestKey, pending] of resourceDetailRequests) {
|
|
2958
|
+
if (pending === request) resourceDetailRequests.delete(requestKey);
|
|
2959
|
+
}
|
|
2931
2960
|
}
|
|
2932
2961
|
})();
|
|
2933
2962
|
resourceDetailRequests.set(key, request);
|
|
2934
2963
|
return request;
|
|
2935
2964
|
}
|
|
2936
2965
|
|
|
2966
|
+
function refreshExpiredAppState(expectedToken) {
|
|
2967
|
+
if (expiredStateRefresh) {
|
|
2968
|
+
if (expiredStateRefresh.token === expectedToken) return expiredStateRefresh.promise;
|
|
2969
|
+
return expiredStateRefresh.promise
|
|
2970
|
+
.catch(() => {})
|
|
2971
|
+
.then(() => refreshExpiredAppState(expectedToken));
|
|
2972
|
+
}
|
|
2973
|
+
if (expectedToken && state.stateToken !== expectedToken) return Promise.resolve();
|
|
2974
|
+
const programQuery = state.selectedProgramId ? "?programId=" + encodeURIComponent(state.selectedProgramId) : "";
|
|
2975
|
+
const holder = { token: expectedToken, promise: null };
|
|
2976
|
+
const refresh = (async () => {
|
|
2977
|
+
const response = await fetch("/api/state/bootstrap" + programQuery);
|
|
2978
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
2979
|
+
const next = normalizeAppState(await response.json());
|
|
2980
|
+
if (expectedToken && state.stateToken !== expectedToken) return;
|
|
2981
|
+
rekeyResourceDetailRequests(state.stateToken, next.stateToken);
|
|
2982
|
+
state = next;
|
|
2983
|
+
stateSectionRequests.clear();
|
|
2984
|
+
})().finally(() => {
|
|
2985
|
+
if (expiredStateRefresh === holder) expiredStateRefresh = null;
|
|
2986
|
+
});
|
|
2987
|
+
holder.promise = refresh;
|
|
2988
|
+
expiredStateRefresh = holder;
|
|
2989
|
+
return refresh;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
function rekeyResourceDetailRequests(previousToken, nextToken) {
|
|
2993
|
+
if (!previousToken || !nextToken || previousToken === nextToken) return;
|
|
2994
|
+
const prefix = previousToken + "\0";
|
|
2995
|
+
for (const [key, request] of resourceDetailRequests) {
|
|
2996
|
+
if (!key.startsWith(prefix)) continue;
|
|
2997
|
+
const nextKey = nextToken + key.slice(previousToken.length);
|
|
2998
|
+
resourceDetailRequests.delete(key);
|
|
2999
|
+
if (!resourceDetailRequests.has(nextKey)) resourceDetailRequests.set(nextKey, request);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
|
|
2937
3003
|
function issueSeed(type, source) {
|
|
2938
3004
|
const owners = source.ownerIds || source.reviewerIds || source.assessorIds || source.testerIds || [];
|
|
2939
3005
|
if (type === "finding") {
|
|
@@ -116,7 +116,7 @@ export async function validateWorkflowHistoryIntegrity(loaded, diagnostics) {
|
|
|
116
116
|
entry.record.type === "exception"
|
|
117
117
|
&& historical.record.reportingRouteSetId
|
|
118
118
|
&& historical.commit !== commits.find((commit) => {
|
|
119
|
-
try { return Number(JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"))?.dataModelVersion) >= 10; } catch { return false; }
|
|
119
|
+
try { return Number(JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"))?.dataModelVersion) >= 10; } catch (error) { rethrowGitDeadline(error); return false; }
|
|
120
120
|
})
|
|
121
121
|
) {
|
|
122
122
|
const records = recordsAtRevision(loaded, historical.commit);
|
|
@@ -146,7 +146,7 @@ export async function validateWorkflowHistoryIntegrity(loaded, diagnostics) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const firstV10Commit = commits.find((commit) => {
|
|
149
|
-
try { return Number(JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"))?.dataModelVersion) >= 10; } catch { return false; }
|
|
149
|
+
try { return Number(JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"))?.dataModelVersion) >= 10; } catch (error) { rethrowGitDeadline(error); return false; }
|
|
150
150
|
});
|
|
151
151
|
const protectedRouteSources = new Map();
|
|
152
152
|
for (const [commit, records] of revisionRecords) {
|
|
@@ -307,7 +307,8 @@ function legacyCollectionReviewTransition(loaded, previous, current) {
|
|
|
307
307
|
try {
|
|
308
308
|
const workspace = JSON.parse(getFileAtRevision(loaded.root, current.commit, "data/workspace.json"));
|
|
309
309
|
return Number(workspace?.dataModelVersion) < 9;
|
|
310
|
-
} catch {
|
|
310
|
+
} catch (error) {
|
|
311
|
+
rethrowGitDeadline(error);
|
|
311
312
|
return false;
|
|
312
313
|
}
|
|
313
314
|
}
|
|
@@ -319,7 +320,8 @@ function historicalState(loaded) {
|
|
|
319
320
|
try {
|
|
320
321
|
const workspace = JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"));
|
|
321
322
|
return Number(workspace?.dataModelVersion) >= 9;
|
|
322
|
-
} catch {
|
|
323
|
+
} catch (error) {
|
|
324
|
+
rethrowGitDeadline(error);
|
|
323
325
|
return false;
|
|
324
326
|
}
|
|
325
327
|
});
|
|
@@ -392,7 +394,8 @@ function legacyCollectionReview(loaded, historical) {
|
|
|
392
394
|
let workspace;
|
|
393
395
|
try {
|
|
394
396
|
workspace = JSON.parse(getFileAtRevision(loaded.root, historical.commit, "data/workspace.json"));
|
|
395
|
-
} catch {
|
|
397
|
+
} catch (error) {
|
|
398
|
+
rethrowGitDeadline(error);
|
|
396
399
|
return false;
|
|
397
400
|
}
|
|
398
401
|
return Number(workspace?.dataModelVersion) < 9
|
|
@@ -408,7 +411,8 @@ function recordAtRevision(loaded, commit, id, identityHistory) {
|
|
|
408
411
|
try {
|
|
409
412
|
const record = JSON.parse(source);
|
|
410
413
|
return record?.id === id ? { record, path: item.path } : null;
|
|
411
|
-
} catch {
|
|
414
|
+
} catch (error) {
|
|
415
|
+
rethrowGitDeadline(error);
|
|
412
416
|
return null;
|
|
413
417
|
}
|
|
414
418
|
}
|
|
@@ -454,7 +458,7 @@ async function validateReportingRouteAuthorityHistory(loaded, commits, revisionR
|
|
|
454
458
|
));
|
|
455
459
|
if (!routeSets.length) return;
|
|
456
460
|
const firstV10Commit = commits.find((commit) => {
|
|
457
|
-
try { return Number(JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"))?.dataModelVersion) >= 10; } catch { return false; }
|
|
461
|
+
try { return Number(JSON.parse(getFileAtRevision(loaded.root, commit, "data/workspace.json"))?.dataModelVersion) >= 10; } catch (error) { rethrowGitDeadline(error); return false; }
|
|
458
462
|
});
|
|
459
463
|
for (const route of routeSets) {
|
|
460
464
|
const versions = changedVersions(route.id, commits, revisionRecords, historiesById);
|
|
@@ -733,7 +737,8 @@ function authorityAppointmentOverlapsRoute(appointment, route, workspaceId, time
|
|
|
733
737
|
routeEnd = route.cancellation?.canceledAt
|
|
734
738
|
? currentCalendarDate(timezone, new Date(route.cancellation.canceledAt))
|
|
735
739
|
: null;
|
|
736
|
-
} catch {
|
|
740
|
+
} catch (error) {
|
|
741
|
+
rethrowGitDeadline(error);
|
|
737
742
|
return false;
|
|
738
743
|
}
|
|
739
744
|
return (!routeEnd || appointment.startsOn <= routeEnd)
|
|
@@ -786,7 +791,8 @@ function historicalWorkspaceTimezoneAtRevision(loaded, commit) {
|
|
|
786
791
|
if (typeof workspace?.timezone !== "string" || !workspace.timezone.trim()) return null;
|
|
787
792
|
new Intl.DateTimeFormat("en", { timeZone: workspace.timezone }).format();
|
|
788
793
|
return workspace.timezone;
|
|
789
|
-
} catch {
|
|
794
|
+
} catch (error) {
|
|
795
|
+
rethrowGitDeadline(error);
|
|
790
796
|
return null;
|
|
791
797
|
}
|
|
792
798
|
}
|
|
@@ -901,3 +907,7 @@ function integrityError(entry, message) {
|
|
|
901
907
|
message
|
|
902
908
|
};
|
|
903
909
|
}
|
|
910
|
+
|
|
911
|
+
function rethrowGitDeadline(error) {
|
|
912
|
+
if (error?.code === "FILEGRC_GIT_DEADLINE") throw error;
|
|
913
|
+
}
|
package/src/workflow.js
CHANGED
|
@@ -141,7 +141,9 @@ async function assessWorkflowUnmeasured(input, options = {}) {
|
|
|
141
141
|
asOf,
|
|
142
142
|
evaluatedAt,
|
|
143
143
|
programId: programRecord.type === "program" ? programRecord.id : undefined,
|
|
144
|
-
controlIds: options.auditId ? audits[0]?.controlIds : programRecord?.controlIds
|
|
144
|
+
controlIds: options.auditId ? audits[0]?.controlIds : programRecord?.controlIds,
|
|
145
|
+
historyDeadlineAt: options.historyDeadlineAt,
|
|
146
|
+
strictHistory: options.strictHistory
|
|
145
147
|
});
|
|
146
148
|
const guidedFindings = [
|
|
147
149
|
...programFindings(program),
|
|
@@ -732,7 +734,10 @@ async function assessPeriodHealth(loaded, options) {
|
|
|
732
734
|
"component"
|
|
733
735
|
].includes(record.type));
|
|
734
736
|
const paths = relevantEntries.map(({ relativePath }) => `data/${relativePath}`);
|
|
735
|
-
const histories = getWorkspaceHistories(loaded.root, paths, 50
|
|
737
|
+
const histories = getWorkspaceHistories(loaded.root, paths, 50, {
|
|
738
|
+
deadlineAt: options.historyDeadlineAt,
|
|
739
|
+
strict: options.strictHistory === true
|
|
740
|
+
});
|
|
736
741
|
const allHistory = [...histories.values()].flat().filter(Boolean);
|
|
737
742
|
const earliestCommitDate = allHistory
|
|
738
743
|
.map(({ timestamp }) => timestamp?.slice(0, 10))
|
package/src/workspace.js
CHANGED
|
@@ -14,11 +14,13 @@ async function loadWorkspaceUnmeasured(input) {
|
|
|
14
14
|
const diagnostics = [];
|
|
15
15
|
const files = await collectJsonFiles(dataRoot);
|
|
16
16
|
const resources = [];
|
|
17
|
+
const sourceEntries = [];
|
|
17
18
|
|
|
18
19
|
for (const path of files) {
|
|
19
20
|
const relativePath = relative(dataRoot, path).split(sep).join("/");
|
|
20
21
|
try {
|
|
21
22
|
const source = await readFile(path, "utf8");
|
|
23
|
+
sourceEntries.push({ path, relativePath, source });
|
|
22
24
|
const record = JSON.parse(source);
|
|
23
25
|
resources.push({ record, path, relativePath, source });
|
|
24
26
|
} catch (error) {
|
|
@@ -68,6 +70,7 @@ async function loadWorkspaceUnmeasured(input) {
|
|
|
68
70
|
model,
|
|
69
71
|
workspace: workspaceEntry?.record ?? null,
|
|
70
72
|
entries: resources,
|
|
73
|
+
sourceEntries,
|
|
71
74
|
resources: resources.map(({ record }) => record),
|
|
72
75
|
diagnostics
|
|
73
76
|
};
|