filegrc 0.3.4 → 0.5.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 +24 -6
- package/model/index.js +41 -3
- package/model/v1.json +81 -47
- package/model/v2.json +8022 -0
- package/model/v3.json +9391 -0
- package/package.json +2 -2
- package/src/agent.js +89 -8
- package/src/appointments.js +19 -0
- package/src/audit-preparation.js +109 -65
- package/src/audit-transition.js +96 -0
- package/src/batch-review.js +109 -0
- package/src/cli.js +563 -148
- package/src/collection-review.js +185 -0
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +149 -77
- package/src/external-reviewer.js +165 -0
- package/src/files.js +267 -29
- package/src/git.js +239 -41
- package/src/index.js +41 -7
- package/src/model-docs.js +103 -7
- package/src/model-migration.js +1958 -0
- package/src/mutation.js +42 -0
- package/src/obligations.js +502 -95
- package/src/parties.js +17 -2
- package/src/program-lifecycle.js +1 -0
- package/src/program-path.js +70 -60
- package/src/program-readiness.js +470 -130
- package/src/reconciliation.js +277 -0
- package/src/resource-status.js +17 -0
- package/src/server.js +347 -48
- package/src/setup.js +57 -26
- package/src/source-coverage.js +61 -0
- package/src/state.js +122 -25
- package/src/timing.js +41 -0
- package/src/validate.js +707 -44
- package/src/web.js +1440 -304
- package/src/workflow.js +1595 -0
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/src/setup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyResourceBatch } from "./files.js";
|
|
2
2
|
import { createResourceId } from "./id.js";
|
|
3
3
|
import { loadWorkspace } from "./workspace.js";
|
|
4
4
|
|
|
@@ -10,18 +10,30 @@ export async function setupWorkspace(input = process.cwd(), payload = {}) {
|
|
|
10
10
|
const setup = normalizeSetupPayload(payload);
|
|
11
11
|
validateSetup(loaded, setup);
|
|
12
12
|
const plan = buildSetupRecords(loaded, setup);
|
|
13
|
+
const updates = [
|
|
14
|
+
...(plan.existingSystem ? [plan.system] : []),
|
|
15
|
+
plan.workspace,
|
|
16
|
+
...(plan.renderer ? [plan.renderer] : [])
|
|
17
|
+
];
|
|
18
|
+
const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, entry.revision]));
|
|
13
19
|
|
|
14
|
-
await
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
await applyResourceBatch(loaded.root, {
|
|
21
|
+
create: [
|
|
22
|
+
...(plan.existingSystem ? [] : [plan.system]),
|
|
23
|
+
...(plan.commitment ? [plan.commitment] : [])
|
|
24
|
+
],
|
|
25
|
+
update: updates,
|
|
26
|
+
expectedRevisions: Object.fromEntries(updates.map((record) => [record.id, revisionById.get(record.id)])),
|
|
27
|
+
validateWholeWorkspace: true
|
|
28
|
+
});
|
|
17
29
|
|
|
18
30
|
return {
|
|
19
31
|
draft: setup.draft,
|
|
20
32
|
system: plan.system,
|
|
21
33
|
workspace: plan.workspace,
|
|
22
34
|
renderer: plan.renderer,
|
|
35
|
+
commitment: plan.commitment,
|
|
23
36
|
linkedControlIds: [],
|
|
24
|
-
evidenceTestDraftIds: [],
|
|
25
37
|
onboardingComplete: !setup.draft
|
|
26
38
|
};
|
|
27
39
|
}
|
|
@@ -40,11 +52,12 @@ export async function planWorkspaceSetup(input = process.cwd(), payload = {}) {
|
|
|
40
52
|
workspace: "update",
|
|
41
53
|
renderer: plan.renderer ? "update" : "unchanged",
|
|
42
54
|
controls: 0,
|
|
43
|
-
|
|
55
|
+
commitment: plan.commitment ? "create" : "unchanged"
|
|
44
56
|
},
|
|
45
57
|
system: setupSystemSummary(plan.system),
|
|
46
58
|
target: setupTargetSummary(plan.workspace),
|
|
47
59
|
renderer: plan.renderer ? setupRendererSummary(plan.renderer) : null,
|
|
60
|
+
commitment: plan.commitment || null,
|
|
48
61
|
onboardingComplete: !setup.draft
|
|
49
62
|
};
|
|
50
63
|
}
|
|
@@ -58,11 +71,12 @@ export function summarizeSetupResult(result) {
|
|
|
58
71
|
system: "saved",
|
|
59
72
|
workspace: "updated",
|
|
60
73
|
controls: result.linkedControlIds?.length || 0,
|
|
61
|
-
|
|
74
|
+
commitment: result.commitment ? "saved" : "unchanged"
|
|
62
75
|
},
|
|
63
76
|
system: setupSystemSummary(result.system),
|
|
64
77
|
target: setupTargetSummary(result.workspace),
|
|
65
78
|
renderer: result.renderer ? setupRendererSummary(result.renderer) : null,
|
|
79
|
+
commitment: result.commitment || null,
|
|
66
80
|
onboardingComplete: result.onboardingComplete
|
|
67
81
|
};
|
|
68
82
|
}
|
|
@@ -77,7 +91,7 @@ export function normalizeSetupPayload(payload = {}) {
|
|
|
77
91
|
boundary: cleanMultilineText(payload.boundary ?? payload.scope, "boundary"),
|
|
78
92
|
ownerId: cleanText(payload.ownerId ?? payload.owner, "ownerId"),
|
|
79
93
|
criticality: cleanText(payload.criticality, "criticality"),
|
|
80
|
-
|
|
94
|
+
classificationId: cleanText(payload.classificationId, "classificationId"),
|
|
81
95
|
internetExposed: booleanValue(payload.internetExposed, "internetExposed"),
|
|
82
96
|
programGoal: cleanText(payload.programGoal ?? "none", "programGoal"),
|
|
83
97
|
draft,
|
|
@@ -91,7 +105,7 @@ function validateSetup(loaded, setup) {
|
|
|
91
105
|
["boundary", setup.boundary],
|
|
92
106
|
["ownerId", setup.ownerId],
|
|
93
107
|
["criticality", setup.criticality],
|
|
94
|
-
["
|
|
108
|
+
["classificationId", setup.classificationId]
|
|
95
109
|
]) {
|
|
96
110
|
if (!value) throw new Error(`Setup field "${name}" is required.`);
|
|
97
111
|
}
|
|
@@ -113,23 +127,24 @@ function validateSetup(loaded, setup) {
|
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
const classifications = Object.keys(loaded.workspace.classificationDefinitions || {});
|
|
116
|
-
if (classifications.length && !classifications.includes(setup.
|
|
117
|
-
throw new Error(`
|
|
130
|
+
if (classifications.length && !classifications.includes(setup.classificationId)) {
|
|
131
|
+
throw new Error(`classificationId must be one of ${classifications.join(", ")}.`);
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
function findSetupSystem(resources, setup) {
|
|
135
|
+
function findSetupSystem(resources, workspace, setup) {
|
|
136
|
+
const scopedSystemIds = new Set(workspace.systemIds || []);
|
|
122
137
|
return (setup.systemId && resources.find(({ type, id }) => type === "system" && id === setup.systemId))
|
|
123
|
-
|| resources.find(({ type,
|
|
138
|
+
|| resources.find(({ type, id, title, status }) => (
|
|
124
139
|
type === "system"
|
|
125
|
-
&&
|
|
140
|
+
&& scopedSystemIds.has(id)
|
|
126
141
|
&& status !== "retired"
|
|
127
142
|
&& title.trim().toLowerCase() === setup.serviceName.toLowerCase()
|
|
128
143
|
));
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
function buildSetupRecords(loaded, setup) {
|
|
132
|
-
const existingSystem = findSetupSystem(loaded.resources, setup);
|
|
147
|
+
const existingSystem = findSetupSystem(loaded.resources, loaded.workspace, setup);
|
|
133
148
|
const systemId = existingSystem?.id || createResourceId(
|
|
134
149
|
"system",
|
|
135
150
|
setup.serviceName,
|
|
@@ -137,7 +152,6 @@ function buildSetupRecords(loaded, setup) {
|
|
|
137
152
|
);
|
|
138
153
|
const system = {
|
|
139
154
|
...(existingSystem || {}),
|
|
140
|
-
schemaVersion: 1,
|
|
141
155
|
id: systemId,
|
|
142
156
|
type: "system",
|
|
143
157
|
title: setup.serviceName,
|
|
@@ -150,9 +164,8 @@ function buildSetupRecords(loaded, setup) {
|
|
|
150
164
|
ownerIds: [setup.ownerId],
|
|
151
165
|
description: setup.boundary,
|
|
152
166
|
systemKind: existingSystem?.systemKind || "service",
|
|
153
|
-
|
|
154
|
-
internetExposed: setup.internetExposed
|
|
155
|
-
inScope: true
|
|
167
|
+
classificationId: setup.classificationId,
|
|
168
|
+
internetExposed: setup.internetExposed
|
|
156
169
|
};
|
|
157
170
|
const existingWorkspace = loaded.resources.find(({ type }) => type === "workspace");
|
|
158
171
|
if (!existingWorkspace) throw new Error("The workspace settings record was not found.");
|
|
@@ -163,13 +176,31 @@ function buildSetupRecords(loaded, setup) {
|
|
|
163
176
|
};
|
|
164
177
|
const existingRenderer = loaded.resources.find(({ type }) => type === "renderer-settings");
|
|
165
178
|
const renderer = existingRenderer ? { ...existingRenderer, showOnboarding: setup.draft } : null;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
const existingCommitment = loaded.resources.find((record) => (
|
|
180
|
+
record.type === "commitment"
|
|
181
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
182
|
+
&& (record.systemIds || []).includes(systemId)
|
|
183
|
+
));
|
|
184
|
+
const commitment = String(loaded.model.modelVersion) === "3" && !existingCommitment
|
|
185
|
+
? {
|
|
186
|
+
id: createResourceId(
|
|
187
|
+
"commitment",
|
|
188
|
+
`${setup.serviceName} service commitment`,
|
|
189
|
+
loaded.resources.map(({ id }) => id)
|
|
190
|
+
),
|
|
191
|
+
type: "commitment",
|
|
192
|
+
title: `${setup.serviceName} service commitment`,
|
|
193
|
+
status: "planned",
|
|
194
|
+
commitmentKind: "service",
|
|
195
|
+
statement: "Replace this starter with the actual customer promise or approved service requirement before activation.",
|
|
196
|
+
systemIds: [systemId],
|
|
197
|
+
ownerIds: [setup.ownerId],
|
|
198
|
+
customerFacing: true,
|
|
199
|
+
...(workspace.requirementIds?.length ? { requirementIds: [...workspace.requirementIds] } : {}),
|
|
200
|
+
...(workspace.controlIds?.length ? { controlIds: [...workspace.controlIds] } : {})
|
|
201
|
+
}
|
|
202
|
+
: null;
|
|
203
|
+
return { existingSystem, system, workspace, renderer, commitment };
|
|
173
204
|
}
|
|
174
205
|
|
|
175
206
|
function assuranceGoalFromSetup(goal) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function sourceCoverageComplete(record, loaded) {
|
|
2
|
+
if (!record?.validFrom || !record.collectionCadence || !record.retention || !record.reconciliationMethod) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (record.coverageKind === "external-system" && (!record.systemId || !(record.retrieverIds || []).length)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (["not-applicable", "zero-population"].includes(record.coverageKind) && !record.applicabilityReview) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (loaded.workspace?.candidateCoverage && !(record.readinessTestEvidenceIds || []).length) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (loaded.workspace?.candidateCoverage) {
|
|
15
|
+
const tests = (record.readinessTestEvidenceIds || []).map((id) => (
|
|
16
|
+
loaded.resources.find((resource) => resource.id === id && resource.type === "evidence")
|
|
17
|
+
));
|
|
18
|
+
if (tests.some((test) => (
|
|
19
|
+
!test
|
|
20
|
+
|| test.readinessTest !== true
|
|
21
|
+
|| test.retrievalResult !== "passed"
|
|
22
|
+
|| test.accessConfirmed !== true
|
|
23
|
+
|| !(test.coveredSourceFamilyIds || []).includes(record.sourceFamilyId)
|
|
24
|
+
|| (record.coverageKind === "external-system" && !(
|
|
25
|
+
test.sourceSystemId === record.systemId
|
|
26
|
+
|| (test.systemIds || []).includes(record.systemId)
|
|
27
|
+
))
|
|
28
|
+
))) return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function assessSourceCoverageReadiness(loaded, selectedControlIds = []) {
|
|
34
|
+
if (!loaded.model.resources["source-coverage"]) return [];
|
|
35
|
+
const selected = new Set(selectedControlIds);
|
|
36
|
+
const selectedControlCodes = new Set(loaded.resources
|
|
37
|
+
.filter((record) => (
|
|
38
|
+
record.type === "control"
|
|
39
|
+
&& selected.has(record.id)
|
|
40
|
+
&& !["not-applicable", "retired"].includes(record.status)
|
|
41
|
+
))
|
|
42
|
+
.map(({ code }) => code)
|
|
43
|
+
.filter(Boolean));
|
|
44
|
+
return (loaded.model.evidenceSourceFamilies || [])
|
|
45
|
+
.filter((family) => family.controlCodes.some((code) => selectedControlCodes.has(code)))
|
|
46
|
+
.map((family) => {
|
|
47
|
+
const records = loaded.resources.filter((record) => (
|
|
48
|
+
record.type === "source-coverage"
|
|
49
|
+
&& record.sourceFamilyId === family.id
|
|
50
|
+
&& record.status !== "retired"
|
|
51
|
+
));
|
|
52
|
+
const record = records.find(({ status }) => status === "active")
|
|
53
|
+
|| records.find(({ status }) => status === "planned")
|
|
54
|
+
|| null;
|
|
55
|
+
return {
|
|
56
|
+
family,
|
|
57
|
+
record,
|
|
58
|
+
complete: Boolean(record?.status === "active" && sourceCoverageComplete(record, loaded))
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
package/src/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
4
|
+
import { assessCollectionReviews } from "./collection-review.js";
|
|
4
5
|
import { getBrowserRepositoryState, getGitSummary, getWorkspaceHistories } from "./git.js";
|
|
5
6
|
import { renderMarkdown } from "./markdown.js";
|
|
6
7
|
import { planObligations } from "./obligations.js";
|
|
@@ -8,36 +9,39 @@ import { resolveDataPath } from "./paths.js";
|
|
|
8
9
|
import { assessProgramReadiness } from "./program-readiness.js";
|
|
9
10
|
import { markdownEntries } from "./resource-markdown.js";
|
|
10
11
|
import { currentCalendarDate } from "./time.js";
|
|
11
|
-
import {
|
|
12
|
+
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
13
|
+
import { fingerprintWorkspace, validateWorkspace } from "./validate.js";
|
|
14
|
+
import { assessWorkflow } from "./workflow.js";
|
|
15
|
+
|
|
16
|
+
const renderedMarkdownCache = new Map();
|
|
17
|
+
const MAX_RENDERED_MARKDOWN_CACHE_ENTRIES = 1_000;
|
|
12
18
|
|
|
13
19
|
export async function createAppState(input = process.cwd(), options = {}) {
|
|
14
|
-
|
|
20
|
+
return serializeWorkspaceMutation(input, (root) => createAppStateUnlocked(root, options));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function createAppStateUnlocked(input, options) {
|
|
24
|
+
let validation;
|
|
25
|
+
if (options.validationProof) {
|
|
26
|
+
const current = await fingerprintWorkspace(input);
|
|
27
|
+
validation = current.fingerprint === options.validationProof.fingerprint
|
|
28
|
+
? { ...options.validationProof.validation, loaded: current.loaded }
|
|
29
|
+
: await validateWorkspace(current.loaded);
|
|
30
|
+
} else {
|
|
31
|
+
validation = await validateWorkspace(input);
|
|
32
|
+
}
|
|
15
33
|
const { loaded } = validation;
|
|
16
34
|
const entries = [];
|
|
17
|
-
const
|
|
18
|
-
const histories =
|
|
35
|
+
const includeDetails = options.includeDetails !== false;
|
|
36
|
+
const histories = includeDetails
|
|
37
|
+
? getWorkspaceHistories(loaded.root, loaded.entries.map((entry) => `data/${entry.relativePath}`), 12)
|
|
38
|
+
: new Map();
|
|
19
39
|
|
|
20
40
|
for (const entry of loaded.entries) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (loaded.model.resources[record.type]) {
|
|
24
|
-
for (const item of markdownEntries(loaded.model, record)) {
|
|
25
|
-
try {
|
|
26
|
-
const path = resolveDataPath(loaded.root, item.path);
|
|
27
|
-
const source = await readFile(path, "utf8");
|
|
28
|
-
content[item.name] = { source, html: renderMarkdown(source), path: item.path, revision: contentRevision(source) };
|
|
29
|
-
} catch {
|
|
30
|
-
// Validation reports missing required Markdown.
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
entries.push({
|
|
35
|
-
record,
|
|
36
|
-
relativePath: `data/${entry.relativePath}`,
|
|
37
|
-
revision: contentRevision(entry.source),
|
|
38
|
-
content,
|
|
41
|
+
entries.push(await createStateEntry(loaded, entry, {
|
|
42
|
+
includeDetails,
|
|
39
43
|
history: histories.get(`data/${entry.relativePath}`) ?? []
|
|
40
|
-
});
|
|
44
|
+
}));
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
const git = getGitSummary(loaded.root);
|
|
@@ -47,7 +51,6 @@ export async function createAppState(input = process.cwd(), options = {}) {
|
|
|
47
51
|
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
|
|
48
52
|
});
|
|
49
53
|
const workspace = loaded.workspace ?? {
|
|
50
|
-
schemaVersion: 1,
|
|
51
54
|
dataModelVersion: loaded.model.modelVersion,
|
|
52
55
|
id: "workspace",
|
|
53
56
|
type: "workspace",
|
|
@@ -72,8 +75,41 @@ export async function createAppState(input = process.cwd(), options = {}) {
|
|
|
72
75
|
return [audit?.id || "none", preparation];
|
|
73
76
|
})
|
|
74
77
|
));
|
|
78
|
+
const obligations = planObligations(entries, {
|
|
79
|
+
asOf,
|
|
80
|
+
now: options.now ?? generatedAt,
|
|
81
|
+
model: loaded.model
|
|
82
|
+
});
|
|
83
|
+
const workflow = await assessWorkflow(loaded, {
|
|
84
|
+
asOf,
|
|
85
|
+
evaluatedAt: generatedAt,
|
|
86
|
+
programReadiness,
|
|
87
|
+
auditPreparations: Object.fromEntries(
|
|
88
|
+
Object.entries(auditPreparations).filter(([id]) => id !== "none")
|
|
89
|
+
),
|
|
90
|
+
obligations,
|
|
91
|
+
git,
|
|
92
|
+
validation
|
|
93
|
+
});
|
|
94
|
+
const collectionReviews = Object.fromEntries(
|
|
95
|
+
assessCollectionReviews(loaded).map((assessment) => [
|
|
96
|
+
assessment.resourceType,
|
|
97
|
+
{
|
|
98
|
+
resourceType: assessment.resourceType,
|
|
99
|
+
configuration: assessment.configuration,
|
|
100
|
+
recordCount: assessment.recordCount,
|
|
101
|
+
review: assessment.review,
|
|
102
|
+
reviewRevision: assessment.reviewRevision,
|
|
103
|
+
collectionRevision: assessment.collectionRevision,
|
|
104
|
+
status: assessment.status,
|
|
105
|
+
complete: assessment.complete,
|
|
106
|
+
message: assessment.message
|
|
107
|
+
}
|
|
108
|
+
])
|
|
109
|
+
);
|
|
75
110
|
return {
|
|
76
111
|
generatedAt,
|
|
112
|
+
asOf,
|
|
77
113
|
readOnly: Boolean(options.readOnly || (repository.mode === "trunk" && !repository.writesAllowed)),
|
|
78
114
|
repository,
|
|
79
115
|
workspace,
|
|
@@ -84,13 +120,74 @@ export async function createAppState(input = process.cwd(), options = {}) {
|
|
|
84
120
|
counts: validation.counts,
|
|
85
121
|
diagnostics: validation.diagnostics
|
|
86
122
|
},
|
|
87
|
-
obligations
|
|
123
|
+
obligations,
|
|
124
|
+
collectionReviews,
|
|
88
125
|
programReadiness,
|
|
89
126
|
auditPreparations,
|
|
127
|
+
workflow,
|
|
90
128
|
git
|
|
91
129
|
};
|
|
92
130
|
}
|
|
93
131
|
|
|
132
|
+
export async function createResourceDetail(input, type, id) {
|
|
133
|
+
return serializeWorkspaceMutation(input, async (root) => {
|
|
134
|
+
const validation = await validateWorkspace(root);
|
|
135
|
+
const entry = validation.loaded.entries.find(({ record }) => record.type === type && record.id === id);
|
|
136
|
+
if (!entry) return null;
|
|
137
|
+
const relativePath = `data/${entry.relativePath}`;
|
|
138
|
+
const histories = getWorkspaceHistories(validation.loaded.root, [relativePath], 12);
|
|
139
|
+
return createStateEntry(validation.loaded, entry, {
|
|
140
|
+
includeDetails: true,
|
|
141
|
+
history: histories.get(relativePath) ?? []
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function createStateEntry(loaded, entry, options) {
|
|
147
|
+
const record = structuredClone(entry.record);
|
|
148
|
+
const content = {};
|
|
149
|
+
if (loaded.model.resources[record.type]) {
|
|
150
|
+
for (const item of markdownEntries(loaded.model, record)) {
|
|
151
|
+
try {
|
|
152
|
+
const path = resolveDataPath(loaded.root, item.path);
|
|
153
|
+
const source = await readFile(path, "utf8");
|
|
154
|
+
content[item.name] = {
|
|
155
|
+
source,
|
|
156
|
+
...(options.includeDetails ? { html: renderMarkdownCached(source) } : {}),
|
|
157
|
+
path: item.path,
|
|
158
|
+
revision: contentRevision(source)
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
// Validation reports missing required Markdown.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
record,
|
|
167
|
+
relativePath: `data/${entry.relativePath}`,
|
|
168
|
+
revision: contentRevision(entry.source),
|
|
169
|
+
content,
|
|
170
|
+
history: options.includeDetails ? options.history : undefined,
|
|
171
|
+
detailsLoaded: options.includeDetails
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderMarkdownCached(source) {
|
|
176
|
+
const revision = contentRevision(source);
|
|
177
|
+
const cached = renderedMarkdownCache.get(revision);
|
|
178
|
+
if (cached !== undefined) {
|
|
179
|
+
renderedMarkdownCache.delete(revision);
|
|
180
|
+
renderedMarkdownCache.set(revision, cached);
|
|
181
|
+
return cached;
|
|
182
|
+
}
|
|
183
|
+
const html = renderMarkdown(source);
|
|
184
|
+
renderedMarkdownCache.set(revision, html);
|
|
185
|
+
if (renderedMarkdownCache.size > MAX_RENDERED_MARKDOWN_CACHE_ENTRIES) {
|
|
186
|
+
renderedMarkdownCache.delete(renderedMarkdownCache.keys().next().value);
|
|
187
|
+
}
|
|
188
|
+
return html;
|
|
189
|
+
}
|
|
190
|
+
|
|
94
191
|
function contentRevision(source) {
|
|
95
192
|
return createHash("sha256").update(source).digest("hex");
|
|
96
193
|
}
|
package/src/timing.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
|
|
4
|
+
const timingContext = new AsyncLocalStorage();
|
|
5
|
+
|
|
6
|
+
export async function collectTimings(task) {
|
|
7
|
+
const timings = new Map();
|
|
8
|
+
const result = await timingContext.run(timings, task);
|
|
9
|
+
return { result, timings: Object.fromEntries(timings) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function measureTiming(name, task) {
|
|
13
|
+
const started = performance.now();
|
|
14
|
+
try {
|
|
15
|
+
return await task();
|
|
16
|
+
} finally {
|
|
17
|
+
recordTiming(name, performance.now() - started);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function measureTimingSync(name, task) {
|
|
22
|
+
const started = performance.now();
|
|
23
|
+
try {
|
|
24
|
+
return task();
|
|
25
|
+
} finally {
|
|
26
|
+
recordTiming(name, performance.now() - started);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function recordTiming(name, durationMs) {
|
|
31
|
+
const timings = timingContext.getStore();
|
|
32
|
+
if (!timings) return;
|
|
33
|
+
const current = timings.get(name) ?? { count: 0, durationMs: 0 };
|
|
34
|
+
current.count += 1;
|
|
35
|
+
current.durationMs += durationMs;
|
|
36
|
+
timings.set(name, current);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function timingEnabled() {
|
|
40
|
+
return process.env.FILEGRC_TIMING === "1";
|
|
41
|
+
}
|