@proofhound/core 0.1.8 → 0.1.9

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.
Files changed (164) hide show
  1. package/dist/server/channels/mcp/annotation.tools.d.ts.map +1 -1
  2. package/dist/server/channels/mcp/annotation.tools.js +18 -5
  3. package/dist/server/channels/mcp/annotation.tools.js.map +1 -1
  4. package/dist/server/channels/mcp/canary-release.tools.d.ts.map +1 -1
  5. package/dist/server/channels/mcp/canary-release.tools.js +2 -1
  6. package/dist/server/channels/mcp/canary-release.tools.js.map +1 -1
  7. package/dist/server/channels/mcp/dataset.tools.d.ts.map +1 -1
  8. package/dist/server/channels/mcp/dataset.tools.js +49 -1
  9. package/dist/server/channels/mcp/dataset.tools.js.map +1 -1
  10. package/dist/server/channels/mcp/mcp-server.factory.d.ts +9 -1
  11. package/dist/server/channels/mcp/mcp-server.factory.d.ts.map +1 -1
  12. package/dist/server/channels/mcp/mcp-server.factory.js +28 -3
  13. package/dist/server/channels/mcp/mcp-server.factory.js.map +1 -1
  14. package/dist/server/channels/mcp/prompt.tools.d.ts.map +1 -1
  15. package/dist/server/channels/mcp/prompt.tools.js +34 -2
  16. package/dist/server/channels/mcp/prompt.tools.js.map +1 -1
  17. package/dist/server/channels/mcp/release-line.tools.d.ts.map +1 -1
  18. package/dist/server/channels/mcp/release-line.tools.js +292 -4
  19. package/dist/server/channels/mcp/release-line.tools.js.map +1 -1
  20. package/dist/server/channels/mcp/run-result.tools.d.ts.map +1 -1
  21. package/dist/server/channels/mcp/run-result.tools.js +7 -5
  22. package/dist/server/channels/mcp/run-result.tools.js.map +1 -1
  23. package/dist/server/infrastructure/llm/run-result-writer.js +3 -3
  24. package/dist/server/modules/annotation/annotation.controller.d.ts +28 -13
  25. package/dist/server/modules/annotation/annotation.controller.d.ts.map +1 -1
  26. package/dist/server/modules/annotation/annotation.repository.d.ts +6 -2
  27. package/dist/server/modules/annotation/annotation.repository.d.ts.map +1 -1
  28. package/dist/server/modules/annotation/annotation.repository.js +340 -96
  29. package/dist/server/modules/annotation/annotation.repository.js.map +1 -1
  30. package/dist/server/modules/annotation/annotation.service.d.ts.map +1 -1
  31. package/dist/server/modules/annotation/annotation.service.js +62 -10
  32. package/dist/server/modules/annotation/annotation.service.js.map +1 -1
  33. package/dist/server/modules/canary-release/canary-release.controller.d.ts +63 -42
  34. package/dist/server/modules/canary-release/canary-release.controller.d.ts.map +1 -1
  35. package/dist/server/modules/canary-release/canary-release.repository.d.ts +23 -5
  36. package/dist/server/modules/canary-release/canary-release.repository.d.ts.map +1 -1
  37. package/dist/server/modules/canary-release/canary-release.repository.js +28 -12
  38. package/dist/server/modules/canary-release/canary-release.repository.js.map +1 -1
  39. package/dist/server/modules/canary-release/canary-release.service.d.ts.map +1 -1
  40. package/dist/server/modules/canary-release/canary-release.service.js +32 -10
  41. package/dist/server/modules/canary-release/canary-release.service.js.map +1 -1
  42. package/dist/server/modules/canary-release/canary-runtime.d.ts +11 -1
  43. package/dist/server/modules/canary-release/canary-runtime.d.ts.map +1 -1
  44. package/dist/server/modules/canary-release/canary-runtime.js +63 -8
  45. package/dist/server/modules/canary-release/canary-runtime.js.map +1 -1
  46. package/dist/server/modules/dataset/dataset-deletion.hook.d.ts +16 -0
  47. package/dist/server/modules/dataset/dataset-deletion.hook.d.ts.map +1 -0
  48. package/dist/server/modules/dataset/dataset-deletion.hook.js +57 -0
  49. package/dist/server/modules/dataset/dataset-deletion.hook.js.map +1 -0
  50. package/dist/server/modules/dataset/dataset-import.controller.d.ts +2 -0
  51. package/dist/server/modules/dataset/dataset-import.controller.d.ts.map +1 -1
  52. package/dist/server/modules/dataset/dataset.controller.d.ts +98 -0
  53. package/dist/server/modules/dataset/dataset.controller.d.ts.map +1 -1
  54. package/dist/server/modules/dataset/dataset.controller.js +36 -0
  55. package/dist/server/modules/dataset/dataset.controller.js.map +1 -1
  56. package/dist/server/modules/dataset/dataset.module.d.ts.map +1 -1
  57. package/dist/server/modules/dataset/dataset.module.js +8 -1
  58. package/dist/server/modules/dataset/dataset.module.js.map +1 -1
  59. package/dist/server/modules/dataset/dataset.repository.d.ts +19 -0
  60. package/dist/server/modules/dataset/dataset.repository.d.ts.map +1 -1
  61. package/dist/server/modules/dataset/dataset.repository.js +248 -9
  62. package/dist/server/modules/dataset/dataset.repository.js.map +1 -1
  63. package/dist/server/modules/dataset/dataset.service.d.ts +33 -1
  64. package/dist/server/modules/dataset/dataset.service.d.ts.map +1 -1
  65. package/dist/server/modules/dataset/dataset.service.js +49 -7
  66. package/dist/server/modules/dataset/dataset.service.js.map +1 -1
  67. package/dist/server/modules/experiment/experiment.controller.d.ts +8 -8
  68. package/dist/server/modules/experiment/experiment.repository.d.ts.map +1 -1
  69. package/dist/server/modules/experiment/experiment.repository.js +28 -0
  70. package/dist/server/modules/experiment/experiment.repository.js.map +1 -1
  71. package/dist/server/modules/experiment/experiment.service.d.ts.map +1 -1
  72. package/dist/server/modules/experiment/experiment.service.js +6 -3
  73. package/dist/server/modules/experiment/experiment.service.js.map +1 -1
  74. package/dist/server/modules/model/project-model.controller.d.ts +5 -5
  75. package/dist/server/modules/monitoring/monitoring.repository.js +1 -1
  76. package/dist/server/modules/optimization/optimization.controller.d.ts +12 -12
  77. package/dist/server/modules/optimization/optimization.repository.d.ts +6 -0
  78. package/dist/server/modules/optimization/optimization.repository.d.ts.map +1 -1
  79. package/dist/server/modules/optimization/optimization.repository.js +96 -4
  80. package/dist/server/modules/optimization/optimization.repository.js.map +1 -1
  81. package/dist/server/modules/optimization/optimization.service.d.ts.map +1 -1
  82. package/dist/server/modules/optimization/optimization.service.js +13 -4
  83. package/dist/server/modules/optimization/optimization.service.js.map +1 -1
  84. package/dist/server/modules/optimization/optimization.workflow.js +1 -1
  85. package/dist/server/modules/optimization/optimization.workflow.js.map +1 -1
  86. package/dist/server/modules/production-release/production-release.controller.d.ts +12 -9
  87. package/dist/server/modules/production-release/production-release.controller.d.ts.map +1 -1
  88. package/dist/server/modules/production-release/production-release.repository.d.ts +2 -1
  89. package/dist/server/modules/production-release/production-release.repository.d.ts.map +1 -1
  90. package/dist/server/modules/production-release/production-release.repository.js +3 -1
  91. package/dist/server/modules/production-release/production-release.repository.js.map +1 -1
  92. package/dist/server/modules/production-release/production-release.service.d.ts.map +1 -1
  93. package/dist/server/modules/production-release/production-release.service.js +10 -1
  94. package/dist/server/modules/production-release/production-release.service.js.map +1 -1
  95. package/dist/server/modules/prompt/prompt-deletion.hook.d.ts +18 -0
  96. package/dist/server/modules/prompt/prompt-deletion.hook.d.ts.map +1 -0
  97. package/dist/server/modules/prompt/prompt-deletion.hook.js +69 -0
  98. package/dist/server/modules/prompt/prompt-deletion.hook.js.map +1 -0
  99. package/dist/server/modules/prompt/prompt.controller.d.ts +146 -38
  100. package/dist/server/modules/prompt/prompt.controller.d.ts.map +1 -1
  101. package/dist/server/modules/prompt/prompt.controller.js +24 -0
  102. package/dist/server/modules/prompt/prompt.controller.js.map +1 -1
  103. package/dist/server/modules/prompt/prompt.module.d.ts.map +1 -1
  104. package/dist/server/modules/prompt/prompt.module.js +7 -1
  105. package/dist/server/modules/prompt/prompt.module.js.map +1 -1
  106. package/dist/server/modules/prompt/prompt.repository.d.ts +33 -3
  107. package/dist/server/modules/prompt/prompt.repository.d.ts.map +1 -1
  108. package/dist/server/modules/prompt/prompt.repository.js +267 -39
  109. package/dist/server/modules/prompt/prompt.repository.js.map +1 -1
  110. package/dist/server/modules/prompt/prompt.service.d.ts +78 -6
  111. package/dist/server/modules/prompt/prompt.service.d.ts.map +1 -1
  112. package/dist/server/modules/prompt/prompt.service.js +79 -49
  113. package/dist/server/modules/prompt/prompt.service.js.map +1 -1
  114. package/dist/server/modules/quick-start/quick-start.controller.d.ts +1 -1
  115. package/dist/server/modules/quick-start/quick-start.service.d.ts +1 -1
  116. package/dist/server/modules/release-line/release-line-deletion.hook.d.ts +16 -0
  117. package/dist/server/modules/release-line/release-line-deletion.hook.d.ts.map +1 -0
  118. package/dist/server/modules/release-line/release-line-deletion.hook.js +60 -0
  119. package/dist/server/modules/release-line/release-line-deletion.hook.js.map +1 -0
  120. package/dist/server/modules/release-line/release-line.controller.d.ts +2503 -82
  121. package/dist/server/modules/release-line/release-line.controller.d.ts.map +1 -1
  122. package/dist/server/modules/release-line/release-line.controller.js +169 -0
  123. package/dist/server/modules/release-line/release-line.controller.js.map +1 -1
  124. package/dist/server/modules/release-line/release-line.module.d.ts.map +1 -1
  125. package/dist/server/modules/release-line/release-line.module.js +8 -1
  126. package/dist/server/modules/release-line/release-line.module.js.map +1 -1
  127. package/dist/server/modules/release-line/release-line.repository.d.ts +55 -3
  128. package/dist/server/modules/release-line/release-line.repository.d.ts.map +1 -1
  129. package/dist/server/modules/release-line/release-line.repository.js +797 -111
  130. package/dist/server/modules/release-line/release-line.repository.js.map +1 -1
  131. package/dist/server/modules/release-line/release-line.service.d.ts +22 -5
  132. package/dist/server/modules/release-line/release-line.service.d.ts.map +1 -1
  133. package/dist/server/modules/release-line/release-line.service.js +231 -3
  134. package/dist/server/modules/release-line/release-line.service.js.map +1 -1
  135. package/dist/server/modules/release-line/release-runner.repository.d.ts +2 -1
  136. package/dist/server/modules/release-line/release-runner.repository.d.ts.map +1 -1
  137. package/dist/server/modules/release-line/release-runner.repository.js +14 -10
  138. package/dist/server/modules/release-line/release-runner.repository.js.map +1 -1
  139. package/dist/server/modules/release-line/release-runner.service.d.ts +2 -1
  140. package/dist/server/modules/release-line/release-runner.service.d.ts.map +1 -1
  141. package/dist/server/modules/release-line/release-runner.service.js +105 -11
  142. package/dist/server/modules/release-line/release-runner.service.js.map +1 -1
  143. package/dist/server/modules/release-line/release-variable-mapping.d.ts +9 -0
  144. package/dist/server/modules/release-line/release-variable-mapping.d.ts.map +1 -0
  145. package/dist/server/modules/release-line/release-variable-mapping.js +83 -0
  146. package/dist/server/modules/release-line/release-variable-mapping.js.map +1 -0
  147. package/dist/server/modules/run-result/run-result.controller.d.ts +10 -7
  148. package/dist/server/modules/run-result/run-result.controller.d.ts.map +1 -1
  149. package/dist/server/modules/run-result/run-result.repository.d.ts.map +1 -1
  150. package/dist/server/modules/run-result/run-result.repository.js +43 -18
  151. package/dist/server/modules/run-result/run-result.repository.js.map +1 -1
  152. package/dist/webhook/channels/webhook/webhook.controller.d.ts +4 -0
  153. package/dist/webhook/channels/webhook/webhook.controller.d.ts.map +1 -1
  154. package/dist/webhook/channels/webhook/webhook.service.d.ts +2 -0
  155. package/dist/webhook/channels/webhook/webhook.service.d.ts.map +1 -1
  156. package/dist/webhook/channels/webhook/webhook.service.js +6 -0
  157. package/dist/webhook/channels/webhook/webhook.service.js.map +1 -1
  158. package/dist/worker/consumers/llm.consumer.js +3 -3
  159. package/dist/worker/consumers/llm.consumer.js.map +1 -1
  160. package/dist/worker/runners/llm-runner.d.ts.map +1 -1
  161. package/dist/worker/runners/llm-runner.js +9 -1
  162. package/dist/worker/runners/llm-runner.js.map +1 -1
  163. package/dist/worker/runners/run-result-writer.js +3 -3
  164. package/package.json +12 -12
@@ -17,7 +17,7 @@ const common_1 = require("@nestjs/common");
17
17
  const drizzle_orm_1 = require("drizzle-orm");
18
18
  const db_1 = require("@proofhound/db");
19
19
  const database_constants_1 = require("../../../shared/database/database.constants");
20
- const { annotationTasks, connectors, models, projects, releaseLineEvents, releaseLines, releaseVariants } = db_1.schema;
20
+ const { annotationTasks, connectors, models, projects, prompts, releaseLineEvents, releaseLines, releaseVersions } = db_1.schema;
21
21
  let ReleaseLineRepository = class ReleaseLineRepository {
22
22
  constructor(db) {
23
23
  this.db = db;
@@ -74,6 +74,15 @@ let ReleaseLineRepository = class ReleaseLineRepository {
74
74
  .orderBy((0, drizzle_orm_1.desc)(releaseLineEvents.createdAt));
75
75
  return this.hydrateEvents(rows);
76
76
  }
77
+ async findEventById(projectId, releaseLineId, eventId) {
78
+ const rows = await this.db
79
+ .select()
80
+ .from(releaseLineEvents)
81
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseLineEvents.id, eventId)))
82
+ .limit(1);
83
+ const hydrated = await this.hydrateEvents(rows);
84
+ return hydrated[0] ?? null;
85
+ }
77
86
  async record(snapshot) {
78
87
  const releaseLineId = await this.db.transaction(async (tx) => {
79
88
  const now = snapshot.updatedAt ?? new Date();
@@ -99,6 +108,19 @@ let ReleaseLineRepository = class ReleaseLineRepository {
99
108
  })
100
109
  .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, line.id), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'production'), (0, drizzle_orm_1.eq)(releaseLineEvents.status, 'running')));
101
110
  }
111
+ if (snapshot.laneType === 'production' && productionOperationReleasesCanarySlot(snapshot.operation)) {
112
+ await tx
113
+ .update(releaseLineEvents)
114
+ .set({
115
+ status: 'cancelled',
116
+ terminalReason: 'cancelled',
117
+ finishedAt: now,
118
+ controlState: null,
119
+ controlStatePayload: null,
120
+ updatedAt: now,
121
+ })
122
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, line.id), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'canary'), (0, drizzle_orm_1.sql) `${releaseLineEvents.status} IN ('running', 'stopped')`));
123
+ }
102
124
  if (snapshot.laneType === 'production' && snapshot.operation === 'force_stop') {
103
125
  await tx
104
126
  .update(releaseLineEvents)
@@ -129,8 +151,8 @@ let ReleaseLineRepository = class ReleaseLineRepository {
129
151
  .set({ status: 'completed', terminalReason: 'replaced', finishedAt: now, updatedAt: now })
130
152
  .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, line.id), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'canary'), (0, drizzle_orm_1.sql) `${releaseLineEvents.status} IN ('running', 'stopped')`));
131
153
  }
132
- const releaseVariant = await this.findOrCreateVariant(tx, line.id, snapshot, now);
133
- const eventValues = await this.buildEventInsert(snapshot, line.id, supersedesEventId, releaseVariant?.id ?? null, now);
154
+ const releaseVersion = await this.resolveReleaseVersion(tx, line.id, snapshot, now);
155
+ const eventValues = await this.buildEventInsert(snapshot, line.id, supersedesEventId, releaseVersion?.id ?? null, now);
134
156
  const inserted = await tx.insert(releaseLineEvents).values(eventValues).returning();
135
157
  const event = inserted[0];
136
158
  if (!event)
@@ -149,24 +171,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
149
171
  if (!line || !canary || (canary.status !== 'running' && canary.status !== 'stopped'))
150
172
  return null;
151
173
  if (canary.status === 'running' && canary.trafficMode === 'split' && trafficRatio >= 1) {
152
- const promoted = await this.record(resetRuntimeStats({
153
- ...eventDtoToSnapshot(line, canary),
154
- laneType: 'production',
155
- operation: 'promote_canary',
156
- status: 'running',
157
- terminalReason: null,
158
- sourceEventId: canary.id,
159
- trafficMode: null,
160
- trafficRatio: null,
161
- submitReason: promotionSubmitReason(line),
162
- createdBy: actorUserId,
163
- createdAt: new Date(),
164
- updatedAt: new Date(),
165
- legacySource: null,
166
- legacySourceId: null,
167
- }));
168
- await this.completeCanaryEvent(canary.id, 'promoted');
169
- return this.findById(projectId, promoted.id);
174
+ return this.promoteCanaryEvent(line, canary, actorUserId);
170
175
  }
171
176
  await this.record(resetRuntimeStats({
172
177
  ...eventDtoToSnapshot(line, canary),
@@ -182,6 +187,474 @@ let ReleaseLineRepository = class ReleaseLineRepository {
182
187
  }));
183
188
  return this.findById(projectId, releaseLineId);
184
189
  }
190
+ async promoteActiveCanary(projectId, releaseLineId, actorUserId) {
191
+ const line = await this.findById(projectId, releaseLineId);
192
+ const canary = line?.activeCanaryEvent;
193
+ if (!line || !canary || canary.status !== 'running')
194
+ return null;
195
+ return this.promoteCanaryEvent(line, canary, actorUserId);
196
+ }
197
+ async stopLine(projectId, releaseLineId, reason, actorUserId) {
198
+ const line = await this.findById(projectId, releaseLineId);
199
+ if (!line || line.status === 'archived')
200
+ return null;
201
+ const runningProduction = line.currentProductionEvent?.status === 'running' ? line.currentProductionEvent : null;
202
+ const runningCanary = line.activeCanaryEvent?.status === 'running' ? line.activeCanaryEvent : null;
203
+ if (!runningProduction && !runningCanary)
204
+ return null;
205
+ const now = new Date();
206
+ if (runningProduction) {
207
+ const stopped = await this.record(resetRuntimeStats({
208
+ ...eventDtoToSnapshot(line, runningProduction),
209
+ operation: 'force_stop',
210
+ status: 'stopped',
211
+ terminalReason: 'force_stopped',
212
+ submitReason: reason,
213
+ createdBy: actorUserId,
214
+ createdAt: now,
215
+ updatedAt: now,
216
+ legacySource: null,
217
+ legacySourceId: null,
218
+ }));
219
+ await this.clearPromptProductionVersion(runningProduction.promptId);
220
+ return this.findById(projectId, stopped.id);
221
+ }
222
+ if (!runningCanary)
223
+ return null;
224
+ const stopped = await this.record(resetRuntimeStats({
225
+ ...eventDtoToSnapshot(line, runningCanary),
226
+ operation: 'stop_lane',
227
+ status: 'stopped',
228
+ terminalReason: null,
229
+ supersedesEventId: runningCanary.id,
230
+ submitReason: reason,
231
+ createdBy: actorUserId,
232
+ createdAt: now,
233
+ updatedAt: now,
234
+ legacySource: null,
235
+ legacySourceId: null,
236
+ }));
237
+ return this.findById(projectId, stopped.id);
238
+ }
239
+ async startLine(projectId, releaseLineId, reason, actorUserId) {
240
+ const line = await this.findById(projectId, releaseLineId);
241
+ if (!line || line.status !== 'stopped')
242
+ return null;
243
+ const resumableEvents = findResumableEvents(line);
244
+ if (resumableEvents.length === 0)
245
+ return null;
246
+ const now = new Date();
247
+ let started = null;
248
+ for (const resumable of resumableEvents) {
249
+ started = await this.record(resetRuntimeStats({
250
+ ...eventDtoToSnapshot(line, resumable),
251
+ operation: 'resume_lane',
252
+ status: 'running',
253
+ terminalReason: null,
254
+ supersedesEventId: resumable.id,
255
+ submitReason: reason ?? 'start release line',
256
+ controlState: null,
257
+ controlStatePayload: null,
258
+ startedAt: now,
259
+ finishedAt: null,
260
+ createdBy: actorUserId,
261
+ createdAt: now,
262
+ updatedAt: now,
263
+ legacySource: null,
264
+ legacySourceId: null,
265
+ }));
266
+ if (resumable.laneType === 'production') {
267
+ await this.setPromptProductionVersion(projectId, resumable.promptId, resumable.promptVersionId);
268
+ }
269
+ }
270
+ return this.findById(projectId, started?.id ?? releaseLineId);
271
+ }
272
+ async archiveLine(projectId, releaseLineId, reason, actorUserId) {
273
+ const line = await this.findById(projectId, releaseLineId);
274
+ if (!line)
275
+ return null;
276
+ if (line.status === 'archived')
277
+ return line;
278
+ const hasRunningLane = line.currentProductionEvent?.status === 'running' || line.activeCanaryEvent?.status === 'running';
279
+ if (hasRunningLane)
280
+ return null;
281
+ const slotEvents = findVisibleSlotEvents(line);
282
+ const fallbackEvent = line.latestEvent ?? line.activeCanaryEvent ?? line.currentProductionEvent ?? null;
283
+ const archiveTargets = slotEvents.length > 0 || !fallbackEvent ? slotEvents : [fallbackEvent];
284
+ const now = new Date();
285
+ await this.db.transaction(async (tx) => {
286
+ let currentProductionEventId = line.currentProductionEventId;
287
+ let activeCanaryEventId = line.activeCanaryEventId;
288
+ for (const target of archiveTargets) {
289
+ const eventValues = await this.buildEventInsert({
290
+ ...eventDtoToSnapshot(line, target),
291
+ operation: 'archive_line',
292
+ status: 'archived',
293
+ terminalReason: 'archived',
294
+ supersedesEventId: target.id,
295
+ submitReason: reason ?? 'archive release line',
296
+ controlState: null,
297
+ controlStatePayload: null,
298
+ finishedAt: now,
299
+ createdBy: actorUserId,
300
+ createdAt: now,
301
+ updatedAt: now,
302
+ legacySource: null,
303
+ legacySourceId: null,
304
+ }, line.id, target.id, target.releaseVersionId, now);
305
+ const inserted = await tx.insert(releaseLineEvents).values(eventValues).returning({ id: releaseLineEvents.id });
306
+ const archivedEventId = inserted[0]?.id ?? null;
307
+ if (target.laneType === 'production')
308
+ currentProductionEventId = archivedEventId;
309
+ if (target.laneType === 'canary')
310
+ activeCanaryEventId = archivedEventId;
311
+ }
312
+ await tx
313
+ .update(releaseLines)
314
+ .set({ status: 'archived', currentProductionEventId, activeCanaryEventId, archivedAt: now, updatedAt: now })
315
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLines.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId)));
316
+ });
317
+ return this.findById(projectId, releaseLineId);
318
+ }
319
+ async unarchiveLine(projectId, releaseLineId, reason, actorUserId) {
320
+ const line = await this.findById(projectId, releaseLineId);
321
+ if (!line || line.status !== 'archived')
322
+ return null;
323
+ const slotEvents = findArchivedSlotEvents(line);
324
+ const fallbackEvent = line.latestEvent ?? line.activeCanaryEvent ?? line.currentProductionEvent ?? null;
325
+ const restoreTargets = slotEvents.length > 0 || !fallbackEvent ? slotEvents : [fallbackEvent];
326
+ const now = new Date();
327
+ if (restoreTargets.length === 0) {
328
+ await this.db
329
+ .update(releaseLines)
330
+ .set({ status: 'stopped', archivedAt: null, updatedAt: now })
331
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLines.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId)));
332
+ return this.findById(projectId, releaseLineId);
333
+ }
334
+ let restored = null;
335
+ for (const target of restoreTargets) {
336
+ restored = await this.record(resetRuntimeStats({
337
+ ...eventDtoToSnapshot(line, target),
338
+ operation: 'unarchive_line',
339
+ status: 'stopped',
340
+ terminalReason: null,
341
+ supersedesEventId: target.id,
342
+ submitReason: reason ?? 'unarchive release line',
343
+ controlState: null,
344
+ controlStatePayload: null,
345
+ finishedAt: now,
346
+ createdBy: actorUserId,
347
+ createdAt: now,
348
+ updatedAt: now,
349
+ legacySource: null,
350
+ legacySourceId: null,
351
+ }));
352
+ }
353
+ return this.findById(projectId, restored?.id ?? releaseLineId);
354
+ }
355
+ async restoreHistoryToLane(projectId, releaseLineId, sourceEventId, targetLaneType, reason, actorUserId) {
356
+ const line = await this.findById(projectId, releaseLineId);
357
+ if (!line || line.status === 'archived')
358
+ return null;
359
+ const source = await this.findEventById(projectId, releaseLineId, sourceEventId);
360
+ if (!source || !source.promptVersionId || !source.modelId)
361
+ return null;
362
+ const currentCanary = line.activeCanaryEvent;
363
+ const status = line.status === 'running' ? 'running' : 'stopped';
364
+ const now = new Date();
365
+ const restored = await this.record(resetRuntimeStats({
366
+ ...eventDtoToSnapshot(line, source),
367
+ laneType: targetLaneType,
368
+ operation: targetLaneType === 'production' ? 'restore_to_production' : 'restore_to_canary',
369
+ status,
370
+ terminalReason: null,
371
+ releaseVersionId: null,
372
+ sourceEventId: source.id,
373
+ supersedesEventId: targetLaneType === 'production' ? line.currentProductionEvent?.id : line.activeCanaryEvent?.id,
374
+ rollbackTargetEventId: targetLaneType === 'production' ? source.id : null,
375
+ trafficMode: targetLaneType === 'canary' ? (source.trafficMode ?? currentCanary?.trafficMode ?? 'split') : null,
376
+ trafficRatio: targetLaneType === 'canary' ? (source.trafficRatio ?? currentCanary?.trafficRatio ?? 0.1) : null,
377
+ submitReason: reason ??
378
+ (targetLaneType === 'production'
379
+ ? 'restore history to production slot'
380
+ : 'restore history to canary slot'),
381
+ controlState: null,
382
+ controlStatePayload: null,
383
+ startedAt: status === 'running' ? now : null,
384
+ finishedAt: null,
385
+ createdBy: actorUserId,
386
+ createdAt: now,
387
+ updatedAt: now,
388
+ legacySource: null,
389
+ legacySourceId: null,
390
+ }));
391
+ if (targetLaneType === 'production' && status === 'running') {
392
+ await this.setPromptProductionVersion(projectId, source.promptId, source.promptVersionId);
393
+ }
394
+ return this.findById(projectId, restored.id);
395
+ }
396
+ async listDeletionImpact(projectId, releaseLineId) {
397
+ const lineRows = await this.db
398
+ .select({ id: releaseLines.id, name: releaseLines.name })
399
+ .from(releaseLines)
400
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLines.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId)))
401
+ .limit(1);
402
+ const line = lineRows[0];
403
+ if (!line)
404
+ return null;
405
+ const eventRows = await this.db
406
+ .select({
407
+ id: releaseLineEvents.id,
408
+ operation: releaseLineEvents.operation,
409
+ laneType: releaseLineEvents.laneType,
410
+ status: releaseLineEvents.status,
411
+ createdAt: releaseLineEvents.createdAt,
412
+ })
413
+ .from(releaseLineEvents)
414
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId)))
415
+ .orderBy((0, drizzle_orm_1.desc)(releaseLineEvents.createdAt));
416
+ const versionRows = await this.db
417
+ .select()
418
+ .from(releaseVersions)
419
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseVersions.projectId, projectId), (0, drizzle_orm_1.eq)(releaseVersions.releaseLineId, releaseLineId)))
420
+ .orderBy(releaseVersions.targetProductionVersionNumber, releaseVersions.kind, releaseVersions.candidateNumber);
421
+ const taskRows = await this.db
422
+ .select({
423
+ id: annotationTasks.id,
424
+ name: annotationTasks.name,
425
+ status: annotationTasks.status,
426
+ scope: annotationTasks.scope,
427
+ createdAt: annotationTasks.createdAt,
428
+ })
429
+ .from(annotationTasks)
430
+ .where((0, drizzle_orm_1.sql) `
431
+ ${annotationTasks.releaseLineEventId} IN (
432
+ SELECT id FROM ph_releases.release_line_events
433
+ WHERE project_id = ${projectId}::uuid AND release_line_id = ${releaseLineId}::uuid
434
+ )
435
+ OR ${annotationTasks.releaseVersionId} IN (
436
+ SELECT id FROM ph_releases.release_versions
437
+ WHERE project_id = ${projectId}::uuid AND release_line_id = ${releaseLineId}::uuid
438
+ )
439
+ `)
440
+ .orderBy((0, drizzle_orm_1.desc)(annotationTasks.createdAt));
441
+ const runResultRows = await this.db.execute((0, drizzle_orm_1.sql) `
442
+ SELECT COUNT(*)::int AS count
443
+ FROM ph_runs.run_results rr
444
+ WHERE rr.project_id = ${projectId}::uuid
445
+ AND rr.source = 'release'
446
+ AND (
447
+ rr.source_id IN (
448
+ SELECT id FROM ph_releases.release_line_events
449
+ WHERE project_id = ${projectId}::uuid AND release_line_id = ${releaseLineId}::uuid
450
+ )
451
+ OR rr.release_version_id IN (
452
+ SELECT id FROM ph_releases.release_versions
453
+ WHERE project_id = ${projectId}::uuid AND release_line_id = ${releaseLineId}::uuid
454
+ )
455
+ )
456
+ `);
457
+ const runResults = Number(unwrapRows(runResultRows)[0]?.['count'] ?? 0);
458
+ return {
459
+ line,
460
+ events: eventRows.map((row) => ({
461
+ id: row.id,
462
+ name: row.operation,
463
+ status: row.status,
464
+ detail: row.laneType,
465
+ createdAt: row.createdAt,
466
+ })),
467
+ versions: versionRows.map((row) => ({
468
+ id: row.id,
469
+ name: formatReleaseVersionLabel(row),
470
+ status: row.kind,
471
+ detail: row.promptName,
472
+ createdAt: row.createdAt,
473
+ })),
474
+ annotationTasks: taskRows.map((row) => ({
475
+ id: row.id,
476
+ name: row.name,
477
+ status: row.status,
478
+ detail: row.scope,
479
+ createdAt: row.createdAt,
480
+ })),
481
+ runResults,
482
+ };
483
+ }
484
+ /**
485
+ * Force-stop every running lane of a release line and drop it out of the runner's runnable set,
486
+ * committed in its OWN transaction ahead of hardDeleteLine. Once the line's slot events are no longer
487
+ * 'running', the next runner scan's findRunnableLine returns null and stops dispatching — a best-effort
488
+ * barrier before the physical delete. A residual LLM job already enqueued before this call may still
489
+ * write a run result; hardDeleteLine's cascade removes those, and any landing after deletion either fail
490
+ * the run_results.release_version_id FK or leave a harmless orphan (permanent delete is a confirmed
491
+ * dangerous action). Archived lines stay archived.
492
+ */
493
+ async forceStopRunningLanesForDelete(projectId, releaseLineId) {
494
+ await this.db.transaction(async (tx) => {
495
+ const now = new Date();
496
+ await tx
497
+ .update(releaseLineEvents)
498
+ .set({
499
+ status: 'stopped',
500
+ terminalReason: 'force_stopped',
501
+ finishedAt: now,
502
+ controlState: null,
503
+ controlStatePayload: null,
504
+ updatedAt: now,
505
+ })
506
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseLineEvents.status, 'running')));
507
+ await tx
508
+ .update(releaseLines)
509
+ .set({
510
+ status: (0, drizzle_orm_1.sql) `CASE WHEN ${releaseLines.status} = 'archived' THEN 'archived' ELSE 'stopped' END`,
511
+ updatedAt: now,
512
+ })
513
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLines.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId)));
514
+ });
515
+ }
516
+ async hardDeleteLine(projectId, releaseLineId) {
517
+ return this.db.transaction(async (tx) => {
518
+ await tx.execute((0, drizzle_orm_1.sql) `
519
+ WITH target_events AS (
520
+ SELECT id, prompt_id, prompt_version_id
521
+ FROM ph_releases.release_line_events
522
+ WHERE project_id = ${projectId}::uuid
523
+ AND release_line_id = ${releaseLineId}::uuid
524
+ ),
525
+ target_versions AS (
526
+ SELECT id
527
+ FROM ph_releases.release_versions
528
+ WHERE project_id = ${projectId}::uuid
529
+ AND release_line_id = ${releaseLineId}::uuid
530
+ ),
531
+ target_tasks AS (
532
+ SELECT id
533
+ FROM ph_releases.annotation_tasks
534
+ WHERE release_line_event_id IN (SELECT id FROM target_events)
535
+ OR release_version_id IN (SELECT id FROM target_versions)
536
+ ),
537
+ target_run_results AS (
538
+ SELECT rr.id, rr.created_at
539
+ FROM ph_runs.run_results rr
540
+ WHERE rr.project_id = ${projectId}::uuid
541
+ AND rr.source = 'release'
542
+ AND (
543
+ rr.source_id IN (SELECT id FROM target_events)
544
+ OR rr.release_version_id IN (SELECT id FROM target_versions)
545
+ )
546
+ )
547
+ DELETE FROM ph_runs.annotations annotation
548
+ WHERE annotation.task_id IN (SELECT id FROM target_tasks)
549
+ OR EXISTS (
550
+ SELECT 1
551
+ FROM target_run_results rr
552
+ WHERE annotation.run_result_id = rr.id
553
+ AND annotation.run_result_created_at = rr.created_at
554
+ )
555
+ `);
556
+ await tx.execute((0, drizzle_orm_1.sql) `
557
+ WITH target_events AS (
558
+ SELECT id
559
+ FROM ph_releases.release_line_events
560
+ WHERE project_id = ${projectId}::uuid
561
+ AND release_line_id = ${releaseLineId}::uuid
562
+ ),
563
+ target_versions AS (
564
+ SELECT id
565
+ FROM ph_releases.release_versions
566
+ WHERE project_id = ${projectId}::uuid
567
+ AND release_line_id = ${releaseLineId}::uuid
568
+ ),
569
+ target_run_results AS (
570
+ SELECT rr.id, rr.created_at
571
+ FROM ph_runs.run_results rr
572
+ WHERE rr.project_id = ${projectId}::uuid
573
+ AND rr.source = 'release'
574
+ AND (
575
+ rr.source_id IN (SELECT id FROM target_events)
576
+ OR rr.release_version_id IN (SELECT id FROM target_versions)
577
+ )
578
+ )
579
+ DELETE FROM ph_runs.run_results rr
580
+ USING target_run_results target
581
+ WHERE rr.id = target.id
582
+ AND rr.created_at = target.created_at
583
+ `);
584
+ await tx.execute((0, drizzle_orm_1.sql) `
585
+ WITH target_events AS (
586
+ SELECT id, prompt_id, prompt_version_id
587
+ FROM ph_releases.release_line_events
588
+ WHERE project_id = ${projectId}::uuid
589
+ AND release_line_id = ${releaseLineId}::uuid
590
+ ),
591
+ target_versions AS (
592
+ SELECT id
593
+ FROM ph_releases.release_versions
594
+ WHERE project_id = ${projectId}::uuid
595
+ AND release_line_id = ${releaseLineId}::uuid
596
+ )
597
+ UPDATE ph_assets.prompts prompt
598
+ SET current_online_version_id = NULL,
599
+ updated_at = NOW()
600
+ WHERE prompt.project_id = ${projectId}::uuid
601
+ AND prompt.current_online_version_id IN (
602
+ SELECT prompt_version_id
603
+ FROM target_events
604
+ WHERE prompt_id = prompt.id
605
+ AND prompt_version_id IS NOT NULL
606
+ )
607
+ `);
608
+ await tx.execute((0, drizzle_orm_1.sql) `
609
+ WITH target_events AS (
610
+ SELECT id
611
+ FROM ph_releases.release_line_events
612
+ WHERE project_id = ${projectId}::uuid
613
+ AND release_line_id = ${releaseLineId}::uuid
614
+ ),
615
+ target_versions AS (
616
+ SELECT id
617
+ FROM ph_releases.release_versions
618
+ WHERE project_id = ${projectId}::uuid
619
+ AND release_line_id = ${releaseLineId}::uuid
620
+ )
621
+ DELETE FROM ph_releases.annotation_tasks task
622
+ WHERE task.release_line_event_id IN (SELECT id FROM target_events)
623
+ OR task.release_version_id IN (SELECT id FROM target_versions)
624
+ `);
625
+ await tx
626
+ .delete(releaseLineEvents)
627
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId)));
628
+ await tx
629
+ .delete(releaseVersions)
630
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseVersions.projectId, projectId), (0, drizzle_orm_1.eq)(releaseVersions.releaseLineId, releaseLineId)));
631
+ const deleted = await tx
632
+ .delete(releaseLines)
633
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLines.projectId, projectId), (0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId)))
634
+ .returning({ id: releaseLines.id });
635
+ return deleted.length;
636
+ });
637
+ }
638
+ async promoteCanaryEvent(line, canary, actorUserId) {
639
+ const promoted = await this.record(resetRuntimeStats({
640
+ ...eventDtoToSnapshot(line, canary),
641
+ laneType: 'production',
642
+ operation: 'promote_canary',
643
+ status: 'running',
644
+ terminalReason: null,
645
+ sourceEventId: canary.id,
646
+ trafficMode: null,
647
+ trafficRatio: null,
648
+ submitReason: promotionSubmitReason(line),
649
+ createdBy: actorUserId,
650
+ createdAt: new Date(),
651
+ updatedAt: new Date(),
652
+ legacySource: null,
653
+ legacySourceId: null,
654
+ }));
655
+ await this.completeCanaryEvent(canary.id, 'promoted');
656
+ return this.findById(line.projectId, promoted.id);
657
+ }
185
658
  async updateActiveLaneRunConfig(projectId, releaseLineId, input, actorUserId) {
186
659
  const line = await this.findById(projectId, releaseLineId);
187
660
  if (!line)
@@ -197,15 +670,20 @@ let ReleaseLineRepository = class ReleaseLineRepository {
197
670
  : null;
198
671
  if (input.modelId && input.modelId !== event.modelId && (!nextModel || !nextModel.isActive))
199
672
  return null;
673
+ const nextRunConfig = inheritCanaryStopConditions(input.laneType, event.runConfig, input.runConfig);
674
+ const releaseVersionId = nextModel || hasTemperatureChanged(event.runConfig, nextRunConfig) ? null : event.releaseVersionId;
200
675
  const updated = await this.record(resetRuntimeStats({
201
676
  ...eventDtoToSnapshot(line, event),
677
+ releaseVersionId,
202
678
  operation: 'config_changed',
203
679
  terminalReason: null,
204
680
  supersedesEventId: event.id,
205
681
  modelId: nextModel?.id ?? event.modelId,
206
682
  modelName: nextModel?.name ?? event.modelName,
207
683
  modelProvider: nextModel?.providerType ?? event.modelProvider,
208
- runConfig: input.runConfig,
684
+ runConfig: nextRunConfig,
685
+ recordMode: input.recordMode ?? event.recordMode,
686
+ recordCategories: normalizeRecordCategoriesForMode(input.recordMode ?? event.recordMode, input.recordCategories ?? event.recordCategories),
209
687
  submitReason: nextModel && input.laneType === 'production'
210
688
  ? '正式发布模型与运行配置变更'
211
689
  : nextModel
@@ -221,6 +699,88 @@ let ReleaseLineRepository = class ReleaseLineRepository {
221
699
  }));
222
700
  return this.findById(projectId, updated.id);
223
701
  }
702
+ async updateActiveLaneOutputRoute(projectId, releaseLineId, input, actorUserId) {
703
+ const line = await this.findById(projectId, releaseLineId);
704
+ if (!line)
705
+ return null;
706
+ const event = input.laneType === 'production' ? line.currentProductionEvent : line.activeCanaryEvent;
707
+ if (!event)
708
+ return null;
709
+ if (event.status !== 'running' && event.status !== 'stopped')
710
+ return null;
711
+ const now = new Date();
712
+ const outputMappingChanged = JSON.stringify(event.outputMapping ?? []) !== JSON.stringify(input.outputMapping);
713
+ const updated = await this.record(resetRuntimeStats({
714
+ ...eventDtoToSnapshot(line, event),
715
+ releaseVersionId: outputMappingChanged ? null : event.releaseVersionId,
716
+ operation: 'config_changed',
717
+ terminalReason: null,
718
+ supersedesEventId: event.id,
719
+ outputConnectorIds: input.outputConnectorIds,
720
+ outputMapping: input.outputMapping,
721
+ submitReason: input.laneType === 'production' ? '正式发布输出路由变更' : '灰度发布输出路由变更',
722
+ createdBy: actorUserId,
723
+ createdAt: now,
724
+ updatedAt: now,
725
+ legacySource: null,
726
+ legacySourceId: null,
727
+ }));
728
+ return this.findById(projectId, updated.id);
729
+ }
730
+ async updateActiveLaneInputRoute(projectId, releaseLineId, input, actorUserId) {
731
+ const line = await this.findById(projectId, releaseLineId);
732
+ if (!line)
733
+ return null;
734
+ const event = input.laneType === 'production' ? line.currentProductionEvent : line.activeCanaryEvent;
735
+ if (!event)
736
+ return null;
737
+ if (event.status !== 'running' && event.status !== 'stopped')
738
+ return null;
739
+ const now = new Date();
740
+ const variableMappingChanged = JSON.stringify(event.variableMapping ?? {}) !== JSON.stringify(input.variableMapping);
741
+ const externalIdFieldChanged = (event.externalIdField ?? '') !== input.externalIdField;
742
+ const updated = await this.record(resetRuntimeStats({
743
+ ...eventDtoToSnapshot(line, event),
744
+ releaseVersionId: variableMappingChanged || externalIdFieldChanged ? null : event.releaseVersionId,
745
+ operation: 'config_changed',
746
+ terminalReason: null,
747
+ supersedesEventId: event.id,
748
+ variableMapping: input.variableMapping,
749
+ filterRules: input.filterRules,
750
+ externalIdField: input.externalIdField,
751
+ submitReason: input.laneType === 'production' ? '正式发布输入路由变更' : '灰度发布输入路由变更',
752
+ createdBy: actorUserId,
753
+ createdAt: now,
754
+ updatedAt: now,
755
+ legacySource: null,
756
+ legacySourceId: null,
757
+ }));
758
+ return this.findById(projectId, updated.id);
759
+ }
760
+ async listConnectorsForProject(projectId, ids) {
761
+ if (ids.length === 0)
762
+ return [];
763
+ return this.db
764
+ .select({ id: connectors.id, name: connectors.name, type: connectors.type, direction: connectors.direction })
765
+ .from(connectors)
766
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(connectors.projectId, projectId), (0, drizzle_orm_1.inArray)(connectors.id, ids), (0, drizzle_orm_1.isNull)(connectors.deletedAt)));
767
+ }
768
+ async clearPromptProductionVersion(promptId) {
769
+ if (!promptId)
770
+ return;
771
+ await this.db
772
+ .update(prompts)
773
+ .set({ currentOnlineVersionId: null, updatedAt: new Date() })
774
+ .where((0, drizzle_orm_1.eq)(prompts.id, promptId));
775
+ }
776
+ async setPromptProductionVersion(projectId, promptId, promptVersionId) {
777
+ if (!promptId || !promptVersionId)
778
+ return;
779
+ await this.db
780
+ .update(prompts)
781
+ .set({ currentOnlineVersionId: promptVersionId, updatedAt: new Date() })
782
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(prompts.projectId, projectId), (0, drizzle_orm_1.eq)(prompts.id, promptId)));
783
+ }
224
784
  async findModelSnapshotForProject(projectId, modelId) {
225
785
  const rows = await this.db
226
786
  .select({
@@ -275,11 +835,11 @@ let ReleaseLineRepository = class ReleaseLineRepository {
275
835
  .from(releaseLineEvents)
276
836
  .where((0, drizzle_orm_1.inArray)(releaseLineEvents.id, Array.from(eventIds)))
277
837
  : [];
278
- const variantRows = await this.db
838
+ const versionRows = await this.db
279
839
  .select()
280
- .from(releaseVariants)
281
- .where((0, drizzle_orm_1.inArray)(releaseVariants.releaseLineId, lines.map((line) => line.id)))
282
- .orderBy(releaseVariants.variantNumber);
840
+ .from(releaseVersions)
841
+ .where((0, drizzle_orm_1.inArray)(releaseVersions.releaseLineId, lines.map((line) => line.id)))
842
+ .orderBy(releaseVersions.targetProductionVersionNumber, releaseVersions.kind, releaseVersions.candidateNumber);
283
843
  const hydratedEvents = await this.hydrateEvents([...latestRows, ...explicitEvents]);
284
844
  const eventById = new Map(hydratedEvents.map((event) => [event.id, event]));
285
845
  const latestByLine = new Map();
@@ -287,11 +847,11 @@ let ReleaseLineRepository = class ReleaseLineRepository {
287
847
  if (!latestByLine.has(event.releaseLineId))
288
848
  latestByLine.set(event.releaseLineId, event);
289
849
  }
290
- const variantsByLine = new Map();
291
- for (const variant of variantRows) {
292
- const list = variantsByLine.get(variant.releaseLineId) ?? [];
293
- list.push(toReleaseVariantDto(variant));
294
- variantsByLine.set(variant.releaseLineId, list);
850
+ const versionsByLine = new Map();
851
+ for (const version of versionRows) {
852
+ const list = versionsByLine.get(version.releaseLineId) ?? [];
853
+ list.push(toReleaseVersionDto(version));
854
+ versionsByLine.set(version.releaseLineId, list);
295
855
  }
296
856
  return lines.map((line) => {
297
857
  const currentProductionEvent = line.currentProductionEventId
@@ -315,7 +875,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
315
875
  activeCanaryEventId: line.activeCanaryEventId,
316
876
  currentProductionEvent,
317
877
  activeCanaryEvent,
318
- variants: variantsByLine.get(line.id) ?? [],
878
+ versions: versionsByLine.get(line.id) ?? [],
319
879
  outputConnectors: mergeOutputConnectors(currentProductionEvent, activeCanaryEvent),
320
880
  latestEvent: latestByLine.get(line.id) ?? null,
321
881
  createdBy: line.createdBy,
@@ -331,15 +891,15 @@ let ReleaseLineRepository = class ReleaseLineRepository {
331
891
  const outputIds = new Set();
332
892
  const sourceEventIds = new Set();
333
893
  const eventIds = new Set();
334
- const variantIds = new Set();
894
+ const versionIds = new Set();
335
895
  for (const row of rows) {
336
896
  eventIds.add(row.id);
337
897
  for (const id of row.outputConnectorIds ?? [])
338
898
  outputIds.add(id);
339
899
  if (row.sourceEventId)
340
900
  sourceEventIds.add(row.sourceEventId);
341
- if (row.releaseVariantId)
342
- variantIds.add(row.releaseVariantId);
901
+ if (row.releaseVersionId)
902
+ versionIds.add(row.releaseVersionId);
343
903
  }
344
904
  const outputMap = new Map();
345
905
  if (outputIds.size > 0) {
@@ -367,14 +927,14 @@ let ReleaseLineRepository = class ReleaseLineRepository {
367
927
  });
368
928
  }
369
929
  }
370
- const variantMap = new Map();
371
- if (variantIds.size > 0) {
372
- const variantRows = await this.db
930
+ const versionMap = new Map();
931
+ if (versionIds.size > 0) {
932
+ const versionRows = await this.db
373
933
  .select()
374
- .from(releaseVariants)
375
- .where((0, drizzle_orm_1.inArray)(releaseVariants.id, Array.from(variantIds)));
376
- for (const variant of variantRows)
377
- variantMap.set(variant.id, variant);
934
+ .from(releaseVersions)
935
+ .where((0, drizzle_orm_1.inArray)(releaseVersions.id, Array.from(versionIds)));
936
+ for (const version of versionRows)
937
+ versionMap.set(version.id, version);
378
938
  }
379
939
  const annotationTaskMap = new Map();
380
940
  if (eventIds.size > 0) {
@@ -389,7 +949,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
389
949
  }
390
950
  }
391
951
  return rows.map((row) => {
392
- const variant = row.releaseVariantId ? (variantMap.get(row.releaseVariantId) ?? null) : null;
952
+ const version = row.releaseVersionId ? (versionMap.get(row.releaseVersionId) ?? null) : null;
393
953
  const outputSnapshots = asArrayOfRecords(row.outputConnectorSnapshots);
394
954
  const outputConnectors = (row.outputConnectorIds ?? []).map((id) => {
395
955
  const fromJoin = outputMap.get(id);
@@ -407,9 +967,12 @@ let ReleaseLineRepository = class ReleaseLineRepository {
407
967
  id: row.id,
408
968
  projectId: row.projectId,
409
969
  releaseLineId: row.releaseLineId,
410
- releaseVariantId: row.releaseVariantId,
411
- releaseVariantNumber: variant?.variantNumber ?? null,
412
- releaseVariantLabel: variant ? formatReleaseVariantLabel(variant.variantNumber) : null,
970
+ releaseVersionId: row.releaseVersionId,
971
+ releaseVersionKind: version?.kind ?? null,
972
+ releaseVersionLabel: version ? formatReleaseVersionLabel(version) : null,
973
+ releaseVersionProductionNumber: version?.productionVersionNumber ?? null,
974
+ releaseVersionTargetProductionNumber: version?.targetProductionVersionNumber ?? null,
975
+ releaseVersionCandidateNumber: version?.candidateNumber ?? null,
413
976
  annotationTaskId: annotationTaskMap.get(row.id) ?? null,
414
977
  laneType: row.laneType,
415
978
  operation: row.operation,
@@ -447,6 +1010,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
447
1010
  outputMapping: row.outputMapping,
448
1011
  filterRules: row.filterRules,
449
1012
  recordMode: row.recordMode,
1013
+ recordCategories: row.recordCategories ?? [],
450
1014
  externalIdField: row.externalIdField,
451
1015
  retentionDays: row.retentionDays,
452
1016
  sourceExperimentId: row.sourceExperimentId,
@@ -502,7 +1066,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
502
1066
  inputConnectorName: snapshot.inputConnectorName,
503
1067
  inputConnectorType: snapshot.inputConnectorType,
504
1068
  inputConnectorSnapshot: connectorSnapshot(snapshot.inputConnectorId, snapshot.inputConnectorName, snapshot.inputConnectorType),
505
- status: snapshot.laneType === 'production' ? 'production' : 'canary',
1069
+ status: snapshot.status === 'running' ? 'running' : 'stopped',
506
1070
  createdBy: snapshot.createdBy,
507
1071
  createdAt: snapshot.createdAt ?? now,
508
1072
  updatedAt: now,
@@ -513,41 +1077,48 @@ let ReleaseLineRepository = class ReleaseLineRepository {
513
1077
  throw new Error('release_lines insert returned no row');
514
1078
  return line;
515
1079
  }
516
- async findOrCreateVariant(tx, releaseLineId, snapshot, now) {
1080
+ async resolveReleaseVersion(tx, releaseLineId, snapshot, now) {
517
1081
  if (!snapshot.promptVersionId || !snapshot.modelId)
518
1082
  return null;
519
- const existing = await tx
520
- .select()
521
- .from(releaseVariants)
522
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseVariants.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseVariants.promptVersionId, snapshot.promptVersionId), (0, drizzle_orm_1.eq)(releaseVariants.modelId, snapshot.modelId)))
523
- .limit(1);
524
- const found = existing[0];
525
- if (found) {
526
- const updated = await tx
527
- .update(releaseVariants)
528
- .set({
529
- promptName: snapshot.promptName,
530
- promptVersionNumber: snapshot.promptVersionNumber ?? null,
531
- promptSnapshot: snapshot.promptSnapshot,
532
- promptVersionSnapshot: snapshot.promptVersionSnapshot,
533
- modelSnapshot: modelSnapshot(snapshot.modelId, snapshot.modelName, snapshot.modelProvider),
534
- updatedAt: now,
535
- })
536
- .where((0, drizzle_orm_1.eq)(releaseVariants.id, found.id))
537
- .returning();
538
- return updated[0] ?? found;
1083
+ if (shouldReuseReleaseVersion(snapshot) && snapshot.releaseVersionId) {
1084
+ const existing = await tx
1085
+ .select()
1086
+ .from(releaseVersions)
1087
+ .where((0, drizzle_orm_1.eq)(releaseVersions.id, snapshot.releaseVersionId))
1088
+ .limit(1);
1089
+ if (existing[0])
1090
+ return existing[0];
539
1091
  }
540
- const maxRows = await tx
541
- .select({ maxVariantNumber: (0, drizzle_orm_1.sql) `MAX(${releaseVariants.variantNumber})::int` })
542
- .from(releaseVariants)
543
- .where((0, drizzle_orm_1.eq)(releaseVariants.releaseLineId, releaseLineId));
544
- const variantNumber = Number(maxRows[0]?.maxVariantNumber ?? 0) + 1;
1092
+ const promotedFromReleaseVersionId = snapshot.operation === 'promote_canary'
1093
+ ? (snapshot.releaseVersionId ?? (await this.findEventReleaseVersionId(tx, snapshot.sourceEventId)))
1094
+ : null;
1095
+ return this.createReleaseVersion(tx, releaseLineId, snapshot, promotedFromReleaseVersionId, now);
1096
+ }
1097
+ async createReleaseVersion(tx, releaseLineId, snapshot, promotedFromReleaseVersionId, now) {
1098
+ if (!snapshot.promptVersionId || !snapshot.modelId)
1099
+ return null;
1100
+ const kind = snapshot.laneType === 'canary' ? 'candidate' : 'production';
1101
+ const maxProductionRows = await tx
1102
+ .select({
1103
+ maxProductionVersionNumber: (0, drizzle_orm_1.sql) `MAX(${releaseVersions.productionVersionNumber})::int`,
1104
+ })
1105
+ .from(releaseVersions)
1106
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseVersions.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseVersions.kind, 'production')));
1107
+ const nextProductionVersionNumber = Number(maxProductionRows[0]?.maxProductionVersionNumber ?? 0) + 1;
1108
+ const targetProductionVersionNumber = kind === 'production'
1109
+ ? nextProductionVersionNumber
1110
+ : Number(maxProductionRows[0]?.maxProductionVersionNumber ?? 0) + 1;
1111
+ const candidateNumber = kind === 'candidate' ? await this.nextCandidateNumber(tx, releaseLineId, targetProductionVersionNumber) : null;
545
1112
  const inserted = await tx
546
- .insert(releaseVariants)
1113
+ .insert(releaseVersions)
547
1114
  .values({
548
1115
  projectId: snapshot.projectId,
549
1116
  releaseLineId,
550
- variantNumber,
1117
+ kind,
1118
+ productionVersionNumber: kind === 'production' ? nextProductionVersionNumber : null,
1119
+ targetProductionVersionNumber,
1120
+ candidateNumber,
1121
+ promotedFromReleaseVersionId,
551
1122
  promptId: snapshot.promptId ?? null,
552
1123
  promptName: snapshot.promptName,
553
1124
  promptVersionId: snapshot.promptVersionId,
@@ -563,6 +1134,24 @@ let ReleaseLineRepository = class ReleaseLineRepository {
563
1134
  .returning();
564
1135
  return inserted[0] ?? null;
565
1136
  }
1137
+ async nextCandidateNumber(tx, releaseLineId, targetProductionVersionNumber) {
1138
+ const maxCandidateRows = await tx
1139
+ .select()
1140
+ .from(releaseVersions)
1141
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseVersions.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseVersions.kind, 'candidate'), (0, drizzle_orm_1.eq)(releaseVersions.targetProductionVersionNumber, targetProductionVersionNumber)));
1142
+ const maxCandidateNumber = maxCandidateRows.reduce((max, row) => Math.max(max, row.candidateNumber ?? 0), 0);
1143
+ return maxCandidateNumber + 1;
1144
+ }
1145
+ async findEventReleaseVersionId(tx, eventId) {
1146
+ if (!eventId)
1147
+ return null;
1148
+ const rows = await tx
1149
+ .select({ releaseVersionId: releaseLineEvents.releaseVersionId })
1150
+ .from(releaseLineEvents)
1151
+ .where((0, drizzle_orm_1.eq)(releaseLineEvents.id, eventId))
1152
+ .limit(1);
1153
+ return rows[0]?.releaseVersionId ?? null;
1154
+ }
566
1155
  async findExistingLegacyEvent(tx, snapshot) {
567
1156
  if (!snapshot.legacySource || !snapshot.legacySourceId)
568
1157
  return null;
@@ -573,7 +1162,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
573
1162
  .limit(1);
574
1163
  return rows[0] ?? null;
575
1164
  }
576
- async buildEventInsert(snapshot, releaseLineId, supersedesEventId, releaseVariantId, now) {
1165
+ async buildEventInsert(snapshot, releaseLineId, supersedesEventId, releaseVersionId, now) {
577
1166
  const outputSnapshots = await this.loadOutputConnectorSnapshots(snapshot.outputConnectorIds);
578
1167
  return {
579
1168
  projectId: snapshot.projectId,
@@ -587,7 +1176,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
587
1176
  rollbackTargetEventId: snapshot.rollbackTargetEventId ?? null,
588
1177
  legacySource: snapshot.legacySource ?? null,
589
1178
  legacySourceId: snapshot.legacySourceId ?? null,
590
- releaseVariantId,
1179
+ releaseVersionId,
591
1180
  promptId: snapshot.promptId ?? null,
592
1181
  promptName: snapshot.promptName,
593
1182
  promptVersionId: snapshot.promptVersionId ?? null,
@@ -607,6 +1196,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
607
1196
  outputMapping: (snapshot.outputMapping ?? []),
608
1197
  filterRules: snapshot.filterRules,
609
1198
  recordMode: snapshot.recordMode,
1199
+ recordCategories: normalizeRecordCategoriesForMode(snapshot.recordMode, snapshot.recordCategories),
610
1200
  externalIdField: snapshot.externalIdField ?? null,
611
1201
  retentionDays: snapshot.retentionDays ?? null,
612
1202
  sourceExperimentId: snapshot.sourceExperimentId ?? null,
@@ -627,21 +1217,34 @@ let ReleaseLineRepository = class ReleaseLineRepository {
627
1217
  };
628
1218
  }
629
1219
  async updateLinePointers(tx, releaseLineId, event, now) {
1220
+ const lineRows = await tx
1221
+ .select({ status: releaseLines.status, archivedAt: releaseLines.archivedAt })
1222
+ .from(releaseLines)
1223
+ .where((0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId))
1224
+ .limit(1);
630
1225
  const productionRows = await tx
631
- .select({ id: releaseLineEvents.id })
1226
+ .select({ id: releaseLineEvents.id, status: releaseLineEvents.status })
632
1227
  .from(releaseLineEvents)
633
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'production'), (0, drizzle_orm_1.eq)(releaseLineEvents.status, 'running')))
1228
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'production'), (0, drizzle_orm_1.sql) `${releaseLineEvents.status} IN ('running', 'stopped', 'archived')`))
634
1229
  .orderBy((0, drizzle_orm_1.desc)(releaseLineEvents.createdAt))
635
1230
  .limit(1);
636
1231
  const canaryRows = await tx
637
- .select({ id: releaseLineEvents.id })
1232
+ .select({ id: releaseLineEvents.id, status: releaseLineEvents.status })
638
1233
  .from(releaseLineEvents)
639
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'canary'), (0, drizzle_orm_1.sql) `${releaseLineEvents.status} IN ('running', 'stopped')`))
1234
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(releaseLineEvents.releaseLineId, releaseLineId), (0, drizzle_orm_1.eq)(releaseLineEvents.laneType, 'canary'), (0, drizzle_orm_1.sql) `${releaseLineEvents.status} IN ('running', 'stopped', 'archived')`))
640
1235
  .orderBy((0, drizzle_orm_1.desc)(releaseLineEvents.createdAt))
641
1236
  .limit(1);
642
1237
  const currentProductionEventId = productionRows[0]?.id ?? null;
643
1238
  const activeCanaryEventId = canaryRows[0]?.id ?? null;
644
- const status = lineStatus(currentProductionEventId, activeCanaryEventId);
1239
+ // Mirror release-runner.repository.refreshLinePointersByEvent: an archived line is
1240
+ // non-runnable and must NEVER be silently resurrected by a new mirror event. The only
1241
+ // legitimate departure from 'archived' is an explicit unarchive (operation 'unarchive_line',
1242
+ // which records a 'stopped' event); every other event leaves status/archivedAt untouched.
1243
+ const lineCurrentlyArchived = (lineRows[0]?.status ?? null) === 'archived' && event.operation !== 'unarchive_line';
1244
+ const status = lineCurrentlyArchived
1245
+ ? 'archived'
1246
+ : lineStatus(productionRows[0]?.status ?? null, canaryRows[0]?.status ?? null);
1247
+ const archivedAt = lineCurrentlyArchived ? (lineRows[0]?.archivedAt ?? null) : null;
645
1248
  await tx
646
1249
  .update(releaseLines)
647
1250
  .set({
@@ -649,7 +1252,7 @@ let ReleaseLineRepository = class ReleaseLineRepository {
649
1252
  activeCanaryEventId,
650
1253
  status,
651
1254
  updatedAt: now,
652
- archivedAt: status === 'archived' ? now : null,
1255
+ archivedAt,
653
1256
  })
654
1257
  .where((0, drizzle_orm_1.eq)(releaseLines.id, releaseLineId));
655
1258
  }
@@ -670,15 +1273,35 @@ exports.ReleaseLineRepository = ReleaseLineRepository = __decorate([
670
1273
  __param(0, (0, common_1.Inject)(database_constants_1.DATABASE_CLIENT)),
671
1274
  __metadata("design:paramtypes", [Object])
672
1275
  ], ReleaseLineRepository);
673
- function lineStatus(currentProductionEventId, activeCanaryEventId) {
674
- if (currentProductionEventId && activeCanaryEventId)
675
- return 'production_with_canary';
676
- if (currentProductionEventId)
677
- return 'production';
678
- if (activeCanaryEventId)
679
- return 'canary';
1276
+ function lineStatus(productionStatus, activeCanaryStatus) {
1277
+ if (productionStatus === 'running' || activeCanaryStatus === 'running')
1278
+ return 'running';
680
1279
  return 'stopped';
681
1280
  }
1281
+ function findResumableEvents(line) {
1282
+ const slotEvents = findVisibleSlotEvents(line).filter((event) => event.status === 'stopped');
1283
+ if (slotEvents.length > 0)
1284
+ return slotEvents;
1285
+ return line.latestEvent?.status === 'stopped' ? [line.latestEvent] : [];
1286
+ }
1287
+ function findVisibleSlotEvents(line) {
1288
+ const events = [line.currentProductionEvent, line.activeCanaryEvent].filter((event) => Boolean(event));
1289
+ return dedupeEvents(events);
1290
+ }
1291
+ function findArchivedSlotEvents(line) {
1292
+ return findVisibleSlotEvents(line).filter((event) => event.status === 'archived');
1293
+ }
1294
+ function dedupeEvents(events) {
1295
+ const seen = new Set();
1296
+ const result = [];
1297
+ for (const event of events) {
1298
+ if (seen.has(event.id))
1299
+ continue;
1300
+ seen.add(event.id);
1301
+ result.push(event);
1302
+ }
1303
+ return result;
1304
+ }
682
1305
  function asRecord(value) {
683
1306
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
684
1307
  }
@@ -687,6 +1310,13 @@ function asArrayOfRecords(value) {
687
1310
  ? value.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
688
1311
  : [];
689
1312
  }
1313
+ function inheritCanaryStopConditions(laneType, previousRunConfig, nextRunConfig) {
1314
+ const next = asRecord(nextRunConfig);
1315
+ if (laneType !== 'canary' || Object.prototype.hasOwnProperty.call(next, 'stopConditions'))
1316
+ return next;
1317
+ const previousStopConditions = asRecord(previousRunConfig)['stopConditions'];
1318
+ return previousStopConditions === undefined ? next : { ...next, stopConditions: previousStopConditions };
1319
+ }
690
1320
  function stringFromSnapshot(snapshot, key) {
691
1321
  const value = asRecord(snapshot)[key];
692
1322
  return typeof value === 'string' ? value : null;
@@ -708,31 +1338,39 @@ function connectorSnapshot(id, name, type) {
708
1338
  function modelSnapshot(id, name, providerType) {
709
1339
  return { id, name, providerType };
710
1340
  }
711
- function formatReleaseVariantLabel(variantNumber) {
712
- return `#${variantNumber}`;
1341
+ function formatReleaseVersionLabel(version) {
1342
+ if (version.kind === 'production')
1343
+ return `v${version.productionVersionNumber ?? version.targetProductionVersionNumber}`;
1344
+ const baseProductionNumber = Math.max(0, version.targetProductionVersionNumber - 1);
1345
+ return `v${baseProductionNumber}.${version.candidateNumber ?? 0}`;
713
1346
  }
714
- function toReleaseVariantDto(variant) {
715
- const promptVersionNumber = variant.promptVersionNumber ?? numberFromSnapshot(variant.promptVersionSnapshot, 'versionNumber');
1347
+ function toReleaseVersionDto(version) {
1348
+ const promptVersionNumber = version.promptVersionNumber ?? numberFromSnapshot(version.promptVersionSnapshot, 'versionNumber');
716
1349
  return {
717
- id: variant.id,
718
- projectId: variant.projectId,
719
- releaseLineId: variant.releaseLineId,
720
- variantNumber: variant.variantNumber,
721
- label: formatReleaseVariantLabel(variant.variantNumber),
722
- promptId: variant.promptId,
723
- promptName: variant.promptName,
724
- promptVersionId: variant.promptVersionId,
1350
+ id: version.id,
1351
+ projectId: version.projectId,
1352
+ releaseLineId: version.releaseLineId,
1353
+ kind: version.kind,
1354
+ productionVersionNumber: version.productionVersionNumber,
1355
+ targetProductionVersionNumber: version.targetProductionVersionNumber,
1356
+ candidateNumber: version.candidateNumber,
1357
+ promotedFromReleaseVersionId: version.promotedFromReleaseVersionId,
1358
+ label: formatReleaseVersionLabel(version),
1359
+ promptId: version.promptId,
1360
+ promptName: version.promptName,
1361
+ promptVersionId: version.promptVersionId,
725
1362
  promptVersionNumber,
726
1363
  promptVersionLabel: promptVersionNumber ? `v${promptVersionNumber}` : null,
727
- promptSnapshot: asRecord(variant.promptSnapshot),
728
- promptVersionSnapshot: asRecord(variant.promptVersionSnapshot),
729
- modelId: variant.modelId,
730
- modelName: stringFromSnapshot(variant.modelSnapshot, 'name'),
731
- modelProvider: stringFromSnapshot(variant.modelSnapshot, 'providerType') ?? stringFromSnapshot(variant.modelSnapshot, 'provider'),
732
- modelSnapshot: asRecord(variant.modelSnapshot),
733
- createdBy: variant.createdBy,
734
- createdAt: variant.createdAt.toISOString(),
735
- updatedAt: variant.updatedAt.toISOString(),
1364
+ promptSnapshot: asRecord(version.promptSnapshot),
1365
+ promptVersionSnapshot: asRecord(version.promptVersionSnapshot),
1366
+ modelId: version.modelId,
1367
+ modelName: stringFromSnapshot(version.modelSnapshot, 'name'),
1368
+ modelProvider: stringFromSnapshot(version.modelSnapshot, 'providerType') ??
1369
+ stringFromSnapshot(version.modelSnapshot, 'provider'),
1370
+ modelSnapshot: asRecord(version.modelSnapshot),
1371
+ createdBy: version.createdBy,
1372
+ createdAt: version.createdAt.toISOString(),
1373
+ updatedAt: version.updatedAt.toISOString(),
736
1374
  };
737
1375
  }
738
1376
  function mergeOutputConnectors(production, canary) {
@@ -743,6 +1381,20 @@ function mergeOutputConnectors(production, canary) {
743
1381
  map.set(connector.id, connector);
744
1382
  return [...map.values()];
745
1383
  }
1384
+ function normalizeRecordCategoriesForMode(mode, categories) {
1385
+ if (mode === 'all')
1386
+ return [];
1387
+ const seen = new Set();
1388
+ const normalized = [];
1389
+ for (const category of categories ?? []) {
1390
+ const trimmed = category.trim();
1391
+ if (!trimmed || seen.has(trimmed))
1392
+ continue;
1393
+ seen.add(trimmed);
1394
+ normalized.push(trimmed);
1395
+ }
1396
+ return normalized;
1397
+ }
746
1398
  function eventDtoToSnapshot(line, event) {
747
1399
  return {
748
1400
  projectId: event.projectId,
@@ -775,6 +1427,7 @@ function eventDtoToSnapshot(line, event) {
775
1427
  outputMapping: event.outputMapping,
776
1428
  filterRules: event.filterRules,
777
1429
  recordMode: event.recordMode,
1430
+ recordCategories: event.recordCategories,
778
1431
  externalIdField: event.externalIdField,
779
1432
  retentionDays: event.retentionDays,
780
1433
  sourceExperimentId: event.sourceExperimentId,
@@ -785,6 +1438,7 @@ function eventDtoToSnapshot(line, event) {
785
1438
  totalFiltered: event.totalFiltered,
786
1439
  totalCorrect: event.totalCorrect,
787
1440
  totalErrors: event.totalErrors,
1441
+ releaseVersionId: event.releaseVersionId,
788
1442
  controlState: event.controlState,
789
1443
  controlStatePayload: event.controlStatePayload,
790
1444
  startedAt: event.startedAt ? new Date(event.startedAt) : null,
@@ -824,6 +1478,30 @@ function resetRuntimeStats(snapshot) {
824
1478
  totalErrors: 0,
825
1479
  };
826
1480
  }
1481
+ function shouldReuseReleaseVersion(snapshot) {
1482
+ if (!snapshot.releaseVersionId)
1483
+ return false;
1484
+ return ([
1485
+ 'traffic_updated',
1486
+ 'mode_updated',
1487
+ 'stop_lane',
1488
+ 'resume_lane',
1489
+ 'cancel_canary',
1490
+ 'force_stop',
1491
+ 'archive_line',
1492
+ 'unarchive_line',
1493
+ ].includes(snapshot.operation) || snapshot.operation === 'config_changed');
1494
+ }
1495
+ function productionOperationReleasesCanarySlot(operation) {
1496
+ return operation === 'rollback' || operation === 'restore_to_production';
1497
+ }
1498
+ function hasTemperatureChanged(previousRunConfig, nextRunConfig) {
1499
+ const previous = asRecord(previousRunConfig)['temperature'];
1500
+ const next = asRecord(nextRunConfig)['temperature'];
1501
+ if (previous === undefined && next === undefined)
1502
+ return false;
1503
+ return Number(previous) !== Number(next);
1504
+ }
827
1505
  function releaseLineIdentityCondition(identity) {
828
1506
  if (identity.inputConnectorId)
829
1507
  return (0, drizzle_orm_1.eq)(releaseLines.inputConnectorId, identity.inputConnectorId);
@@ -831,4 +1509,12 @@ function releaseLineIdentityCondition(identity) {
831
1509
  return (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNull)(releaseLines.inputConnectorId), (0, drizzle_orm_1.eq)(releaseLines.promptId, identity.promptId));
832
1510
  return (0, drizzle_orm_1.isNull)(releaseLines.inputConnectorId);
833
1511
  }
1512
+ function unwrapRows(result) {
1513
+ if (Array.isArray(result))
1514
+ return result;
1515
+ if (result && typeof result === 'object' && 'rows' in result) {
1516
+ return (result.rows ?? []);
1517
+ }
1518
+ return [];
1519
+ }
834
1520
  //# sourceMappingURL=release-line.repository.js.map