@vercel/factory 0.0.9 → 0.0.11

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/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as repositoryIdsSchema, c as SCHEMA_VERSION, d as schemaVersionSchema, f as usdSchema, i as repositoryIdSchema, l as isoDateTimeSchema, n as factoryIdSchema, o as signalIdSchema, p as vocabularySchema, r as receiptIdSchema, s as taskIdSchema, t as changeIdSchema, u as replyToSchema } from "./id-DwCihQr4.mjs";
2
2
  import { a as findingCategorySchema, c as findingSchema, d as findingVerificationSchema, i as requiresApproval, l as findingVerificationLevelSchema, n as approvalRuleSchema, o as findingFingerprint, r as defaultApprovalPolicy, s as findingPrioritySchema, t as approvalPolicySchema, u as findingVerificationMethodSchema } from "./approval-DdvEKLiE.mjs";
3
3
  import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "./driver-Bbd25rNM.mjs";
4
- import { a as taskKindSchema, c as taskSchema, d as changeSchema, f as changeStateSchema, g as dispatchTask, h as DispatchRefusedError, i as taskDevboxSchema, l as taskStateSchema, m as DEFAULT_MAX_LAUNCH_ATTEMPTS, n as standardTaskKinds, o as taskOriginSchema, p as levelSchema, r as taskApprovalSchema, s as taskQuestionSchema, t as harnessSchema, u as changeEvidenceSchema } from "./task-DMHuvBI0.mjs";
4
+ import { _ as taskSchema, a as DEFAULT_MAX_LAUNCH_ATTEMPTS, c as coordinationRepositoryIdsSchema, d as taskApprovalSchema, f as taskDevboxSchema, g as taskQuestionSchema, h as taskOriginSchema, i as levelSchema, l as harnessSchema, m as taskKindSchema, n as changeSchema, o as DispatchRefusedError, p as taskExecutionRepositoryIdsSchema, r as changeStateSchema, s as dispatchTask, t as changeEvidenceSchema, u as standardTaskKinds, v as taskStateSchema } from "./change-BPYhWKuL.mjs";
5
5
  import { a as repositorySlugSchema, i as repositorySchema, n as factoryConfigRepositorySchema, r as factoryConfigSchema, t as defineFactory } from "./factory-config-ifd3JxiU.mjs";
6
6
  import { z } from "zod";
7
7
  import { createHash, randomUUID } from "node:crypto";
@@ -56,6 +56,238 @@ async function adoptPullRequest(stores, input) {
56
56
  });
57
57
  }
58
58
  //#endregion
59
+ //#region src/coordination.ts
60
+ /** Create one non-executable parent and one independently executable child per repository. */
61
+ async function createCoordination(stores, input) {
62
+ const repositoryIds = [...new Set(input.repositoryIds)];
63
+ if (repositoryIds.length < 2) throw new Error("coordination requires at least two distinct repositories");
64
+ const parent = await stores.tasks.create({
65
+ repositoryIds,
66
+ kind: "coordination",
67
+ origin: { signalId: input.signalId },
68
+ replyTo: input.replyTo
69
+ });
70
+ return {
71
+ parent: await stores.tasks.transition(parent.id, "running", { reason: "repository-scoped child tasks created" }),
72
+ children: await Promise.all(repositoryIds.map((repositoryId) => stores.tasks.create({
73
+ repositoryIds: [repositoryId],
74
+ kind: "code_change",
75
+ origin: { parentTaskId: parent.id },
76
+ replyTo: input.replyTo
77
+ })))
78
+ };
79
+ }
80
+ /** Reconcile a coordination parent from its direct children without ever dispatching the parent. */
81
+ async function reconcileCoordination(stores, parentTaskId, options) {
82
+ const parent = await stores.tasks.get(parentTaskId);
83
+ if (parent === null) throw new RecordNotFoundError("tasks", parentTaskId);
84
+ if (parent.kind !== "coordination") throw new Error(`task ${parentTaskId} is ${parent.kind}, not coordination`);
85
+ const children = (await stores.tasks.list()).filter((task) => task.origin.parentTaskId === parentTaskId);
86
+ if (children.length !== parent.repositoryIds.length) return {
87
+ status: "pending",
88
+ parent,
89
+ children
90
+ };
91
+ if (parent.state === "succeeded" || parent.state === "failed") return {
92
+ status: parent.state,
93
+ transitioned: false,
94
+ parent,
95
+ children
96
+ };
97
+ if (children.some((child) => child.state === "failed" || child.state === "cancelled")) {
98
+ await options.present({
99
+ status: "failed",
100
+ parent,
101
+ children
102
+ });
103
+ return {
104
+ status: "failed",
105
+ transitioned: true,
106
+ parent: await stores.tasks.transition(parent.id, "failed", { reason: "one or more repository tasks failed" }),
107
+ children
108
+ };
109
+ }
110
+ if (!children.every((child) => child.state === "succeeded")) return {
111
+ status: "pending",
112
+ parent,
113
+ children
114
+ };
115
+ await options.present({
116
+ status: "succeeded",
117
+ parent,
118
+ children
119
+ });
120
+ await stores.tasks.transition(parent.id, "verifying", { reason: "all repository tasks succeeded" });
121
+ return {
122
+ status: "succeeded",
123
+ transitioned: true,
124
+ parent: await stores.tasks.transition(parent.id, "succeeded", { reason: "aggregate multi-repository handoff is ready" }),
125
+ children
126
+ };
127
+ }
128
+ //#endregion
129
+ //#region src/metrics.ts
130
+ const bucketTable = {
131
+ low: {
132
+ low: "review",
133
+ medium: "stamp",
134
+ high: "stamp"
135
+ },
136
+ medium: {
137
+ low: "review",
138
+ medium: "review",
139
+ high: "stamp"
140
+ },
141
+ high: {
142
+ low: "blocked",
143
+ medium: "review",
144
+ high: "review"
145
+ }
146
+ };
147
+ function reviewBucket(risk, confidence) {
148
+ return bucketTable[risk][confidence];
149
+ }
150
+ function collectFacts(rows) {
151
+ const byChange = /* @__PURE__ */ new Map();
152
+ for (const receipt of rows.receipts) {
153
+ if (receipt.kind !== "change" || receipt.changeId === void 0) continue;
154
+ const list = byChange.get(receipt.changeId) ?? [];
155
+ list.push(receipt);
156
+ byChange.set(receipt.changeId, list);
157
+ }
158
+ const at = (receipts, state) => {
159
+ const found = receipts?.find((receipt) => receipt.state === state);
160
+ return found === void 0 ? null : Date.parse(found.recordedAt);
161
+ };
162
+ return rows.changes.map((change) => {
163
+ const receipts = byChange.get(change.id);
164
+ const requiredState = change.risk === "high" ? "rolled_out" : "deployed";
165
+ return {
166
+ change,
167
+ readyAt: at(receipts, "ready"),
168
+ mergedAt: at(receipts, "merged"),
169
+ reachedAt: at(receipts, requiredState),
170
+ rolledBackAt: at(receipts, "rolled_back")
171
+ };
172
+ });
173
+ }
174
+ /** True for a change a human saw as ready and then closed unmerged. */
175
+ function closedAfterReady(facts) {
176
+ return facts.change.state === "closed" && (facts.readyAt !== null || facts.change.decidedBy !== void 0);
177
+ }
178
+ function median(values) {
179
+ if (values.length === 0) return null;
180
+ const sorted = [...values].sort((a, b) => a - b);
181
+ const mid = Math.floor(sorted.length / 2);
182
+ return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
183
+ }
184
+ /**
185
+ * The six product metrics from ledger rows alone. One rule governs them: a
186
+ * change counts only after a human merged it and it stayed. Activity that
187
+ * never landed is not a result. Compute over rows already filtered to the
188
+ * period and repositories you are reporting on.
189
+ */
190
+ function computeLedgerMetrics(rows, options = {}) {
191
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
192
+ const holdMs = (options.holdDays ?? 7) * 24 * 60 * 60 * 1e3;
193
+ const facts = collectFacts(rows);
194
+ let landed = 0;
195
+ let provisional = 0;
196
+ let rolledBack = 0;
197
+ let closedAfterReadyCount = 0;
198
+ let closedInVerification = 0;
199
+ const landedChangeIds = /* @__PURE__ */ new Set();
200
+ for (const fact of facts) {
201
+ const { change } = fact;
202
+ if (fact.rolledBackAt !== null) {
203
+ rolledBack += 1;
204
+ continue;
205
+ }
206
+ if (change.state === "closed") {
207
+ if (closedAfterReady(fact)) closedAfterReadyCount += 1;
208
+ else closedInVerification += 1;
209
+ continue;
210
+ }
211
+ if (change.decidedBy !== void 0 && fact.reachedAt !== null) {
212
+ landed += 1;
213
+ landedChangeIds.add(change.id);
214
+ if (now - fact.reachedAt < holdMs) provisional += 1;
215
+ }
216
+ }
217
+ const stickDenominator = landed + rolledBack + closedAfterReadyCount;
218
+ const humanTimes = {
219
+ stamp: [],
220
+ review: []
221
+ };
222
+ for (const fact of facts) {
223
+ if (fact.readyAt === null || fact.mergedAt === null) continue;
224
+ const bucket = reviewBucket(fact.change.risk, fact.change.confidence);
225
+ if (bucket === "blocked") continue;
226
+ humanTimes[bucket].push(fact.mergedAt - fact.readyAt);
227
+ }
228
+ let settledOverLanded = 0;
229
+ let sawSettledCost = false;
230
+ for (const task of rows.tasks) {
231
+ if (task.settledUsd === void 0) continue;
232
+ if (task.kind === "code_change" && task.changeId !== void 0 && landedChangeIds.has(task.changeId)) {
233
+ settledOverLanded += task.settledUsd;
234
+ sawSettledCost = true;
235
+ }
236
+ }
237
+ const rejectedSignals = rows.signals.filter((signal) => signal.outcome !== "pending" && signal.outcome !== "accepted").length;
238
+ return {
239
+ landed,
240
+ provisional,
241
+ stickRate: stickDenominator === 0 ? null : landed / stickDenominator,
242
+ stampedClean: null,
243
+ humanTimeMs: {
244
+ stamp: median(humanTimes.stamp),
245
+ review: median(humanTimes.review)
246
+ },
247
+ costPerLandedUsd: sawSettledCost && landed > 0 ? settledOverLanded / landed : null,
248
+ filtered: rejectedSignals + closedInVerification
249
+ };
250
+ }
251
+ const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
252
+ /**
253
+ * Green, yellow, or red from rows, never from a model. Budget and onboarding
254
+ * signals are not evaluated yet: budgets do not settle and onboarding
255
+ * investigations are not recorded, so those spec rules have no rows to read.
256
+ */
257
+ function computeRepositoryHealth(rows, options = {}) {
258
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
259
+ const dayMs = 864e5;
260
+ const red = [];
261
+ const yellow = [];
262
+ for (const receipt of rows.receipts) {
263
+ if (now - Date.parse(receipt.recordedAt) >= dayMs) continue;
264
+ if (receipt.state === "rolled_back") red.push(`rollback in the last 24h (${receipt.receiptId})`);
265
+ if (receipt.state === "cancelled" && receipt.reason?.toLowerCase().includes("kill")) red.push(`kill in the last 24h (${receipt.receiptId})`);
266
+ }
267
+ const openQuestions = rows.tasks.filter((task) => task.kind === "question" && task.state === "needs_human").length;
268
+ if (openQuestions > 0) yellow.push(`${openQuestions} open question(s)`);
269
+ const needsHuman = rows.tasks.filter((task) => task.kind !== "question" && task.state === "needs_human").length;
270
+ if (needsHuman > 0) yellow.push(`${needsHuman} task(s) waiting on a human`);
271
+ const blocked = rows.changes.filter((change) => openChangeStates.has(change.state) && reviewBucket(change.risk, change.confidence) === "blocked").length;
272
+ if (blocked > 0) yellow.push(`${blocked} change(s) in the blocked bucket`);
273
+ if (options.maxOpenChanges !== void 0) {
274
+ const open = rows.changes.filter((change) => openChangeStates.has(change.state)).length;
275
+ if (open >= options.maxOpenChanges) yellow.push(`open changes at the cap (${open}/${options.maxOpenChanges})`);
276
+ }
277
+ if (red.length > 0) return {
278
+ color: "red",
279
+ reasons: red
280
+ };
281
+ if (yellow.length > 0) return {
282
+ color: "yellow",
283
+ reasons: yellow
284
+ };
285
+ return {
286
+ color: "green",
287
+ reasons: []
288
+ };
289
+ }
290
+ //#endregion
59
291
  //#region src/schema/budget.ts
60
292
  const budgetMonthSchema = z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/);
61
293
  const budgetSchema = z.strictObject({
@@ -86,6 +318,7 @@ const standardReceiptStates = [
86
318
  "needs_human",
87
319
  "cancelled",
88
320
  "settled",
321
+ "ready",
89
322
  "merged",
90
323
  "deployed",
91
324
  "rolled_out",
@@ -438,6 +671,18 @@ var ApprovalError = class extends Error {
438
671
  this.reason = reason;
439
672
  }
440
673
  };
674
+ var ChangeAssociationError = class extends Error {
675
+ taskId;
676
+ repositoryId;
677
+ reason;
678
+ constructor(taskId, repositoryId, reason) {
679
+ super(`change for task "${taskId}" refused: ${reason}`);
680
+ this.name = "ChangeAssociationError";
681
+ this.taskId = taskId;
682
+ this.repositoryId = repositoryId;
683
+ this.reason = reason;
684
+ }
685
+ };
441
686
  const taskReceiptStates = {
442
687
  running: "dispatched",
443
688
  succeeded: "succeeded",
@@ -446,6 +691,7 @@ const taskReceiptStates = {
446
691
  cancelled: "cancelled"
447
692
  };
448
693
  const changeReceiptStates = {
694
+ ready: "ready",
449
695
  merged: "merged",
450
696
  deployed: "deployed",
451
697
  rolled_out: "rolled_out",
@@ -688,6 +934,13 @@ function createStores(options) {
688
934
  createdAt: timestamp,
689
935
  updatedAt: timestamp
690
936
  });
937
+ await mustRead("repositories", change.repositoryId, repositorySchema);
938
+ if (change.taskId !== void 0) {
939
+ const task = (await mustRead("tasks", change.taskId, taskSchema)).value;
940
+ if (task.kind !== "code_change") throw new ChangeAssociationError(change.taskId, change.repositoryId, "changes can only be produced by code_change tasks");
941
+ const taskRepositoryId = task.repositoryIds[0];
942
+ if (taskRepositoryId !== change.repositoryId) throw new ChangeAssociationError(change.taskId, change.repositoryId, `repository "${change.repositoryId}" does not match task repository "${taskRepositoryId}"`);
943
+ }
691
944
  await driver.insert("changes", change.id, change);
692
945
  return change;
693
946
  },
@@ -805,6 +1058,21 @@ async function sweepQueued(stores, options) {
805
1058
  };
806
1059
  const queued = (await stores.tasks.list()).filter((task) => task.state === "queued" && task.kind !== "question").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
807
1060
  for (const task of queued) {
1061
+ if (task.kind === "coordination") {
1062
+ result.skipped.push({
1063
+ taskId: task.id,
1064
+ reason: "coordination tasks are reconciled, not dispatched"
1065
+ });
1066
+ continue;
1067
+ }
1068
+ const executionRepositoryIds = taskExecutionRepositoryIdsSchema.safeParse(task.repositoryIds);
1069
+ if (!executionRepositoryIds.success) {
1070
+ result.skipped.push({
1071
+ taskId: task.id,
1072
+ reason: "task execution requires exactly one repository"
1073
+ });
1074
+ continue;
1075
+ }
808
1076
  if (task.approval === "required") {
809
1077
  result.skipped.push({
810
1078
  taskId: task.id,
@@ -820,8 +1088,7 @@ async function sweepQueued(stores, options) {
820
1088
  });
821
1089
  continue;
822
1090
  }
823
- const repositoryId = task.repositoryIds?.[0];
824
- const repository = repositoryId === void 0 ? null : await stores.repositories.get(repositoryId);
1091
+ const repository = await stores.repositories.get(executionRepositoryIds.data[0]);
825
1092
  if (repository === null) {
826
1093
  result.skipped.push({
827
1094
  taskId: task.id,
@@ -849,6 +1116,6 @@ async function sweepQueued(stores, options) {
849
1116
  return result;
850
1117
  }
851
1118
  //#endregion
852
- export { ApprovalError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, VersionConflictError, adoptPullRequest, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetMonthSchema, budgetSchema, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, costSchema, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defineFactory, dispatchTask, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepQueued, taskApprovalSchema, taskDevboxSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, terminalStates, usdSchema, vocabularySchema };
1119
+ export { ApprovalError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, VersionConflictError, adoptPullRequest, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetMonthSchema, budgetSchema, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, computeLedgerMetrics, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createCoordination, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defineFactory, dispatchTask, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, reconcileCoordination, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepQueued, taskApprovalSchema, taskDevboxSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, terminalStates, usdSchema, vocabularySchema };
853
1120
 
854
1121
  //# sourceMappingURL=index.mjs.map