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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { applyResourceBatch } from "./files.js";
|
|
3
|
+
import { getGitSummary } from "./git.js";
|
|
4
|
+
import { loadWorkspace } from "./workspace.js";
|
|
5
|
+
|
|
6
|
+
export function collectionRevision(loaded, resourceType) {
|
|
7
|
+
const records = loaded.entries
|
|
8
|
+
.filter(({ record }) => record.type === resourceType)
|
|
9
|
+
.map(({ record, source }) => ({
|
|
10
|
+
id: record.id,
|
|
11
|
+
revision: createHash("sha256").update(source).digest("hex")
|
|
12
|
+
}))
|
|
13
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
14
|
+
const workspaceScope = {
|
|
15
|
+
assuranceGoal: loaded.workspace?.assuranceGoal ?? null,
|
|
16
|
+
candidateCoverage: loaded.workspace?.candidateCoverage ?? null,
|
|
17
|
+
systemIds: [...(loaded.workspace?.systemIds || [])].sort(),
|
|
18
|
+
frameworkIds: [...(loaded.workspace?.frameworkIds || [])].sort(),
|
|
19
|
+
requirementIds: [...(loaded.workspace?.requirementIds || [])].sort(),
|
|
20
|
+
controlIds: [...(loaded.workspace?.controlIds || [])].sort()
|
|
21
|
+
};
|
|
22
|
+
return createHash("sha256")
|
|
23
|
+
.update(JSON.stringify({ resourceType, records, workspaceScope }))
|
|
24
|
+
.digest("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function assessCollectionReviews(input) {
|
|
28
|
+
const loaded = input?.resources && input?.model && input?.entries
|
|
29
|
+
? input
|
|
30
|
+
: null;
|
|
31
|
+
if (!loaded) throw new Error("Collection review assessment requires a loaded workspace.");
|
|
32
|
+
return Object.keys(loaded.model.collectionReviews || {})
|
|
33
|
+
.map((resourceType) => assessCollectionReview(loaded, resourceType));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assessCollectionReview(loaded, resourceType) {
|
|
37
|
+
const configuration = loaded.model.collectionReviews?.[resourceType];
|
|
38
|
+
if (!configuration) return null;
|
|
39
|
+
const records = loaded.resources.filter((record) => record.type === resourceType);
|
|
40
|
+
const reviewEntry = loaded.entries.find(({ record }) => (
|
|
41
|
+
record.type === "collection-review"
|
|
42
|
+
&& record.resourceType === resourceType
|
|
43
|
+
&& record.status !== "retired"
|
|
44
|
+
));
|
|
45
|
+
const review = reviewEntry?.record || null;
|
|
46
|
+
const currentRevision = collectionRevision(loaded, resourceType);
|
|
47
|
+
const allowedDecisions = configuration.decisions || ["complete"];
|
|
48
|
+
const complete = Boolean(
|
|
49
|
+
review?.status === "active"
|
|
50
|
+
&& allowedDecisions.includes(review.decision)
|
|
51
|
+
&& review.collectionRevision === currentRevision
|
|
52
|
+
);
|
|
53
|
+
const stale = Boolean(
|
|
54
|
+
review?.status === "active"
|
|
55
|
+
&& review.collectionRevision
|
|
56
|
+
&& review.collectionRevision !== currentRevision
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
resourceType,
|
|
60
|
+
configuration,
|
|
61
|
+
records,
|
|
62
|
+
recordCount: records.length,
|
|
63
|
+
review,
|
|
64
|
+
reviewRevision: reviewEntry
|
|
65
|
+
? createHash("sha256").update(reviewEntry.source).digest("hex")
|
|
66
|
+
: null,
|
|
67
|
+
collectionRevision: currentRevision,
|
|
68
|
+
status: complete ? "current" : stale ? "stale" : "review-required",
|
|
69
|
+
complete,
|
|
70
|
+
message: complete
|
|
71
|
+
? `${configuration.title} were reviewed on ${review.reviewedOn}.`
|
|
72
|
+
: stale
|
|
73
|
+
? `${configuration.title} changed after the last confirmation. Review the current records again.`
|
|
74
|
+
: `Review ${configuration.title.toLowerCase()} before this page can be ready.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function scaffoldCollectionReview(input = process.cwd(), options = {}) {
|
|
79
|
+
const loaded = await loadWorkspace(input);
|
|
80
|
+
const resourceType = requiredType(loaded, options.resourceType);
|
|
81
|
+
const assessment = assessCollectionReview(loaded, resourceType);
|
|
82
|
+
const allowedDecisions = assessment.configuration.decisions || ["complete"];
|
|
83
|
+
return {
|
|
84
|
+
resourceType,
|
|
85
|
+
decision: assessment.records.length
|
|
86
|
+
? "complete"
|
|
87
|
+
: allowedDecisions.includes("zero-population") ? "zero-population" : null,
|
|
88
|
+
rationale: null,
|
|
89
|
+
reviewedByIds: [],
|
|
90
|
+
reviewedOn: null,
|
|
91
|
+
authoritativeSystemId: null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function planCollectionReview(input = process.cwd(), options = {}) {
|
|
96
|
+
const loaded = await loadWorkspace(input);
|
|
97
|
+
const resourceType = requiredType(loaded, options.resourceType);
|
|
98
|
+
const assessment = assessCollectionReview(loaded, resourceType);
|
|
99
|
+
const configuration = assessment.configuration;
|
|
100
|
+
const decision = String(options.decision || "").trim();
|
|
101
|
+
const rationale = String(options.rationale || "").trim();
|
|
102
|
+
const reviewedByIds = [...new Set((options.reviewedByIds || []).map(String).filter(Boolean))];
|
|
103
|
+
const reviewedOn = String(options.reviewedOn || "").trim();
|
|
104
|
+
const scopeRevision = String(options.scopeRevision || getGitSummary(loaded.root).commit || "uncommitted").trim();
|
|
105
|
+
const authoritativeSystemId = String(options.authoritativeSystemId || "").trim();
|
|
106
|
+
if (!(configuration.decisions || ["complete"]).includes(decision)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${configuration.title} review must use one of: ${(configuration.decisions || ["complete"]).join(", ")}.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (!assessment.records.length && decision === "complete") {
|
|
112
|
+
throw new Error(`${configuration.title} has no records. Use zero-population or another allowed conclusion.`);
|
|
113
|
+
}
|
|
114
|
+
if (assessment.records.length && decision === "zero-population") {
|
|
115
|
+
throw new Error(`${configuration.title} has ${assessment.records.length} records and cannot be confirmed as a zero population.`);
|
|
116
|
+
}
|
|
117
|
+
if (!rationale || !reviewedByIds.length || !reviewedOn) {
|
|
118
|
+
throw new Error(`${configuration.title} review needs review notes, a reviewer, and a review date.`);
|
|
119
|
+
}
|
|
120
|
+
if (decision === "externally-managed") {
|
|
121
|
+
const system = loaded.resources.find((record) => (
|
|
122
|
+
record.type === "system"
|
|
123
|
+
&& record.id === authoritativeSystemId
|
|
124
|
+
&& record.status === "active"
|
|
125
|
+
));
|
|
126
|
+
if (!system) throw new Error(`${configuration.title} review needs an active authoritative System.`);
|
|
127
|
+
}
|
|
128
|
+
const existing = assessment.review;
|
|
129
|
+
const record = {
|
|
130
|
+
...(existing || {
|
|
131
|
+
id: `collection-review-${resourceType}`,
|
|
132
|
+
type: "collection-review",
|
|
133
|
+
title: `${configuration.title} review`,
|
|
134
|
+
resourceType,
|
|
135
|
+
scopeResourceIds: [loaded.workspace.id]
|
|
136
|
+
}),
|
|
137
|
+
status: "active",
|
|
138
|
+
decision,
|
|
139
|
+
rationale,
|
|
140
|
+
reviewedByIds,
|
|
141
|
+
reviewedOn,
|
|
142
|
+
collectionRevision: assessment.collectionRevision,
|
|
143
|
+
scopeRevision,
|
|
144
|
+
...(decision === "externally-managed" ? { authoritativeSystemId } : {})
|
|
145
|
+
};
|
|
146
|
+
if (decision !== "externally-managed") delete record.authoritativeSystemId;
|
|
147
|
+
return {
|
|
148
|
+
operation: "collection-review",
|
|
149
|
+
resourceType,
|
|
150
|
+
assessment,
|
|
151
|
+
changes: {
|
|
152
|
+
...(existing ? { update: [record] } : { create: [record] }),
|
|
153
|
+
...(existing ? {
|
|
154
|
+
expectedRevisions: {
|
|
155
|
+
[existing.id]: options.expectedRevision || assessment.reviewRevision
|
|
156
|
+
}
|
|
157
|
+
} : {}),
|
|
158
|
+
validateWholeWorkspace: true
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function applyCollectionReview(input = process.cwd(), options = {}) {
|
|
164
|
+
if (options.confirmed !== true) {
|
|
165
|
+
throw new Error("Preview the collection review and confirm the write.");
|
|
166
|
+
}
|
|
167
|
+
const plan = await planCollectionReview(input, options);
|
|
168
|
+
const result = await applyResourceBatch(input, plan.changes);
|
|
169
|
+
const loaded = await loadWorkspace(input);
|
|
170
|
+
return {
|
|
171
|
+
...plan,
|
|
172
|
+
result,
|
|
173
|
+
assessment: assessCollectionReview(loaded, plan.resourceType)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requiredType(loaded, value) {
|
|
178
|
+
const resourceType = String(value || "").trim();
|
|
179
|
+
if (!loaded.model.collectionReviews?.[resourceType]) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Collection review type must be one of: ${Object.keys(loaded.model.collectionReviews || {}).join(", ")}.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return resourceType;
|
|
185
|
+
}
|
package/src/coverage.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function coverageBounds(coverage) {
|
|
2
|
+
if (coverage?.kind === "as-of" && typeof coverage.on === "string") {
|
|
3
|
+
return { start: coverage.on, end: coverage.on };
|
|
4
|
+
}
|
|
5
|
+
if (
|
|
6
|
+
coverage?.kind === "range"
|
|
7
|
+
&& typeof coverage.startsOn === "string"
|
|
8
|
+
&& typeof coverage.endsOn === "string"
|
|
9
|
+
) {
|
|
10
|
+
return { start: coverage.startsOn, end: coverage.endsOn };
|
|
11
|
+
}
|
|
12
|
+
return { start: null, end: null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function coverageStart(coverage) {
|
|
16
|
+
return coverageBounds(coverage).start;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function coverageEnd(coverage) {
|
|
20
|
+
return coverageBounds(coverage).end;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function coverageMatches(coverage, start, end = start) {
|
|
24
|
+
const bounds = coverageBounds(coverage);
|
|
25
|
+
return bounds.start === start && bounds.end === end;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function coverageOverlaps(coverage, start, end = start) {
|
|
29
|
+
const bounds = coverageBounds(coverage);
|
|
30
|
+
return Boolean(bounds.start && bounds.end && bounds.start <= end && bounds.end >= start);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function coverageContains(coverage, date) {
|
|
34
|
+
return coverageOverlaps(coverage, date, date);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function coverageLabel(coverage) {
|
|
38
|
+
const bounds = coverageBounds(coverage);
|
|
39
|
+
if (!bounds.start || !bounds.end) return "";
|
|
40
|
+
return bounds.start === bounds.end ? bounds.start : `${bounds.start} through ${bounds.end}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function legacyCoverage(record, options = {}) {
|
|
44
|
+
const asOf = options.asOfFields?.map((field) => record[field]).find(Boolean);
|
|
45
|
+
if (asOf) return { kind: "as-of", on: asOf };
|
|
46
|
+
const start = options.startFields?.map((field) => record[field]).find(Boolean);
|
|
47
|
+
const end = options.endFields?.map((field) => record[field]).find(Boolean);
|
|
48
|
+
if (start && end) return { kind: "range", startsOn: start, endsOn: end };
|
|
49
|
+
return null;
|
|
50
|
+
}
|