@proofhound/core 0.1.7 → 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 (199) 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/common/contracts/index.d.ts +1 -0
  24. package/dist/server/common/contracts/index.d.ts.map +1 -1
  25. package/dist/server/common/contracts/index.js +1 -0
  26. package/dist/server/common/contracts/index.js.map +1 -1
  27. package/dist/server/common/contracts/local-contracts.module.d.ts.map +1 -1
  28. package/dist/server/common/contracts/local-contracts.module.js +3 -0
  29. package/dist/server/common/contracts/local-contracts.module.js.map +1 -1
  30. package/dist/server/common/contracts/usage-metering.hook.d.ts +33 -0
  31. package/dist/server/common/contracts/usage-metering.hook.d.ts.map +1 -0
  32. package/dist/server/common/contracts/usage-metering.hook.js +43 -0
  33. package/dist/server/common/contracts/usage-metering.hook.js.map +1 -0
  34. package/dist/server/infrastructure/llm/run-result-writer.d.ts +4 -1
  35. package/dist/server/infrastructure/llm/run-result-writer.d.ts.map +1 -1
  36. package/dist/server/infrastructure/llm/run-result-writer.js +66 -14
  37. package/dist/server/infrastructure/llm/run-result-writer.js.map +1 -1
  38. package/dist/server/modules/annotation/annotation.controller.d.ts +28 -13
  39. package/dist/server/modules/annotation/annotation.controller.d.ts.map +1 -1
  40. package/dist/server/modules/annotation/annotation.repository.d.ts +6 -2
  41. package/dist/server/modules/annotation/annotation.repository.d.ts.map +1 -1
  42. package/dist/server/modules/annotation/annotation.repository.js +340 -96
  43. package/dist/server/modules/annotation/annotation.repository.js.map +1 -1
  44. package/dist/server/modules/annotation/annotation.service.d.ts.map +1 -1
  45. package/dist/server/modules/annotation/annotation.service.js +62 -10
  46. package/dist/server/modules/annotation/annotation.service.js.map +1 -1
  47. package/dist/server/modules/canary-release/canary-release.controller.d.ts +63 -42
  48. package/dist/server/modules/canary-release/canary-release.controller.d.ts.map +1 -1
  49. package/dist/server/modules/canary-release/canary-release.repository.d.ts +23 -5
  50. package/dist/server/modules/canary-release/canary-release.repository.d.ts.map +1 -1
  51. package/dist/server/modules/canary-release/canary-release.repository.js +28 -12
  52. package/dist/server/modules/canary-release/canary-release.repository.js.map +1 -1
  53. package/dist/server/modules/canary-release/canary-release.service.d.ts.map +1 -1
  54. package/dist/server/modules/canary-release/canary-release.service.js +32 -10
  55. package/dist/server/modules/canary-release/canary-release.service.js.map +1 -1
  56. package/dist/server/modules/canary-release/canary-runtime.d.ts +11 -1
  57. package/dist/server/modules/canary-release/canary-runtime.d.ts.map +1 -1
  58. package/dist/server/modules/canary-release/canary-runtime.js +63 -8
  59. package/dist/server/modules/canary-release/canary-runtime.js.map +1 -1
  60. package/dist/server/modules/dataset/dataset-deletion.hook.d.ts +16 -0
  61. package/dist/server/modules/dataset/dataset-deletion.hook.d.ts.map +1 -0
  62. package/dist/server/modules/dataset/dataset-deletion.hook.js +57 -0
  63. package/dist/server/modules/dataset/dataset-deletion.hook.js.map +1 -0
  64. package/dist/server/modules/dataset/dataset-import.controller.d.ts +2 -0
  65. package/dist/server/modules/dataset/dataset-import.controller.d.ts.map +1 -1
  66. package/dist/server/modules/dataset/dataset-import.service.d.ts.map +1 -1
  67. package/dist/server/modules/dataset/dataset-import.service.js +7 -0
  68. package/dist/server/modules/dataset/dataset-import.service.js.map +1 -1
  69. package/dist/server/modules/dataset/dataset.controller.d.ts +98 -0
  70. package/dist/server/modules/dataset/dataset.controller.d.ts.map +1 -1
  71. package/dist/server/modules/dataset/dataset.controller.js +36 -0
  72. package/dist/server/modules/dataset/dataset.controller.js.map +1 -1
  73. package/dist/server/modules/dataset/dataset.module.d.ts.map +1 -1
  74. package/dist/server/modules/dataset/dataset.module.js +8 -1
  75. package/dist/server/modules/dataset/dataset.module.js.map +1 -1
  76. package/dist/server/modules/dataset/dataset.repository.d.ts +19 -0
  77. package/dist/server/modules/dataset/dataset.repository.d.ts.map +1 -1
  78. package/dist/server/modules/dataset/dataset.repository.js +248 -9
  79. package/dist/server/modules/dataset/dataset.repository.js.map +1 -1
  80. package/dist/server/modules/dataset/dataset.service.d.ts +45 -1
  81. package/dist/server/modules/dataset/dataset.service.d.ts.map +1 -1
  82. package/dist/server/modules/dataset/dataset.service.js +132 -7
  83. package/dist/server/modules/dataset/dataset.service.js.map +1 -1
  84. package/dist/server/modules/experiment/experiment.controller.d.ts +8 -8
  85. package/dist/server/modules/experiment/experiment.repository.d.ts.map +1 -1
  86. package/dist/server/modules/experiment/experiment.repository.js +28 -0
  87. package/dist/server/modules/experiment/experiment.repository.js.map +1 -1
  88. package/dist/server/modules/experiment/experiment.service.d.ts.map +1 -1
  89. package/dist/server/modules/experiment/experiment.service.js +6 -3
  90. package/dist/server/modules/experiment/experiment.service.js.map +1 -1
  91. package/dist/server/modules/model/model.service.d.ts +5 -1
  92. package/dist/server/modules/model/model.service.d.ts.map +1 -1
  93. package/dist/server/modules/model/model.service.js +65 -3
  94. package/dist/server/modules/model/model.service.js.map +1 -1
  95. package/dist/server/modules/model/project-model.controller.d.ts +5 -5
  96. package/dist/server/modules/monitoring/monitoring.repository.js +1 -1
  97. package/dist/server/modules/optimization/optimization.controller.d.ts +12 -12
  98. package/dist/server/modules/optimization/optimization.repository.d.ts +6 -0
  99. package/dist/server/modules/optimization/optimization.repository.d.ts.map +1 -1
  100. package/dist/server/modules/optimization/optimization.repository.js +96 -4
  101. package/dist/server/modules/optimization/optimization.repository.js.map +1 -1
  102. package/dist/server/modules/optimization/optimization.service.d.ts.map +1 -1
  103. package/dist/server/modules/optimization/optimization.service.js +13 -4
  104. package/dist/server/modules/optimization/optimization.service.js.map +1 -1
  105. package/dist/server/modules/optimization/optimization.workflow.js +1 -1
  106. package/dist/server/modules/optimization/optimization.workflow.js.map +1 -1
  107. package/dist/server/modules/production-release/production-release.controller.d.ts +12 -9
  108. package/dist/server/modules/production-release/production-release.controller.d.ts.map +1 -1
  109. package/dist/server/modules/production-release/production-release.repository.d.ts +2 -1
  110. package/dist/server/modules/production-release/production-release.repository.d.ts.map +1 -1
  111. package/dist/server/modules/production-release/production-release.repository.js +3 -1
  112. package/dist/server/modules/production-release/production-release.repository.js.map +1 -1
  113. package/dist/server/modules/production-release/production-release.service.d.ts.map +1 -1
  114. package/dist/server/modules/production-release/production-release.service.js +10 -1
  115. package/dist/server/modules/production-release/production-release.service.js.map +1 -1
  116. package/dist/server/modules/prompt/prompt-deletion.hook.d.ts +18 -0
  117. package/dist/server/modules/prompt/prompt-deletion.hook.d.ts.map +1 -0
  118. package/dist/server/modules/prompt/prompt-deletion.hook.js +69 -0
  119. package/dist/server/modules/prompt/prompt-deletion.hook.js.map +1 -0
  120. package/dist/server/modules/prompt/prompt.controller.d.ts +146 -38
  121. package/dist/server/modules/prompt/prompt.controller.d.ts.map +1 -1
  122. package/dist/server/modules/prompt/prompt.controller.js +24 -0
  123. package/dist/server/modules/prompt/prompt.controller.js.map +1 -1
  124. package/dist/server/modules/prompt/prompt.module.d.ts.map +1 -1
  125. package/dist/server/modules/prompt/prompt.module.js +7 -1
  126. package/dist/server/modules/prompt/prompt.module.js.map +1 -1
  127. package/dist/server/modules/prompt/prompt.repository.d.ts +33 -3
  128. package/dist/server/modules/prompt/prompt.repository.d.ts.map +1 -1
  129. package/dist/server/modules/prompt/prompt.repository.js +267 -39
  130. package/dist/server/modules/prompt/prompt.repository.js.map +1 -1
  131. package/dist/server/modules/prompt/prompt.service.d.ts +78 -6
  132. package/dist/server/modules/prompt/prompt.service.d.ts.map +1 -1
  133. package/dist/server/modules/prompt/prompt.service.js +79 -49
  134. package/dist/server/modules/prompt/prompt.service.js.map +1 -1
  135. package/dist/server/modules/quick-start/quick-start.controller.d.ts +1 -1
  136. package/dist/server/modules/quick-start/quick-start.service.d.ts +1 -1
  137. package/dist/server/modules/release-line/release-line-deletion.hook.d.ts +16 -0
  138. package/dist/server/modules/release-line/release-line-deletion.hook.d.ts.map +1 -0
  139. package/dist/server/modules/release-line/release-line-deletion.hook.js +60 -0
  140. package/dist/server/modules/release-line/release-line-deletion.hook.js.map +1 -0
  141. package/dist/server/modules/release-line/release-line.controller.d.ts +2503 -82
  142. package/dist/server/modules/release-line/release-line.controller.d.ts.map +1 -1
  143. package/dist/server/modules/release-line/release-line.controller.js +169 -0
  144. package/dist/server/modules/release-line/release-line.controller.js.map +1 -1
  145. package/dist/server/modules/release-line/release-line.module.d.ts.map +1 -1
  146. package/dist/server/modules/release-line/release-line.module.js +8 -1
  147. package/dist/server/modules/release-line/release-line.module.js.map +1 -1
  148. package/dist/server/modules/release-line/release-line.repository.d.ts +55 -3
  149. package/dist/server/modules/release-line/release-line.repository.d.ts.map +1 -1
  150. package/dist/server/modules/release-line/release-line.repository.js +797 -111
  151. package/dist/server/modules/release-line/release-line.repository.js.map +1 -1
  152. package/dist/server/modules/release-line/release-line.service.d.ts +25 -5
  153. package/dist/server/modules/release-line/release-line.service.d.ts.map +1 -1
  154. package/dist/server/modules/release-line/release-line.service.js +312 -4
  155. package/dist/server/modules/release-line/release-line.service.js.map +1 -1
  156. package/dist/server/modules/release-line/release-runner.repository.d.ts +2 -1
  157. package/dist/server/modules/release-line/release-runner.repository.d.ts.map +1 -1
  158. package/dist/server/modules/release-line/release-runner.repository.js +14 -10
  159. package/dist/server/modules/release-line/release-runner.repository.js.map +1 -1
  160. package/dist/server/modules/release-line/release-runner.service.d.ts +6 -2
  161. package/dist/server/modules/release-line/release-runner.service.d.ts.map +1 -1
  162. package/dist/server/modules/release-line/release-runner.service.js +138 -11
  163. package/dist/server/modules/release-line/release-runner.service.js.map +1 -1
  164. package/dist/server/modules/release-line/release-variable-mapping.d.ts +9 -0
  165. package/dist/server/modules/release-line/release-variable-mapping.d.ts.map +1 -0
  166. package/dist/server/modules/release-line/release-variable-mapping.js +83 -0
  167. package/dist/server/modules/release-line/release-variable-mapping.js.map +1 -0
  168. package/dist/server/modules/run-result/run-result.controller.d.ts +10 -7
  169. package/dist/server/modules/run-result/run-result.controller.d.ts.map +1 -1
  170. package/dist/server/modules/run-result/run-result.repository.d.ts.map +1 -1
  171. package/dist/server/modules/run-result/run-result.repository.js +43 -18
  172. package/dist/server/modules/run-result/run-result.repository.js.map +1 -1
  173. package/dist/webhook/channels/webhook/webhook.controller.d.ts +4 -0
  174. package/dist/webhook/channels/webhook/webhook.controller.d.ts.map +1 -1
  175. package/dist/webhook/channels/webhook/webhook.service.d.ts +2 -0
  176. package/dist/webhook/channels/webhook/webhook.service.d.ts.map +1 -1
  177. package/dist/webhook/channels/webhook/webhook.service.js +6 -0
  178. package/dist/webhook/channels/webhook/webhook.service.js.map +1 -1
  179. package/dist/worker/consumers/llm.consumer.d.ts +4 -1
  180. package/dist/worker/consumers/llm.consumer.d.ts.map +1 -1
  181. package/dist/worker/consumers/llm.consumer.js +41 -6
  182. package/dist/worker/consumers/llm.consumer.js.map +1 -1
  183. package/dist/worker/consumers/probe.consumer.d.ts +4 -1
  184. package/dist/worker/consumers/probe.consumer.d.ts.map +1 -1
  185. package/dist/worker/consumers/probe.consumer.js +35 -3
  186. package/dist/worker/consumers/probe.consumer.js.map +1 -1
  187. package/dist/worker/runners/llm-runner.d.ts +3 -1
  188. package/dist/worker/runners/llm-runner.d.ts.map +1 -1
  189. package/dist/worker/runners/llm-runner.js +110 -55
  190. package/dist/worker/runners/llm-runner.js.map +1 -1
  191. package/dist/worker/runners/probe-runner.d.ts +9 -2
  192. package/dist/worker/runners/probe-runner.d.ts.map +1 -1
  193. package/dist/worker/runners/probe-runner.js +46 -2
  194. package/dist/worker/runners/probe-runner.js.map +1 -1
  195. package/dist/worker/runners/run-result-writer.d.ts +4 -1
  196. package/dist/worker/runners/run-result-writer.d.ts.map +1 -1
  197. package/dist/worker/runners/run-result-writer.js +63 -13
  198. package/dist/worker/runners/run-result-writer.js.map +1 -1
  199. package/package.json +12 -12
@@ -39,28 +39,78 @@ let AnnotationRepository = class AnnotationRepository {
39
39
  line.status AS release_line_status,
40
40
  line.prompt_name AS prompt_name,
41
41
  line.input_connector_name AS input_connector_name,
42
- variant.id AS release_variant_id,
43
- variant.variant_number,
44
- variant.prompt_version_id,
45
- variant.prompt_version_number,
46
- variant.prompt_version_snapshot,
47
- COALESCE(variant.model_snapshot->>'name', model.name) AS model_name,
42
+ version.id AS release_version_id,
43
+ version.kind AS release_version_kind,
44
+ version.production_version_number,
45
+ version.target_production_version_number,
46
+ version.candidate_number,
47
+ version.prompt_version_id,
48
+ version.prompt_version_number,
49
+ version.prompt_version_snapshot,
50
+ COALESCE(version.model_snapshot->>'name', model.name) AS model_name,
48
51
  COALESCE(
49
- variant.model_snapshot->>'providerType',
50
- variant.model_snapshot->>'provider',
52
+ version.model_snapshot->>'providerType',
53
+ version.model_snapshot->>'provider',
51
54
  model.provider_type
52
55
  ) AS model_provider,
53
- variant.model_id,
56
+ version.model_id,
57
+ COUNT(rr.id)::int AS run_result_count,
54
58
  COUNT(rr.id) FILTER (WHERE event.lane_type = 'canary')::int AS canary_count,
55
- COUNT(rr.id) FILTER (WHERE event.lane_type = 'production')::int AS online_count
59
+ COUNT(rr.id) FILTER (WHERE event.lane_type = 'production')::int AS online_count,
60
+ (
61
+ SELECT COALESCE(jsonb_object_agg(category_counts.category, category_counts.total), '{}'::jsonb)
62
+ FROM (
63
+ SELECT category_rr.decision_output AS category, COUNT(*)::int AS total
64
+ FROM ph_runs.run_results category_rr
65
+ INNER JOIN ph_releases.release_line_events category_event
66
+ ON category_event.id = category_rr.source_id
67
+ AND category_event.project_id = category_rr.project_id
68
+ INNER JOIN ph_releases.release_versions category_version
69
+ ON category_version.id = COALESCE(category_rr.release_version_id, category_event.release_version_id)
70
+ WHERE category_rr.project_id = line.project_id
71
+ AND category_rr.source = 'release'
72
+ AND category_event.release_line_id = line.id
73
+ AND category_version.id = version.id
74
+ AND category_rr.decision_output IS NOT NULL
75
+ GROUP BY category_rr.decision_output
76
+ ) category_counts
77
+ ) AS category_counts,
78
+ (
79
+ SELECT COUNT(*)::int
80
+ FROM ph_runs.run_results journey_rr
81
+ INNER JOIN ph_releases.release_line_events journey_event
82
+ ON journey_event.id = journey_rr.source_id
83
+ AND journey_event.project_id = journey_rr.project_id
84
+ INNER JOIN ph_releases.release_versions journey_version
85
+ ON journey_version.id = COALESCE(journey_rr.release_version_id, journey_event.release_version_id)
86
+ WHERE journey_rr.project_id = line.project_id
87
+ AND journey_rr.source = 'release'
88
+ AND journey_event.release_line_id = line.id
89
+ AND journey_event.lane_type = 'canary'
90
+ AND journey_version.target_production_version_number = version.target_production_version_number
91
+ ) AS journey_canary_count,
92
+ (
93
+ SELECT COUNT(*)::int
94
+ FROM ph_runs.run_results journey_rr
95
+ INNER JOIN ph_releases.release_line_events journey_event
96
+ ON journey_event.id = journey_rr.source_id
97
+ AND journey_event.project_id = journey_rr.project_id
98
+ INNER JOIN ph_releases.release_versions journey_version
99
+ ON journey_version.id = COALESCE(journey_rr.release_version_id, journey_event.release_version_id)
100
+ WHERE journey_rr.project_id = line.project_id
101
+ AND journey_rr.source = 'release'
102
+ AND journey_event.release_line_id = line.id
103
+ AND journey_event.lane_type = 'production'
104
+ AND journey_version.target_production_version_number = version.target_production_version_number
105
+ ) AS journey_online_count
56
106
  FROM ph_releases.release_lines line
57
- INNER JOIN ph_releases.release_variants variant
58
- ON variant.release_line_id = line.id
59
- AND variant.project_id = line.project_id
60
- LEFT JOIN ph_assets.models model ON model.id = variant.model_id
107
+ INNER JOIN ph_releases.release_versions version
108
+ ON version.release_line_id = line.id
109
+ AND version.project_id = line.project_id
110
+ LEFT JOIN ph_assets.models model ON model.id = version.model_id
61
111
  LEFT JOIN ph_releases.release_line_events event
62
112
  ON event.release_line_id = line.id
63
- AND event.release_variant_id = variant.id
113
+ AND event.release_version_id = version.id
64
114
  AND event.project_id = line.project_id
65
115
  LEFT JOIN ph_runs.run_results rr
66
116
  ON rr.source = 'release'
@@ -74,16 +124,19 @@ let AnnotationRepository = class AnnotationRepository {
74
124
  line.status,
75
125
  line.prompt_name,
76
126
  line.input_connector_name,
77
- variant.id,
78
- variant.variant_number,
79
- variant.prompt_version_id,
80
- variant.prompt_version_number,
81
- variant.prompt_version_snapshot,
82
- variant.model_id,
83
- variant.model_snapshot,
127
+ version.id,
128
+ version.kind,
129
+ version.production_version_number,
130
+ version.target_production_version_number,
131
+ version.candidate_number,
132
+ version.prompt_version_id,
133
+ version.prompt_version_number,
134
+ version.prompt_version_snapshot,
135
+ version.model_id,
136
+ version.model_snapshot,
84
137
  model.name,
85
138
  model.provider_type
86
- ORDER BY line.updated_at DESC, variant.variant_number ASC
139
+ ORDER BY line.updated_at DESC, version.target_production_version_number ASC, version.kind DESC, version.candidate_number ASC
87
140
  `);
88
141
  const byLine = new Map();
89
142
  for (const row of unwrapRows(rows)) {
@@ -94,64 +147,125 @@ let AnnotationRepository = class AnnotationRepository {
94
147
  status: String(row['release_line_status'] ?? ''),
95
148
  promptName: String(row['prompt_name'] ?? ''),
96
149
  inputConnectorName: row['input_connector_name'] ?? null,
97
- variants: [],
150
+ versions: [],
98
151
  };
99
- const variantNumber = Number(row['variant_number'] ?? 0);
100
- line.variants.push({
101
- id: row['release_variant_id'],
152
+ const categoryOptions = (0, shared_1.deriveClassificationOptionsFromPromptVersionSnapshot)(row['prompt_version_snapshot']);
153
+ line.versions.push({
154
+ id: row['release_version_id'],
102
155
  releaseLineId: lineId,
103
- label: formatVariantLabel(variantNumber),
156
+ label: formatReleaseVersionLabel(row),
157
+ kind: row['release_version_kind'],
158
+ productionVersionNumber: toNumberOrNull(row['production_version_number']),
159
+ targetProductionVersionNumber: Number(row['target_production_version_number'] ?? 1),
160
+ candidateNumber: toNumberOrNull(row['candidate_number']),
104
161
  promptVersionId: row['prompt_version_id'],
105
162
  promptVersionNumber: toNumberOrNull(row['prompt_version_number']),
106
163
  promptVersionLabel: formatPromptVersionLabel(row['prompt_version_number']),
107
- categoryOptions: (0, shared_1.deriveClassificationOptionsFromPromptVersionSnapshot)(row['prompt_version_snapshot']),
164
+ categoryOptions,
108
165
  modelId: row['model_id'],
109
166
  modelName: row['model_name'] ?? null,
110
167
  modelProvider: row['model_provider'] ?? null,
168
+ runResultCount: Number(row['run_result_count'] ?? 0),
111
169
  canaryCount: Number(row['canary_count'] ?? 0),
112
170
  onlineCount: Number(row['online_count'] ?? 0),
171
+ journeyCanaryCount: Number(row['journey_canary_count'] ?? 0),
172
+ journeyOnlineCount: Number(row['journey_online_count'] ?? 0),
173
+ journeyCompatible: true,
174
+ categoryCounts: parseCategoryCounts(row['category_counts'], categoryOptions),
113
175
  });
114
176
  byLine.set(lineId, line);
115
177
  }
178
+ for (const line of byLine.values()) {
179
+ const targetCategoryKeys = new Map();
180
+ for (const version of line.versions) {
181
+ const currentKey = categoryOptionsKey(version.categoryOptions);
182
+ const previousKey = targetCategoryKeys.get(version.targetProductionVersionNumber);
183
+ targetCategoryKeys.set(version.targetProductionVersionNumber, previousKey === undefined || previousKey === currentKey ? currentKey : null);
184
+ }
185
+ for (const version of line.versions) {
186
+ version.journeyCompatible =
187
+ targetCategoryKeys.get(version.targetProductionVersionNumber) === categoryOptionsKey(version.categoryOptions);
188
+ }
189
+ }
116
190
  return Array.from(byLine.values());
117
191
  }
118
- async countMatchingRunResults(projectId, releaseLineId, releaseVariantId, scope) {
192
+ async countMatchingRunResults(projectId, releaseLineId, releaseVersionId, releaseVersionScope, scope) {
193
+ const versionFilter = releaseVersionFilterSql(releaseVersionId, releaseVersionScope);
194
+ const scopeFilter = scopeFilterSql(scope);
119
195
  const rows = await this.db.execute((0, drizzle_orm_1.sql) `
120
196
  SELECT COUNT(*)::int AS total
121
197
  FROM ph_runs.run_results rr
122
198
  INNER JOIN ph_releases.release_line_events event
123
199
  ON event.id = rr.source_id
124
200
  AND event.project_id = rr.project_id
201
+ LEFT JOIN ph_releases.release_versions version
202
+ ON version.id = COALESCE(rr.release_version_id, event.release_version_id)
125
203
  WHERE rr.project_id = ${projectId}::uuid
126
204
  AND rr.source = 'release'
127
205
  AND event.release_line_id = ${releaseLineId}::uuid
128
- AND event.lane_type = ${scopeToLane(scope)}
129
- AND COALESCE(rr.release_variant_id, event.release_variant_id) = ${releaseVariantId}::uuid
206
+ AND ${scopeFilter}
207
+ AND ${versionFilter}
130
208
  `);
131
209
  return Number(unwrapRows(rows)[0]?.['total'] ?? 0);
132
210
  }
133
- async findVariantCategoryOptions(projectId, releaseLineId, releaseVariantId) {
211
+ async countMatchingRunResultsByCategory(projectId, releaseLineId, releaseVersionId, releaseVersionScope, scope) {
212
+ const versionFilter = releaseVersionFilterSql(releaseVersionId, releaseVersionScope);
213
+ const scopeFilter = scopeFilterSql(scope);
214
+ const rows = await this.db.execute((0, drizzle_orm_1.sql) `
215
+ SELECT rr.decision_output AS category, COUNT(*)::int AS total
216
+ FROM ph_runs.run_results rr
217
+ INNER JOIN ph_releases.release_line_events event
218
+ ON event.id = rr.source_id
219
+ AND event.project_id = rr.project_id
220
+ LEFT JOIN ph_releases.release_versions version
221
+ ON version.id = COALESCE(rr.release_version_id, event.release_version_id)
222
+ WHERE rr.project_id = ${projectId}::uuid
223
+ AND rr.source = 'release'
224
+ AND event.release_line_id = ${releaseLineId}::uuid
225
+ AND ${scopeFilter}
226
+ AND ${versionFilter}
227
+ AND rr.decision_output IS NOT NULL
228
+ GROUP BY rr.decision_output
229
+ `);
230
+ return new Map(unwrapRows(rows).map((row) => [String(row['category']), Number(row['total'] ?? 0)]));
231
+ }
232
+ async findReleaseVersionCategoryOptions(projectId, releaseLineId, releaseVersionId, releaseVersionScope) {
233
+ const versionFilter = releaseVersionScope === 'journey'
234
+ ? (0, drizzle_orm_1.sql) `version.target_production_version_number = selected.target_production_version_number`
235
+ : (0, drizzle_orm_1.sql) `version.id = selected.id`;
134
236
  const rows = await this.db.execute((0, drizzle_orm_1.sql) `
135
- SELECT variant.prompt_version_snapshot
136
- FROM ph_releases.release_variants variant
237
+ SELECT version.prompt_version_snapshot
238
+ FROM ph_releases.release_versions selected
239
+ INNER JOIN ph_releases.release_versions version
240
+ ON version.project_id = selected.project_id
241
+ AND version.release_line_id = selected.release_line_id
137
242
  INNER JOIN ph_releases.release_lines line
138
- ON line.id = variant.release_line_id
139
- AND line.project_id = variant.project_id
140
- WHERE variant.project_id = ${projectId}::uuid
141
- AND variant.id = ${releaseVariantId}::uuid
142
- AND variant.release_line_id = ${releaseLineId}::uuid
243
+ ON line.id = version.release_line_id
244
+ AND line.project_id = version.project_id
245
+ WHERE selected.project_id = ${projectId}::uuid
246
+ AND selected.id = ${releaseVersionId}::uuid
247
+ AND selected.release_line_id = ${releaseLineId}::uuid
248
+ AND ${versionFilter}
143
249
  AND line.status <> 'archived'
144
- LIMIT 1
250
+ ORDER BY version.kind, version.candidate_number NULLS FIRST, version.production_version_number NULLS LAST
145
251
  `);
146
- return (0, shared_1.deriveClassificationOptionsFromPromptVersionSnapshot)(unwrapRows(rows)[0]?.['prompt_version_snapshot']);
252
+ const optionSets = unwrapRows(rows).map((row) => (0, shared_1.deriveClassificationOptionsFromPromptVersionSnapshot)(row['prompt_version_snapshot']));
253
+ if (optionSets.length === 0)
254
+ return { options: [], compatible: true };
255
+ const firstKey = categoryOptionsKey(optionSets[0] ?? []);
256
+ const compatible = optionSets.every((options) => categoryOptionsKey(options) === firstKey);
257
+ return { options: compatible ? (optionSets[0] ?? []) : [], compatible };
147
258
  }
148
259
  async createTask(projectId, input, actorUserId, availableCount, categoryOptions) {
149
260
  const annotationSchema = JSON.stringify(buildAnnotationSchema(categoryOptions));
261
+ const requestedSampleSize = getRequestedSampleSize(input);
262
+ const categorySampleCounts = getPositiveCategorySampleCounts(input);
150
263
  return this.db.transaction(async (tx) => {
151
264
  const taskRows = await tx.execute((0, drizzle_orm_1.sql) `
152
265
  INSERT INTO ph_releases.annotation_tasks (
153
266
  scope,
154
- release_variant_id,
267
+ release_version_id,
268
+ release_version_scope,
155
269
  name,
156
270
  annotation_schema,
157
271
  sampling_config,
@@ -164,15 +278,19 @@ let AnnotationRepository = class AnnotationRepository {
164
278
  )
165
279
  SELECT
166
280
  ${input.scope},
167
- variant.id,
281
+ version.id,
282
+ ${input.releaseVersionScope},
168
283
  ${input.name},
169
284
  ${annotationSchema}::jsonb,
170
285
  ${JSON.stringify({
171
286
  releaseLineId: input.releaseLineId,
172
- releaseVariantId: input.releaseVariantId,
287
+ releaseVersionId: input.releaseVersionId,
288
+ releaseVersionScope: input.releaseVersionScope,
173
289
  scope: input.scope,
290
+ samplingMode: input.samplingMode,
174
291
  availableCount,
175
- sampleSize: input.sampleSize,
292
+ sampleSize: requestedSampleSize,
293
+ categorySampleCounts,
176
294
  })}::jsonb,
177
295
  0,
178
296
  0,
@@ -180,13 +298,13 @@ let AnnotationRepository = class AnnotationRepository {
180
298
  ${actorUserId}::uuid,
181
299
  NOW(),
182
300
  NOW()
183
- FROM ph_releases.release_variants variant
301
+ FROM ph_releases.release_versions version
184
302
  INNER JOIN ph_releases.release_lines line
185
- ON line.id = variant.release_line_id
186
- AND line.project_id = variant.project_id
187
- WHERE variant.project_id = ${projectId}::uuid
188
- AND variant.id = ${input.releaseVariantId}::uuid
189
- AND variant.release_line_id = ${input.releaseLineId}::uuid
303
+ ON line.id = version.release_line_id
304
+ AND line.project_id = version.project_id
305
+ WHERE version.project_id = ${projectId}::uuid
306
+ AND version.id = ${input.releaseVersionId}::uuid
307
+ AND version.release_line_id = ${input.releaseLineId}::uuid
190
308
  AND line.status <> 'archived'
191
309
  RETURNING id
192
310
  `);
@@ -194,20 +312,7 @@ let AnnotationRepository = class AnnotationRepository {
194
312
  if (!taskId)
195
313
  throw new Error('annotation_task_source_not_found');
196
314
  const insertedRows = await tx.execute((0, drizzle_orm_1.sql) `
197
- WITH candidates AS (
198
- SELECT rr.id, rr.created_at
199
- FROM ph_runs.run_results rr
200
- INNER JOIN ph_releases.release_line_events event
201
- ON event.id = rr.source_id
202
- AND event.project_id = rr.project_id
203
- WHERE rr.project_id = ${projectId}::uuid
204
- AND rr.source = 'release'
205
- AND event.release_line_id = ${input.releaseLineId}::uuid
206
- AND event.lane_type = ${scopeToLane(input.scope)}
207
- AND COALESCE(rr.release_variant_id, event.release_variant_id) = ${input.releaseVariantId}::uuid
208
- ORDER BY rr.created_at DESC
209
- LIMIT ${input.sampleSize}
210
- ),
315
+ WITH ${sampleCandidateCtesSql(projectId, input, requestedSampleSize, categorySampleCounts)},
211
316
  inserted AS (
212
317
  INSERT INTO ph_runs.annotations (
213
318
  run_result_id,
@@ -243,11 +348,11 @@ let AnnotationRepository = class AnnotationRepository {
243
348
  });
244
349
  }
245
350
  async listTasks(projectId) {
246
- const rows = await this.db.execute(taskSelectSql((0, drizzle_orm_1.sql) `variant.project_id = ${projectId}::uuid`));
351
+ const rows = await this.db.execute(taskSelectSql((0, drizzle_orm_1.sql) `version.project_id = ${projectId}::uuid`));
247
352
  return unwrapRows(rows).map(mapTaskRow);
248
353
  }
249
354
  async findTask(projectId, taskId) {
250
- const rows = await this.db.execute(taskSelectSql((0, drizzle_orm_1.sql) `variant.project_id = ${projectId}::uuid AND task.id = ${taskId}::uuid`));
355
+ const rows = await this.db.execute(taskSelectSql((0, drizzle_orm_1.sql) `version.project_id = ${projectId}::uuid AND task.id = ${taskId}::uuid`));
251
356
  return unwrapRows(rows).map(mapTaskRow)[0] ?? null;
252
357
  }
253
358
  async listSamples(taskId, filter) {
@@ -383,9 +488,10 @@ function taskSelectSql(whereSql) {
383
488
  return (0, drizzle_orm_1.sql) `
384
489
  SELECT
385
490
  task.id,
386
- variant.project_id,
491
+ version.project_id,
387
492
  task.name,
388
493
  task.scope,
494
+ task.release_version_scope,
389
495
  task.status,
390
496
  task.annotation_schema,
391
497
  task.created_by,
@@ -393,17 +499,20 @@ function taskSelectSql(whereSql) {
393
499
  task.updated_at,
394
500
  line.id AS release_line_id,
395
501
  line.name AS release_line_name,
396
- variant.id AS release_variant_id,
397
- variant.variant_number,
398
- variant.prompt_name,
399
- variant.prompt_version_id,
400
- variant.prompt_version_number,
401
- variant.prompt_version_snapshot,
402
- variant.model_id,
403
- COALESCE(variant.model_snapshot->>'name', model.name) AS model_name,
502
+ version.id AS release_version_id,
503
+ version.kind AS release_version_kind,
504
+ version.production_version_number,
505
+ version.target_production_version_number,
506
+ version.candidate_number,
507
+ version.prompt_name,
508
+ version.prompt_version_id,
509
+ version.prompt_version_number,
510
+ version.prompt_version_snapshot,
511
+ version.model_id,
512
+ COALESCE(version.model_snapshot->>'name', model.name) AS model_name,
404
513
  COALESCE(
405
- variant.model_snapshot->>'providerType',
406
- variant.model_snapshot->>'provider',
514
+ version.model_snapshot->>'providerType',
515
+ version.model_snapshot->>'provider',
407
516
  model.provider_type
408
517
  ) AS model_provider,
409
518
  COUNT(annotation.id)::int AS total,
@@ -425,17 +534,18 @@ function taskSelectSql(whereSql) {
425
534
  AND (rr.decision_output IS NULL OR annotation.fields->>'expected_output' <> rr.decision_output)
426
535
  )::int AS mismatched
427
536
  FROM ph_releases.annotation_tasks task
428
- INNER JOIN ph_releases.release_variants variant ON variant.id = task.release_variant_id
429
- INNER JOIN ph_releases.release_lines line ON line.id = variant.release_line_id
430
- LEFT JOIN ph_assets.models model ON model.id = variant.model_id
537
+ INNER JOIN ph_releases.release_versions version ON version.id = task.release_version_id
538
+ INNER JOIN ph_releases.release_lines line ON line.id = version.release_line_id
539
+ LEFT JOIN ph_assets.models model ON model.id = version.model_id
431
540
  LEFT JOIN ph_runs.annotations annotation ON annotation.task_id = task.id
432
541
  LEFT JOIN ph_runs.run_results rr ON rr.id = annotation.run_result_id
433
542
  WHERE ${whereSql}
434
543
  GROUP BY
435
544
  task.id,
436
- variant.project_id,
545
+ version.project_id,
437
546
  task.name,
438
547
  task.scope,
548
+ task.release_version_scope,
439
549
  task.status,
440
550
  task.annotation_schema,
441
551
  task.created_by,
@@ -443,14 +553,17 @@ function taskSelectSql(whereSql) {
443
553
  task.updated_at,
444
554
  line.id,
445
555
  line.name,
446
- variant.id,
447
- variant.variant_number,
448
- variant.prompt_name,
449
- variant.prompt_version_id,
450
- variant.prompt_version_number,
451
- variant.prompt_version_snapshot,
452
- variant.model_id,
453
- variant.model_snapshot,
556
+ version.id,
557
+ version.kind,
558
+ version.production_version_number,
559
+ version.target_production_version_number,
560
+ version.candidate_number,
561
+ version.prompt_name,
562
+ version.prompt_version_id,
563
+ version.prompt_version_number,
564
+ version.prompt_version_snapshot,
565
+ version.model_id,
566
+ version.model_snapshot,
454
567
  model.name,
455
568
  model.provider_type
456
569
  ORDER BY task.created_at DESC
@@ -510,7 +623,6 @@ function mapTaskRow(row) {
510
623
  const matched = Number(row['matched'] ?? 0);
511
624
  const mismatched = Number(row['mismatched'] ?? 0);
512
625
  const judged = matched + mismatched;
513
- const variantNumber = Number(row['variant_number'] ?? 0);
514
626
  return {
515
627
  id: row['id'],
516
628
  projectId: row['project_id'],
@@ -518,8 +630,9 @@ function mapTaskRow(row) {
518
630
  scope: row['scope'],
519
631
  releaseLineId: row['release_line_id'],
520
632
  releaseLineName: String(row['release_line_name'] ?? ''),
521
- releaseVariantId: row['release_variant_id'],
522
- releaseVariantLabel: formatVariantLabel(variantNumber),
633
+ releaseVersionId: row['release_version_id'],
634
+ releaseVersionLabel: formatReleaseVersionLabel(row),
635
+ releaseVersionScope: row['release_version_scope'] ?? 'exact',
523
636
  promptName: String(row['prompt_name'] ?? ''),
524
637
  promptVersionId: row['prompt_version_id'],
525
638
  promptVersionNumber: toNumberOrNull(row['prompt_version_number']),
@@ -583,15 +696,140 @@ function buildAnnotationSchema(categoryOptions) {
583
696
  },
584
697
  ];
585
698
  }
699
+ function sampleCandidateCtesSql(projectId, input, sampleSize, categorySampleCounts) {
700
+ const scopeFilter = scopeFilterSql(input.scope);
701
+ const versionFilter = releaseVersionFilterSql(input.releaseVersionId, input.releaseVersionScope);
702
+ if (input.samplingMode === 'per_category') {
703
+ const categoryRequestsJson = JSON.stringify(categorySampleCounts.map((item) => ({ category: item.category, sample_size: item.sampleSize })));
704
+ return (0, drizzle_orm_1.sql) `
705
+ requested_categories AS (
706
+ SELECT category, sample_size
707
+ FROM jsonb_to_recordset(${categoryRequestsJson}::jsonb) AS requested(category text, sample_size int)
708
+ WHERE sample_size > 0
709
+ ),
710
+ ranked_candidates AS (
711
+ SELECT
712
+ rr.id,
713
+ rr.created_at,
714
+ rr.decision_output,
715
+ ROW_NUMBER() OVER (PARTITION BY rr.decision_output ORDER BY random()) AS category_rank
716
+ FROM ph_runs.run_results rr
717
+ INNER JOIN ph_releases.release_line_events event
718
+ ON event.id = rr.source_id
719
+ AND event.project_id = rr.project_id
720
+ LEFT JOIN ph_releases.release_versions version
721
+ ON version.id = COALESCE(rr.release_version_id, event.release_version_id)
722
+ INNER JOIN requested_categories requested
723
+ ON requested.category = rr.decision_output
724
+ WHERE rr.project_id = ${projectId}::uuid
725
+ AND rr.source = 'release'
726
+ AND event.release_line_id = ${input.releaseLineId}::uuid
727
+ AND ${scopeFilter}
728
+ AND ${versionFilter}
729
+ ),
730
+ candidates AS (
731
+ SELECT ranked_candidates.id, ranked_candidates.created_at
732
+ FROM ranked_candidates
733
+ INNER JOIN requested_categories requested
734
+ ON requested.category = ranked_candidates.decision_output
735
+ WHERE ranked_candidates.category_rank <= requested.sample_size
736
+ )
737
+ `;
738
+ }
739
+ return (0, drizzle_orm_1.sql) `
740
+ candidates AS (
741
+ SELECT rr.id, rr.created_at
742
+ FROM ph_runs.run_results rr
743
+ INNER JOIN ph_releases.release_line_events event
744
+ ON event.id = rr.source_id
745
+ AND event.project_id = rr.project_id
746
+ LEFT JOIN ph_releases.release_versions version
747
+ ON version.id = COALESCE(rr.release_version_id, event.release_version_id)
748
+ WHERE rr.project_id = ${projectId}::uuid
749
+ AND rr.source = 'release'
750
+ AND event.release_line_id = ${input.releaseLineId}::uuid
751
+ AND ${scopeFilter}
752
+ AND ${versionFilter}
753
+ ORDER BY random()
754
+ LIMIT ${sampleSize}
755
+ )
756
+ `;
757
+ }
758
+ function getRequestedSampleSize(input) {
759
+ if (input.samplingMode === 'per_category') {
760
+ return getPositiveCategorySampleCounts(input).reduce((sum, item) => sum + item.sampleSize, 0);
761
+ }
762
+ return input.sampleSize ?? 0;
763
+ }
764
+ function getPositiveCategorySampleCounts(input) {
765
+ return (input.categorySampleCounts ?? []).filter((item) => item.sampleSize > 0);
766
+ }
586
767
  function getTaskCategoryOptions(row) {
587
768
  const fromSchema = (0, shared_1.deriveClassificationOptionsFromAnnotationSchema)(row['annotation_schema']);
588
769
  if (fromSchema.length > 0)
589
770
  return fromSchema;
590
771
  return (0, shared_1.deriveClassificationOptionsFromPromptVersionSnapshot)(row['prompt_version_snapshot']);
591
772
  }
773
+ function scopeFilterSql(scope) {
774
+ if (scope === 'all')
775
+ return (0, drizzle_orm_1.sql) `TRUE`;
776
+ return (0, drizzle_orm_1.sql) `event.lane_type = ${scopeToLane(scope)}`;
777
+ }
592
778
  function scopeToLane(scope) {
593
779
  return scope === 'online' ? 'production' : 'canary';
594
780
  }
781
+ function parseCategoryCounts(value, categoryOptions) {
782
+ const counts = parseCountMap(value);
783
+ const categories = categoryOptions.length > 0 ? categoryOptions : Array.from(counts.keys()).sort();
784
+ return categories.map((category) => ({ category, count: counts.get(category) ?? 0 }));
785
+ }
786
+ function parseCountMap(value) {
787
+ const raw = parseRecord(value);
788
+ return new Map(Object.entries(raw)
789
+ .map(([category, count]) => [category, Number(count)])
790
+ .filter((entry) => entry[0].length > 0 && Number.isFinite(entry[1])));
791
+ }
792
+ function parseRecord(value) {
793
+ if (isRecord(value))
794
+ return value;
795
+ if (typeof value !== 'string')
796
+ return {};
797
+ try {
798
+ const parsed = JSON.parse(value);
799
+ return isRecord(parsed) ? parsed : {};
800
+ }
801
+ catch {
802
+ return {};
803
+ }
804
+ }
805
+ function releaseVersionFilterSql(releaseVersionId, releaseVersionScope) {
806
+ // The version table is LEFT JOINed so that release-line traffic whose
807
+ // release_version_id was nulled out by a run-config / route change (both
808
+ // rr.release_version_id and event.release_version_id NULL) is still reachable.
809
+ // For the version-scoped ('exact') path we restrict to the specific version,
810
+ // and a NULL-version row legitimately does not match it. For the
811
+ // non-version-scoped ('journey') path the task spans the whole release-line
812
+ // journey, so those detached NULL-version rows must be included.
813
+ if (releaseVersionScope === 'exact')
814
+ return (0, drizzle_orm_1.sql) `version.id = ${releaseVersionId}::uuid`;
815
+ return (0, drizzle_orm_1.sql) `(
816
+ version.id IS NULL
817
+ OR version.target_production_version_number = (
818
+ SELECT selected.target_production_version_number
819
+ FROM ph_releases.release_versions selected
820
+ WHERE selected.id = ${releaseVersionId}::uuid
821
+ AND selected.release_line_id = version.release_line_id
822
+ LIMIT 1
823
+ )
824
+ )`;
825
+ }
826
+ function categoryOptionsKey(options) {
827
+ // Canonicalize for set-equality comparison only: identical category sets in
828
+ // different declaration order must produce the same key. This key is used
829
+ // solely for compatibility checks, never to derive the displayed/stored
830
+ // option order (which is preserved from the source snapshot).
831
+ return JSON.stringify([...options].sort());
832
+ }
595
833
  function unwrapRows(result) {
596
834
  if (Array.isArray(result))
597
835
  return result;
@@ -624,8 +862,14 @@ function toNumberOrNull(value) {
624
862
  const n = typeof value === 'number' ? value : Number(value);
625
863
  return Number.isFinite(n) ? n : null;
626
864
  }
627
- function formatVariantLabel(value) {
628
- return value > 0 ? `#${value}` : '#?';
865
+ function formatReleaseVersionLabel(row) {
866
+ const kind = row['release_version_kind'] ?? row['kind'];
867
+ const productionVersionNumber = toNumberOrNull(row['production_version_number']);
868
+ const targetProductionVersionNumber = toNumberOrNull(row['target_production_version_number']) ?? 1;
869
+ const candidateNumber = toNumberOrNull(row['candidate_number']) ?? 0;
870
+ if (kind === 'production')
871
+ return `v${productionVersionNumber ?? targetProductionVersionNumber}`;
872
+ return `v${Math.max(0, targetProductionVersionNumber - 1)}.${candidateNumber}`;
629
873
  }
630
874
  function formatPromptVersionLabel(value) {
631
875
  const num = toNumberOrNull(value);