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.
@@ -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
+ }
@@ -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
+ }