filegrc 0.11.0 → 0.12.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.
@@ -5,10 +5,12 @@ import {
5
5
  collectionRevisionMatches
6
6
  } from "./collection-revision.js";
7
7
  import { scopedCollectionRecords } from "./collection-scope.js";
8
- import { applyResourceBatch } from "./files.js";
8
+ import { applyResourceBatch, INTERNAL_WORKFLOW_CAPABILITIES } from "./files.js";
9
9
  import { getGitSummary } from "./git.js";
10
+ import { createResourceId } from "./id.js";
10
11
  import { loadWorkspace } from "./workspace.js";
11
12
  import { resolveProgram } from "./program.js";
13
+ import { currentCalendarDate } from "./time.js";
12
14
 
13
15
  export { collectionRevision };
14
16
 
@@ -41,6 +43,13 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
41
43
  authoritativeSourceId
42
44
  });
43
45
  const allowedDecisions = configuration.decisions || ["complete"];
46
+ const temporalReview = !modelSupports(loaded.model, "temporal-collection-reviews") || Boolean(
47
+ review?.coverage?.kind === "as-of"
48
+ && review.coverage.on === review.reviewedOn
49
+ && review.knowledgeCutoffAt
50
+ && Array.isArray(review.populationResourceIds)
51
+ && sameIds(review.populationResourceIds, records.map(({ id }) => id))
52
+ );
44
53
  const allowsEmptyCollection = allowedDecisions.some((decision) => (
45
54
  decision === "zero-population" || decision === "externally-managed"
46
55
  ));
@@ -58,6 +67,7 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
58
67
  review?.status === "active"
59
68
  && allowedDecisions.includes(review.decision)
60
69
  && revisionMatches
70
+ && temporalReview
61
71
  );
62
72
  const stale = Boolean(
63
73
  review?.status === "active"
@@ -116,7 +126,17 @@ export async function planCollectionReview(input = process.cwd(), options = {})
116
126
  const rationale = String(options.rationale || "").trim();
117
127
  const reviewedByIds = [...new Set((options.reviewedByIds || []).map(String).filter(Boolean))];
118
128
  const reviewedOn = String(options.reviewedOn || "").trim();
119
- const scopeRevision = String(options.scopeRevision || getGitSummary(loaded.root).commit || "uncommitted").trim();
129
+ const temporalReviews = modelSupports(loaded.model, "temporal-collection-reviews");
130
+ const now = options.now ? new Date(options.now) : new Date();
131
+ if (Number.isNaN(now.getTime())) throw new Error("A valid Collection Review time is required.");
132
+ const today = currentCalendarDate(loaded.workspace.timezone, now);
133
+ const gitSummary = getGitSummary(loaded.root);
134
+ if (temporalReviews && (!gitSummary.available || !gitSummary.clean || !gitSummary.commit)) {
135
+ throw new Error("Commit the current workspace before recording a Collection Review so its scope revision is retrievable from Git.");
136
+ }
137
+ const scopeRevision = temporalReviews
138
+ ? gitSummary.commit
139
+ : String(options.scopeRevision || gitSummary.commit || "uncommitted").trim();
120
140
  const v4 = modelSupports(loaded.model, "program-scope");
121
141
  const authoritativeSourceId = String(v4 ? options.authoritativeComponentId : options.authoritativeSystemId || "").trim();
122
142
  if (!(configuration.decisions || ["complete"]).includes(decision)) {
@@ -138,6 +158,14 @@ export async function planCollectionReview(input = process.cwd(), options = {})
138
158
  if (!rationale || !reviewedByIds.length || !reviewedOn) {
139
159
  throw new Error(`${configuration.title} review needs review notes, a reviewer, and a review date.`);
140
160
  }
161
+ if (temporalReviews) {
162
+ if (reviewedOn !== today) {
163
+ throw new Error("A current Collection Review must use today's workspace-local review date. Reconstruct an older population from Git history instead of backdating today's files.");
164
+ }
165
+ if (options.scopeRevision && options.scopeRevision !== scopeRevision) {
166
+ throw new Error("Collection Review scope revision is derived from Git and cannot be supplied.");
167
+ }
168
+ }
141
169
  if (decision === "externally-managed") {
142
170
  const system = loaded.resources.find((record) => (
143
171
  record.type === (v4 ? "component" : "system")
@@ -151,14 +179,29 @@ export async function planCollectionReview(input = process.cwd(), options = {})
151
179
  authoritativeSourceId: decision === "externally-managed" ? authoritativeSourceId : null
152
180
  });
153
181
  const existing = assessment.review;
182
+ const preservesReviewHistory = temporalReviews;
154
183
  const record = {
155
- ...(existing || {
156
- id: `collection-review-${resourceType}`,
184
+ ...(!existing ? {
185
+ id: modelSupports(loaded.model, "program-scope")
186
+ ? createResourceId(
187
+ "collection-review",
188
+ `${program.id} ${configuration.title} review`,
189
+ loaded.resources.map(({ id }) => id)
190
+ )
191
+ : `collection-review-${resourceType}`,
157
192
  type: "collection-review",
158
193
  title: `${configuration.title} review`,
159
194
  resourceType,
160
195
  scopeResourceIds: [program.id]
161
- }),
196
+ } : preservesReviewHistory ? {
197
+ ...existing,
198
+ id: createResourceId(
199
+ "collection-review",
200
+ `${configuration.title} review ${reviewedOn}`,
201
+ loaded.resources.map(({ id }) => id)
202
+ ),
203
+ supersedesId: existing.id
204
+ } : existing),
162
205
  status: "active",
163
206
  decision,
164
207
  rationale,
@@ -166,6 +209,11 @@ export async function planCollectionReview(input = process.cwd(), options = {})
166
209
  reviewedOn,
167
210
  collectionRevision: currentRevision,
168
211
  scopeRevision,
212
+ ...(temporalReviews ? {
213
+ coverage: { kind: "as-of", on: reviewedOn },
214
+ knowledgeCutoffAt: now.toISOString(),
215
+ populationResourceIds: assessment.records.map(({ id }) => id).sort()
216
+ } : {}),
169
217
  ...(decision === "externally-managed"
170
218
  ? { [v4 ? "authoritativeComponentId" : "authoritativeSystemId"]: authoritativeSourceId }
171
219
  : {})
@@ -179,17 +227,33 @@ export async function planCollectionReview(input = process.cwd(), options = {})
179
227
  resourceType,
180
228
  assessment,
181
229
  changes: {
182
- ...(existing ? { update: [record] } : { create: [record] }),
230
+ ...(!existing || preservesReviewHistory ? { create: [record] } : { update: [record] }),
231
+ ...(existing && preservesReviewHistory ? {
232
+ update: [{
233
+ ...existing,
234
+ status: "retired",
235
+ statusTransition: {
236
+ changedByIds: reviewedByIds,
237
+ changedOn: reviewedOn,
238
+ reason: `Superseded by ${record.id}.`
239
+ }
240
+ }]
241
+ } : {}),
183
242
  ...(existing ? {
184
243
  expectedRevisions: {
185
244
  [existing.id]: options.expectedRevision || assessment.reviewRevision
186
245
  }
187
246
  } : {}),
188
- validateWholeWorkspace: true
247
+ validateWholeWorkspace: true,
248
+ workflowCapability: INTERNAL_WORKFLOW_CAPABILITIES.collectionReviewReassessment
189
249
  }
190
250
  };
191
251
  }
192
252
 
253
+ function sameIds(left, right) {
254
+ return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
255
+ }
256
+
193
257
  export async function applyCollectionReview(input = process.cwd(), options = {}) {
194
258
  if (options.confirmed !== true) {
195
259
  throw new Error("Preview the collection review and confirm the write.");
@@ -55,6 +55,14 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
55
55
  ));
56
56
  }
57
57
 
58
+ export function selectScopedCollectionRecords(loaded, selector, program) {
59
+ if (!selector?.resourceType) return [];
60
+ return scopedCollectionRecords(loaded, selector.resourceType, program).filter((record) => (
61
+ (!selector.statuses?.length || selector.statuses.includes(record.status))
62
+ && (!selector.criticalities?.length || selector.criticalities.includes(record.criticality))
63
+ ));
64
+ }
65
+
58
66
  function scopedProgramPeople(loaded, program = {}) {
59
67
  const personIds = new Set(loaded.resources.filter(({ type }) => type === "person").map(({ id }) => id));
60
68
  const systemIds = new Set(program.systemIds || []);