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.
package/src/server.js CHANGED
@@ -5,6 +5,7 @@ import { extname, join, resolve } from "node:path";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { getResourceDefinition } from "../model/index.js";
7
7
  import { prepareAuditWorkspace } from "./audit-preparation.js";
8
+ import { saveAuditPopulation, scaffoldAuditPopulationCorrection } from "./audit-populations.js";
8
9
  import { activateDocuments, activateGovernedContent } from "./document-activation.js";
9
10
  import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
10
11
  import { applyApplicabilityReviewWithContext, planApplicabilityReview } from "./batch-review.js";
@@ -33,11 +34,22 @@ import {
33
34
  } from "./git.js";
34
35
  import { normalizeResourceMutation, serializeWorkspaceMutation } from "./mutation.js";
35
36
  import {
37
+ approveReportingRouteSet,
38
+ assessReportingRouteSets,
39
+ cancelReportingRouteSet,
40
+ proposeReportingRouteSet,
41
+ scaffoldReportingRouteSet
42
+ } from "./reporting-route-sets.js";
43
+ import {
44
+ activateObligationRule,
36
45
  completeObligationAction,
37
46
  completeObligationEvent,
38
47
  completeObligationOccurrence,
39
48
  createObligationEvent,
40
- planObligations
49
+ planObligations,
50
+ saveObligationOccurrence,
51
+ scaffoldObligationOccurrence,
52
+ scaffoldObligationRuleActivation
41
53
  } from "./obligations.js";
42
54
  import {
43
55
  planExternalReviewerGovernance,
@@ -80,6 +92,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
80
92
  return json(response, 403, { error: "The request host is not allowed." });
81
93
  }
82
94
  const url = new URL(request.url, "http://localhost");
95
+ const requestOptions = { ...options, programId: url.searchParams.get("programId") || undefined };
83
96
  if (["POST", "PUT", "DELETE"].includes(request.method) && !sameOrigin(request)) {
84
97
  return json(response, 403, { error: "Cross-origin writes are not allowed." });
85
98
  }
@@ -87,6 +100,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
87
100
  if (timingEnabled()) {
88
101
  const { result, timings } = await collectTimings(() => measureTiming("state", () => createAppState(input, {
89
102
  allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites,
103
+ programId: requestOptions.programId,
90
104
  includeDetails: false
91
105
  })));
92
106
  console.error(`[filegrc timing] ${JSON.stringify({ operation: "state", ...timings })}`);
@@ -94,6 +108,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
94
108
  }
95
109
  return json(response, 200, await createAppState(input, {
96
110
  allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites,
111
+ programId: requestOptions.programId,
97
112
  includeDetails: false
98
113
  }));
99
114
  }
@@ -107,7 +122,10 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
107
122
  };
108
123
  stateSessions.set(token, session);
109
124
  while (stateSessions.size > 8) stateSessions.delete(stateSessions.keys().next().value);
110
- const state = await createAppBootstrap(loaded, { generatedAt: session.generatedAt });
125
+ const state = await createAppBootstrap(loaded, {
126
+ generatedAt: session.generatedAt,
127
+ programId: url.searchParams.get("programId") || undefined
128
+ });
111
129
  state.stateToken = token;
112
130
  return json(response, 200, state);
113
131
  }
@@ -119,7 +137,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
119
137
  const token = url.searchParams.get("token");
120
138
  const session = stateSessions.get(token);
121
139
  if (!session) return json(response, 409, { error: "The workspace state expired. Reload it and try again." });
122
- const state = await loadStateSessionSection(session, section, options);
140
+ const state = await loadStateSessionSection(session, section, options, url.searchParams.get("programId") || undefined);
123
141
  return json(response, 200, { stateToken: token, section, state });
124
142
  }
125
143
  if (request.method === "GET" && url.pathname === "/api/history") {
@@ -130,6 +148,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
130
148
  if (request.method === "GET" && url.pathname === "/api/obligations") {
131
149
  const loaded = await loadWorkspace(input);
132
150
  return json(response, 200, planObligations(loaded.resources, {
151
+ programId: url.searchParams.get("programId") || undefined,
133
152
  asOf: url.searchParams.get("asOf") || undefined,
134
153
  from: url.searchParams.get("from") || undefined,
135
154
  through: url.searchParams.get("through") || undefined,
@@ -163,10 +182,11 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
163
182
  }
164
183
  if (request.method === "POST" && url.pathname === "/api/reconciliation") {
165
184
  const payload = await readJson(request);
166
- return json(response, 201, await browserMutation(input, options, {
185
+ return json(response, 201, await browserMutation(input, requestOptions, {
167
186
  message: (result) => `Reconcile policy event: ${result.event?.title || payload.candidateId}`
168
187
  }, () => applyReconciliation(input, {
169
188
  ...payload,
189
+ programId: payload.programId || requestOptions.programId,
170
190
  confirmed: payload.confirmed === true
171
191
  })));
172
192
  }
@@ -175,7 +195,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
175
195
  }
176
196
  if (request.method === "POST" && url.pathname === "/api/external-reviewer-governance") {
177
197
  const payload = await readJson(request);
178
- return json(response, 201, await browserMutation(input, options, {
198
+ return json(response, 201, await browserMutation(input, requestOptions, {
179
199
  message: (result) => `Assign external independent reviewer: ${result.reviewerId}`
180
200
  }, () => setupExternalReviewerGovernance(input, {
181
201
  ...payload,
@@ -187,7 +207,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
187
207
  }
188
208
  if (request.method === "POST" && url.pathname === "/api/audit-cycle") {
189
209
  const payload = await readJson(request);
190
- return json(response, 201, await browserMutation(input, options, {
210
+ return json(response, 201, await browserMutation(input, requestOptions, {
191
211
  message: (result) => `Create next audit cycle: ${result.audit?.title || payload.priorAuditId}`
192
212
  }, () => createNextAuditCycle(input, {
193
213
  ...payload,
@@ -195,48 +215,88 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
195
215
  })));
196
216
  }
197
217
  if (request.method === "POST" && url.pathname === "/api/applicability-review/preview") {
198
- return json(response, 200, await planApplicabilityReview(input, await readJson(request)));
218
+ const payload = await readJson(request);
219
+ return json(response, 200, await planApplicabilityReview(input, {
220
+ ...payload,
221
+ programId: url.searchParams.get("programId") || payload.programId
222
+ }));
199
223
  }
200
224
  if (request.method === "POST" && url.pathname === "/api/applicability-review") {
201
225
  const payload = await readJson(request);
202
226
  const { prefetchToken, ...reviewPayload } = payload;
203
- return json(response, 201, await browserMutation(input, options, {
227
+ return json(response, 201, await browserMutation(input, requestOptions, {
204
228
  message: (result) => `Record ${result.reviewedIds?.length || 0} applicability decisions`,
205
229
  fastResponse: prefersFastMutation(request),
206
230
  prefetchToken
207
231
  }, (_root, mutationContext) => applyApplicabilityReviewWithContext(input, {
208
232
  ...reviewPayload,
233
+ programId: url.searchParams.get("programId") || reviewPayload.programId,
209
234
  confirmed: payload.confirmed === true
210
235
  }, {
211
236
  repositorySnapshot: mutationContext?.repositorySnapshot
212
237
  })));
213
238
  }
214
239
  if (request.method === "POST" && url.pathname === "/api/collection-review/preview") {
215
- return json(response, 200, await planCollectionReview(input, await readJson(request)));
240
+ const payload = await readJson(request);
241
+ return json(response, 200, await planCollectionReview(input, {
242
+ ...payload,
243
+ programId: url.searchParams.get("programId") || payload.programId
244
+ }));
216
245
  }
217
246
  if (request.method === "POST" && url.pathname === "/api/collection-review") {
218
247
  const payload = await readJson(request);
219
248
  const { prefetchToken, ...reviewPayload } = payload;
220
- return json(response, 201, await browserMutation(input, options, {
249
+ return json(response, 201, await browserMutation(input, requestOptions, {
221
250
  message: (result) => `Confirm ${result.assessment?.configuration?.title || payload.resourceType}`,
222
251
  fastResponse: prefersFastMutation(request),
223
252
  prefetchToken
224
253
  }, (_root, mutationContext) => applyCollectionReview(input, {
225
254
  ...reviewPayload,
255
+ programId: url.searchParams.get("programId") || reviewPayload.programId,
226
256
  scopeRevision: mutationContext?.repositorySnapshot?.currentCommit,
227
257
  confirmed: payload.confirmed === true
228
258
  })));
229
259
  }
230
260
  if (request.method === "POST" && url.pathname === "/api/obligation-events") {
231
261
  const payload = await readJson(request);
232
- return json(response, 201, await browserMutation(input, options, {
262
+ return json(response, 201, await browserMutation(input, requestOptions, {
233
263
  message: (result) => `Create policy event: ${result.event?.title || payload.title || payload.eventType}`
234
- }, () => createObligationEvent(input, payload)));
264
+ }, () => createObligationEvent(input, { ...payload, programId: payload.programId || requestOptions.programId })));
265
+ }
266
+ if (request.method === "GET" && url.pathname === "/api/reporting-route-sets") {
267
+ return json(response, 200, await assessReportingRouteSets(input, {
268
+ programId: url.searchParams.get("programId") || requestOptions.programId,
269
+ at: url.searchParams.get("at") || undefined
270
+ }));
271
+ }
272
+ if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/scaffold") {
273
+ return json(response, 200, scaffoldReportingRouteSet(await readJson(request)));
274
+ }
275
+ if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/propose") {
276
+ const payload = await readJson(request);
277
+ if (!safeSegment(payload.routeSetId)) return json(response, 400, { error: "A safe Reporting Route Set ID is required." });
278
+ return json(response, 200, await browserMutation(input, requestOptions, {
279
+ message: () => `Propose reporting route set: ${payload.routeSetId}`
280
+ }, () => proposeReportingRouteSet(input, payload)));
281
+ }
282
+ if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/approve") {
283
+ const payload = await readJson(request);
284
+ if (!safeSegment(payload.routeSetId)) return json(response, 400, { error: "A safe Reporting Route Set ID is required." });
285
+ return json(response, 200, await browserMutation(input, requestOptions, {
286
+ message: () => `Approve reporting route set: ${payload.routeSetId}`
287
+ }, () => approveReportingRouteSet(input, payload)));
288
+ }
289
+ if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/cancel") {
290
+ const payload = await readJson(request);
291
+ if (!safeSegment(payload.routeSetId)) return json(response, 400, { error: "A safe Reporting Route Set ID is required." });
292
+ return json(response, 200, await browserMutation(input, requestOptions, {
293
+ message: () => `Cancel reporting route set: ${payload.routeSetId}`
294
+ }, () => cancelReportingRouteSet(input, payload)));
235
295
  }
236
296
  if (request.method === "POST" && url.pathname === "/api/obligation-completions") {
237
297
  const payload = await readJson(request);
238
298
  if (!safeSegment(payload.obligationId)) return json(response, 400, { error: "A safe obligation ID is required." });
239
- const result = await browserMutation(input, options, {
299
+ const result = await browserMutation(input, requestOptions, {
240
300
  message: () => `Complete ${resourceTypeLabel(payload.record?.type)}: ${payload.record?.title || payload.obligationId}`
241
301
  }, () => completeObligationOccurrence(input, {
242
302
  obligationId: payload.obligationId,
@@ -246,12 +306,59 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
246
306
  }));
247
307
  return json(response, 201, result);
248
308
  }
309
+ if (request.method === "POST" && url.pathname === "/api/obligation-occurrences/scaffold") {
310
+ const payload = await readJson(request);
311
+ if (!safeSegment(payload.obligationId)) return json(response, 400, { error: "A safe Obligation ID is required." });
312
+ return json(response, 200, await scaffoldObligationOccurrence(input, { ...payload, programId: payload.programId || requestOptions.programId }));
313
+ }
314
+ if (request.method === "POST" && url.pathname === "/api/audit-population-corrections/scaffold") {
315
+ const payload = await readJson(request);
316
+ if (!safeSegment(payload.populationId)) return json(response, 400, { error: "A safe Audit Population ID is required." });
317
+ return json(response, 200, await scaffoldAuditPopulationCorrection(input, payload));
318
+ }
319
+ if (request.method === "POST" && url.pathname === "/api/audit-population-corrections") {
320
+ const payload = await readJson(request);
321
+ if (!safeSegment(payload.record?.id)) return json(response, 400, { error: "A safe Audit Population correction ID is required." });
322
+ return json(response, 201, await browserMutation(input, requestOptions, {
323
+ message: () => `Correct audit population: ${payload.record.title || payload.record.id}`
324
+ }, () => saveAuditPopulation(input, {
325
+ record: payload.record,
326
+ content: payload.content,
327
+ expectedRevision: requireRevision(payload.revision, `audit-population/${payload.record.supersedesId}`)
328
+ })));
329
+ }
330
+ if (request.method === "POST" && url.pathname === "/api/obligation-occurrences") {
331
+ const payload = await readJson(request);
332
+ if (!safeSegment(payload.record?.id)) return json(response, 400, { error: "A safe occurrence ID is required." });
333
+ return json(response, 201, await browserMutation(input, requestOptions, {
334
+ message: () => `Save obligation occurrence: ${payload.record.title || payload.record.id}`
335
+ }, () => saveObligationOccurrence(input, {
336
+ record: payload.record,
337
+ programId: payload.programId || requestOptions.programId,
338
+ content: payload.content,
339
+ expectedRevision: payload.record?.supersedesId
340
+ ? requireRevision(payload.revision, `obligation-occurrence/${payload.record.supersedesId}`)
341
+ : payload.revision
342
+ })));
343
+ }
344
+ if (request.method === "POST" && url.pathname === "/api/obligation-rule-activations/scaffold") {
345
+ const payload = await readJson(request);
346
+ if (!safeSegment(payload.ruleId)) return json(response, 400, { error: "A safe Obligation Rule ID is required." });
347
+ return json(response, 200, await scaffoldObligationRuleActivation(input, payload));
348
+ }
349
+ if (request.method === "POST" && url.pathname === "/api/obligation-rule-activations") {
350
+ const payload = await readJson(request);
351
+ if (!safeSegment(payload.ruleId)) return json(response, 400, { error: "A safe Obligation Rule ID is required." });
352
+ return json(response, 201, await browserMutation(input, requestOptions, {
353
+ message: () => `Activate obligation rule: ${payload.ruleId}`
354
+ }, () => activateObligationRule(input, payload)));
355
+ }
249
356
  if (request.method === "POST" && url.pathname === "/api/action-completions") {
250
357
  const payload = await readJson(request);
251
358
  if (!safeSegment(payload.actionItemId)) {
252
359
  return json(response, 400, { error: "A safe Action Item ID is required." });
253
360
  }
254
- const result = await browserMutation(input, options, {
361
+ const result = await browserMutation(input, requestOptions, {
255
362
  message: () => `Complete action item: ${payload.actionItemId}`
256
363
  }, () => completeObligationAction(input, {
257
364
  actionItemId: payload.actionItemId,
@@ -267,10 +374,11 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
267
374
  if (!safeSegment(payload.eventId)) {
268
375
  return json(response, 400, { error: "A safe Policy Event ID is required." });
269
376
  }
270
- const result = await browserMutation(input, options, {
377
+ const result = await browserMutation(input, requestOptions, {
271
378
  message: () => `Complete policy event: ${payload.eventId}`
272
379
  }, () => completeObligationEvent(input, {
273
380
  eventId: payload.eventId,
381
+ programId: payload.programId || requestOptions.programId,
274
382
  completedOn: payload.completedOn,
275
383
  expectedRevision: requireRevision(payload.revision, `obligation-event/${payload.eventId}`)
276
384
  }));
@@ -288,7 +396,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
288
396
  try {
289
397
  const temporaryPath = join(temporaryDirectory, attachment);
290
398
  await writeFile(temporaryPath, await readBytes(request));
291
- const result = await browserMutation(input, options, {
399
+ const result = await browserMutation(input, requestOptions, {
292
400
  message: () => `Attach evidence file: ${attachment}`
293
401
  }, () => addEvidenceAttachment(input, evidenceId, temporaryPath, {
294
402
  name: attachment,
@@ -306,7 +414,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
306
414
  return json(response, 400, { error: "Safe Evidence and attachment identifiers are required." });
307
415
  }
308
416
  const revision = requireRevision(url.searchParams.get("revision"), `evidence/${evidenceId}`);
309
- const result = await browserMutation(input, options, {
417
+ const result = await browserMutation(input, requestOptions, {
310
418
  message: () => `Remove evidence file: ${attachment}`
311
419
  }, () => removeEvidenceAttachment(input, evidenceId, attachment, {
312
420
  expectedRevision: revision
@@ -317,7 +425,8 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
317
425
  return json(response, 200, await prepareEvidencePacket(input, {
318
426
  start: url.searchParams.get("start"),
319
427
  end: url.searchParams.get("end"),
320
- auditId: url.searchParams.get("auditId") || undefined
428
+ auditId: url.searchParams.get("auditId") || undefined,
429
+ programId: url.searchParams.get("programId") || undefined
321
430
  }));
322
431
  }
323
432
  if (request.method === "POST" && url.pathname === "/api/evidence-packet") {
@@ -339,14 +448,14 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
339
448
  }
340
449
  if (request.method === "POST" && url.pathname === "/api/audit-preparation") {
341
450
  const payload = await readJson(request);
342
- return json(response, 201, await browserMutation(input, options, {
451
+ return json(response, 201, await browserMutation(input, requestOptions, {
343
452
  message: () => `Prepare audit: ${payload.auditId || "engagement"}`
344
453
  }, () => prepareAuditWorkspace(input, payload)));
345
454
  }
346
455
  if (request.method === "POST" && url.pathname === "/api/setup") {
347
456
  const payload = await readJson(request);
348
457
  const completeSetup = async () => {
349
- return browserMutation(input, options, {
458
+ return browserMutation(input, requestOptions, {
350
459
  message: (setupResult) => `${payload.draft === true ? "Save onboarding draft" : "Complete onboarding"} for ${setupResult.workspace.organizationName}`,
351
460
  fastResponse: prefersFastMutation(request)
352
461
  }, () => setupWorkspace(input, payload));
@@ -355,21 +464,21 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
355
464
  }
356
465
  if (request.method === "POST" && url.pathname === "/api/policy-activations") {
357
466
  const payload = await readJson(request);
358
- const result = await browserMutation(input, options, {
467
+ const result = await browserMutation(input, requestOptions, {
359
468
  message: (activation) => `Activate ${activation.policyIds.length} ${activation.policyIds.length === 1 ? "Policy" : "Policies"}`
360
469
  }, () => activatePolicies(input, { ...payload, confirmed: true }));
361
470
  return json(response, 200, result);
362
471
  }
363
472
  if (request.method === "POST" && url.pathname === "/api/document-activations") {
364
473
  const payload = await readJson(request);
365
- const result = await browserMutation(input, options, {
474
+ const result = await browserMutation(input, requestOptions, {
366
475
  message: (activation) => `Activate ${activation.documentIds.length} governed ${activation.documentIds.length === 1 ? "Document" : "Documents"}`
367
476
  }, () => activateDocuments(input, { ...payload, confirmed: true }));
368
477
  return json(response, 200, result);
369
478
  }
370
479
  if (request.method === "POST" && url.pathname === "/api/governed-content-activations") {
371
480
  const payload = await readJson(request);
372
- const result = await browserMutation(input, options, {
481
+ const result = await browserMutation(input, requestOptions, {
373
482
  message: (activation) => `Activate ${activation.resourceIds.length} governed-content ${activation.resourceIds.length === 1 ? "record" : "records"}`
374
483
  }, () => activateGovernedContent(input, { ...payload, confirmed: true }));
375
484
  return json(response, 200, result);
@@ -377,7 +486,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
377
486
  if (request.method === "POST" && url.pathname === "/api/resources") {
378
487
  const payload = normalizeResourceMutation(await readJson(request));
379
488
  const { record } = payload;
380
- const result = await browserMutation(input, options, {
489
+ const result = await browserMutation(input, requestOptions, {
381
490
  message: () => `Create ${resourceTypeLabel(record.type)}: ${record.title || record.id}`
382
491
  }, () => createResource(input, record, { content: payload.content }));
383
492
  return json(response, 201, result);
@@ -385,15 +494,15 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
385
494
  if (request.method === "POST" && url.pathname === "/api/commit") {
386
495
  await requireManualBrowserGit(input, options);
387
496
  const payload = await readJson(request);
388
- return json(response, 201, await manualGitResultWithState(input, options, () => commitAndPushWorkspace(input, payload.message)));
497
+ return json(response, 201, await manualGitResultWithState(input, requestOptions, () => commitAndPushWorkspace(input, payload.message)));
389
498
  }
390
499
  if (request.method === "POST" && url.pathname === "/api/git/pull") {
391
500
  await requireManualBrowserGit(input, options);
392
- return json(response, 200, await manualGitResultWithState(input, options, () => pullWorkspace(input)));
501
+ return json(response, 200, await manualGitResultWithState(input, requestOptions, () => pullWorkspace(input)));
393
502
  }
394
503
  if (request.method === "POST" && url.pathname === "/api/git/push") {
395
504
  await requireManualBrowserGit(input, options);
396
- return json(response, 200, await manualGitResultWithState(input, options, () => pushWorkspace(input)));
505
+ return json(response, 200, await manualGitResultWithState(input, requestOptions, () => pushWorkspace(input)));
397
506
  }
398
507
  if (request.method === "POST" && url.pathname === "/api/git/retry-sync") {
399
508
  const result = await retryBrowserSync(input, {
@@ -431,7 +540,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
431
540
  }
432
541
  if (request.method === "PUT" && url.pathname === "/api/content") {
433
542
  const payload = await readJson(request);
434
- const result = await browserMutation(input, options, {
543
+ const result = await browserMutation(input, requestOptions, {
435
544
  message: () => `Update content: ${payload.path}`
436
545
  }, () => updateContent(input, payload.path, payload.source, {
437
546
  expectedRevision: requireRevision(payload.revision, `content/${payload.path}`)
@@ -455,7 +564,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
455
564
  if (request.method === "PUT") {
456
565
  const payload = normalizeResourceMutation(await readJson(request), { requireRevision: true });
457
566
  const { record } = payload;
458
- const result = await browserMutation(input, options, {
567
+ const result = await browserMutation(input, requestOptions, {
459
568
  message: () => `Update ${resourceTypeLabel(type)}: ${record.title || id}`
460
569
  }, () => updateResource(input, type, id, record, {
461
570
  content: payload.content,
@@ -467,7 +576,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
467
576
  }
468
577
  if (request.method === "DELETE") {
469
578
  const revision = requireRevision(url.searchParams.get("revision"), `${type}/${id}`);
470
- const result = await browserMutation(input, options, {
579
+ const result = await browserMutation(input, requestOptions, {
471
580
  message: () => `Delete ${resourceTypeLabel(type)}: ${id}`
472
581
  }, () => deleteResource(input, type, id, { expectedRevision: revision }));
473
582
  return json(response, 200, {
@@ -575,16 +684,16 @@ function listen(server, port, host) {
575
684
  });
576
685
  }
577
686
 
578
- function browserMutation(input, options, mutationOptions, task) {
687
+ function browserMutation(input, requestOptions, mutationOptions, task) {
579
688
  const run = () => serializeWorkspaceMutation(input, async (root) => {
580
689
  const fastResponse = mutationOptions.fastResponse === true;
581
690
  const workflowBefore = fastResponse
582
691
  ? null
583
- : await measureTiming("workflow-before", () => assessWorkflow(root));
692
+ : await measureTiming("workflow-before", () => assessWorkflow(root, { programId: requestOptions.programId }));
584
693
  const result = await measureTiming("mutation", () => runBrowserMutation(root, {
585
694
  ...mutationOptions,
586
- allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites === true,
587
- backgroundPushDelayMs: options.backgroundPushDelayMs,
695
+ allowNonAuthoritativeWrites: requestOptions.allowNonAuthoritativeWrites === true,
696
+ backgroundPushDelayMs: requestOptions.backgroundPushDelayMs,
588
697
  includeValidationProof: !fastResponse
589
698
  }, task));
590
699
  if (fastResponse) {
@@ -594,7 +703,8 @@ function browserMutation(input, options, mutationOptions, task) {
594
703
  };
595
704
  }
596
705
  const state = await measureTiming("state", () => createAppState(root, {
597
- allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites,
706
+ allowNonAuthoritativeWrites: requestOptions.allowNonAuthoritativeWrites,
707
+ programId: requestOptions.programId,
598
708
  includeDetails: false,
599
709
  validationProof: result?.[BROWSER_VALIDATION]
600
710
  }));
@@ -633,17 +743,18 @@ export function reconcileMutationSynchronization(synchronization, repository) {
633
743
  };
634
744
  }
635
745
 
636
- function loadStateSessionSection(session, section, serverOptions) {
637
- if (session.promises.has(section)) return session.promises.get(section);
746
+ function loadStateSessionSection(session, section, serverOptions, programId) {
747
+ const cacheKey = `${section}:${programId || "default"}`;
748
+ if (session.promises.has(cacheKey)) return session.promises.get(cacheKey);
638
749
  const dependencies = section === "workflow"
639
750
  ? Promise.all([
640
- loadStateSessionSection(session, "repository", serverOptions),
641
- loadStateSessionSection(session, "program", serverOptions),
642
- loadStateSessionSection(session, "obligations", serverOptions),
643
- loadStateSessionSection(session, "audits", serverOptions)
751
+ loadStateSessionSection(session, "repository", serverOptions, programId),
752
+ loadStateSessionSection(session, "program", serverOptions, programId),
753
+ loadStateSessionSection(session, "obligations", serverOptions, programId),
754
+ loadStateSessionSection(session, "audits", serverOptions, programId)
644
755
  ])
645
756
  : section === "audits"
646
- ? Promise.all([loadStateSessionSection(session, "program", serverOptions)])
757
+ ? Promise.all([loadStateSessionSection(session, "program", serverOptions, programId)])
647
758
  : Promise.resolve([]);
648
759
  const promise = dependencies.then((results) => {
649
760
  const repository = section === "workflow" ? results[0] : null;
@@ -657,13 +768,14 @@ function loadStateSessionSection(session, section, serverOptions) {
657
768
  auditPreparations: audits?.auditPreparations,
658
769
  obligations: obligations?.obligations,
659
770
  git: repository?.git,
660
- validation: repository?.validation
771
+ validation: repository?.validation,
772
+ programId
661
773
  });
662
774
  }).catch((error) => {
663
- session.promises.delete(section);
775
+ session.promises.delete(cacheKey);
664
776
  throw error;
665
777
  });
666
- session.promises.set(section, promise);
778
+ session.promises.set(cacheKey, promise);
667
779
  return promise;
668
780
  }
669
781
 
@@ -682,10 +794,11 @@ async function requireManualBrowserGit(input, options) {
682
794
  }
683
795
  }
684
796
 
685
- async function manualGitResultWithState(input, options, task) {
797
+ async function manualGitResultWithState(input, requestOptions, task) {
686
798
  const result = await task();
687
799
  const state = await createAppState(input, {
688
- allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites,
800
+ allowNonAuthoritativeWrites: requestOptions.allowNonAuthoritativeWrites,
801
+ programId: requestOptions.programId,
689
802
  includeDetails: false
690
803
  });
691
804
  return { ...result, state };
package/src/state.js CHANGED
@@ -15,6 +15,7 @@ import { fingerprintWorkspace, validateWorkspace } from "./validate.js";
15
15
  import { assessWorkflow } from "./workflow.js";
16
16
  import { measureTiming } from "./timing.js";
17
17
  import { soc2RequirementApplicabilityConstraint } from "./soc2.js";
18
+ import { modelSupports } from "../model/index.js";
18
19
 
19
20
  const renderedMarkdownCache = new Map();
20
21
  const MAX_RENDERED_MARKDOWN_CACHE_ENTRIES = 1_000;
@@ -27,7 +28,8 @@ export async function createAppState(input = process.cwd(), options = {}) {
27
28
  options.allowNonAuthoritativeWrites === true,
28
29
  options.includeDetails !== false,
29
30
  options.asOf ?? null,
30
- options.now ?? null
31
+ options.now ?? null,
32
+ options.programId ?? null
31
33
  ]);
32
34
  if (!options.validationProof && appStatePromises.has(key)) return appStatePromises.get(key);
33
35
  const promise = serializeWorkspaceMutation(input, (root) => createAppStateUnlocked(root, options)).finally(() => {
@@ -48,6 +50,10 @@ export async function createAppBootstrap(input = process.cwd(), options = {}) {
48
50
  timezone: "UTC"
49
51
  };
50
52
  const generatedAt = options.generatedAt ?? new Date().toISOString();
53
+ const programs = loaded.resources.filter((record) => record.type === "program" && record.status !== "retired");
54
+ const selectedProgram = options.programId
55
+ ? resolveProgram(loaded, options.programId)
56
+ : programs[0] || null;
51
57
  return {
52
58
  generatedAt,
53
59
  asOf: options.asOf ?? currentCalendarDate(workspace.timezone),
@@ -60,6 +66,7 @@ export async function createAppBootstrap(input = process.cwd(), options = {}) {
60
66
  writesAllowed: false
61
67
  },
62
68
  workspace,
69
+ selectedProgramId: selectedProgram?.id || null,
63
70
  model: loaded.model,
64
71
  resources: loaded.entries.map((entry) => ({
65
72
  record: structuredClone(entry.record),
@@ -129,14 +136,14 @@ export async function createAppStateSection(input, section, options = {}) {
129
136
  };
130
137
  }
131
138
  if (section === "program") {
132
- const programReadiness = options.programReadiness ?? await assessProgramReadiness(loaded, { asOf, generatedAt });
133
- const activeProgram = resolveProgram(loaded);
139
+ const programReadiness = options.programReadiness ?? await assessProgramReadiness(loaded, { programId: options.programId, asOf, generatedAt });
140
+ const activeProgram = resolveProgram(loaded, options.programId);
134
141
  return {
135
142
  generatedAt,
136
143
  asOf,
137
144
  programReadiness,
138
145
  collectionReviews: Object.fromEntries(
139
- assessCollectionReviews(loaded).map((assessment) => [
146
+ assessCollectionReviews(loaded, { programId: activeProgram.id }).map((assessment) => [
140
147
  assessment.resourceType,
141
148
  {
142
149
  resourceType: assessment.resourceType,
@@ -158,10 +165,12 @@ export async function createAppStateSection(input, section, options = {}) {
158
165
  };
159
166
  }
160
167
  if (section === "obligations") {
168
+ const activeProgram = resolveProgram(loaded, options.programId);
161
169
  return {
162
170
  generatedAt,
163
171
  asOf,
164
172
  obligations: planObligations(loaded.resources, {
173
+ programId: activeProgram.id,
165
174
  asOf,
166
175
  now: options.now ?? generatedAt,
167
176
  model: loaded.model
@@ -170,7 +179,11 @@ export async function createAppStateSection(input, section, options = {}) {
170
179
  }
171
180
  if (section === "audits") {
172
181
  const programReadiness = options.programReadiness;
173
- const audits = loaded.resources.filter((record) => record.type === "audit");
182
+ const activeProgram = resolveProgram(loaded, options.programId);
183
+ const audits = loaded.resources.filter((record) => (
184
+ record.type === "audit"
185
+ && (!modelSupports(loaded.model, "program-scope") || record.programId === activeProgram.id)
186
+ ));
174
187
  return {
175
188
  generatedAt,
176
189
  asOf,
@@ -179,6 +192,7 @@ export async function createAppStateSection(input, section, options = {}) {
179
192
  audit?.id || "none",
180
193
  await assessAuditPreparation(loaded, {
181
194
  auditId: audit?.id,
195
+ programId: audit?.programId || options.programId,
182
196
  asOf,
183
197
  generatedAt,
184
198
  ...(audit || !programReadiness ? {} : { programReadiness })
@@ -192,6 +206,7 @@ export async function createAppStateSection(input, section, options = {}) {
192
206
  generatedAt,
193
207
  asOf,
194
208
  workflow: await assessWorkflow(loaded, {
209
+ programId: options.programId,
195
210
  asOf,
196
211
  evaluatedAt: generatedAt,
197
212
  programReadiness: options.programReadiness,
@@ -249,15 +264,19 @@ async function createAppStateUnlocked(input, options) {
249
264
  const asOf = options.asOf ?? currentCalendarDate(workspace.timezone);
250
265
  const generatedAt = new Date().toISOString();
251
266
  const programReadiness = await measureTiming("state-program-readiness", () => assessProgramReadiness(loaded, {
267
+ programId: options.programId,
252
268
  asOf,
253
269
  generatedAt
254
270
  }));
255
- const activeProgram = resolveProgram(loaded);
271
+ const activeProgram = resolveProgram(loaded, options.programId);
256
272
  const applicabilityConstraints = Object.fromEntries(loaded.resources.flatMap((record) => {
257
273
  const constraint = soc2RequirementApplicabilityConstraint(record, activeProgram, loaded.model.modelVersion);
258
274
  return constraint ? [[record.id, constraint]] : [];
259
275
  }));
260
- const audits = loaded.resources.filter((record) => record.type === "audit");
276
+ const audits = loaded.resources.filter((record) => (
277
+ record.type === "audit"
278
+ && (!modelSupports(loaded.model, "program-scope") || record.programId === activeProgram.id)
279
+ ));
261
280
  const auditPreparations = await measureTiming("state-audit-preparation", async () => Object.fromEntries(await Promise.all(
262
281
  (audits.length ? audits : [null]).map(async (audit) => {
263
282
  const preparation = await assessAuditPreparation(loaded, {
@@ -270,11 +289,13 @@ async function createAppStateUnlocked(input, options) {
270
289
  })
271
290
  )));
272
291
  const obligations = planObligations(entries, {
292
+ programId: activeProgram.id,
273
293
  asOf,
274
294
  now: options.now ?? generatedAt,
275
295
  model: loaded.model
276
296
  });
277
297
  const workflow = await measureTiming("state-workflow", () => assessWorkflow(loaded, {
298
+ programId: activeProgram.id,
278
299
  asOf,
279
300
  evaluatedAt: generatedAt,
280
301
  programReadiness,
@@ -286,7 +307,7 @@ async function createAppStateUnlocked(input, options) {
286
307
  validation
287
308
  }));
288
309
  const collectionReviews = Object.fromEntries(
289
- assessCollectionReviews(loaded).map((assessment) => [
310
+ assessCollectionReviews(loaded, { programId: activeProgram.id }).map((assessment) => [
290
311
  assessment.resourceType,
291
312
  {
292
313
  resourceType: assessment.resourceType,
@@ -307,6 +328,7 @@ async function createAppStateUnlocked(input, options) {
307
328
  readOnly: Boolean(options.readOnly || (repository.mode === "trunk" && !repository.writesAllowed)),
308
329
  repository,
309
330
  workspace,
331
+ selectedProgramId: activeProgram.id,
310
332
  model: loaded.model,
311
333
  resources: entries,
312
334
  validation: {