@cat-factory/app 0.151.1 → 0.152.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/panels/ResultWindowShell.vue +70 -1
- package/app/components/panels/StepEffortReport.vue +16 -9
- package/app/stores/environmentWizard/context.ts +68 -0
- package/app/stores/environmentWizard/flow.ts +143 -0
- package/app/stores/environmentWizard/recipe.ts +63 -0
- package/app/stores/environmentWizard/save.ts +132 -0
- package/app/stores/environmentWizard.ts +46 -278
- package/app/stores/pipelines/context.ts +103 -0
- package/app/stores/pipelines/draftActions.ts +307 -0
- package/app/stores/pipelines/persistence.ts +136 -0
- package/app/stores/pipelines.ts +37 -458
- package/app/utils/effort.spec.ts +30 -0
- package/app/utils/effort.ts +29 -0
- package/package.json +1 -1
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import type { AgentKind, Pipeline } from '~/types/domain'
|
|
3
|
+
import type { ConsensusStepConfig } from '~/types/consensus'
|
|
4
|
+
import type { StepOptions } from '@cat-factory/contracts'
|
|
5
|
+
import { companionForProducer } from '~/utils/catalog'
|
|
6
|
+
import { defaultConsensusConfig, type PipelinesContext } from './context'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The pipeline-builder draft manipulation: inserting/removing/reordering steps and toggling each
|
|
10
|
+
* step's per-step config, plus `clearDraft` / `loadForEdit`. Extracted from the store setup into a
|
|
11
|
+
* factory closing over the shared {@link PipelinesContext} so behaviour is byte-identical to the
|
|
12
|
+
* former in-closure functions — the split is purely to keep the setup within the size budget.
|
|
13
|
+
*/
|
|
14
|
+
export function createPipelineDraftActions(ctx: PipelinesContext) {
|
|
15
|
+
const {
|
|
16
|
+
draft,
|
|
17
|
+
draftGates,
|
|
18
|
+
draftEnabled,
|
|
19
|
+
draftThresholds,
|
|
20
|
+
draftConsensus,
|
|
21
|
+
draftGating,
|
|
22
|
+
draftFollowUps,
|
|
23
|
+
draftTesterQuality,
|
|
24
|
+
draftStepOptions,
|
|
25
|
+
draftLabels,
|
|
26
|
+
draftPurpose,
|
|
27
|
+
draftName,
|
|
28
|
+
draftDescription,
|
|
29
|
+
editingId,
|
|
30
|
+
} = ctx
|
|
31
|
+
|
|
32
|
+
/** Insert a step (with its default per-step config) at `index`, keeping arrays aligned. */
|
|
33
|
+
function insertAt(index: number, kind: AgentKind) {
|
|
34
|
+
draft.value.splice(index, 0, kind)
|
|
35
|
+
draftGates.value.splice(index, 0, false)
|
|
36
|
+
draftEnabled.value.splice(index, 0, true)
|
|
37
|
+
draftThresholds.value.splice(index, 0, null)
|
|
38
|
+
draftConsensus.value.splice(index, 0, null)
|
|
39
|
+
draftGating.value.splice(index, 0, null)
|
|
40
|
+
draftFollowUps.value.splice(index, 0, null)
|
|
41
|
+
draftTesterQuality.value.splice(index, 0, null)
|
|
42
|
+
draftStepOptions.value.splice(index, 0, null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addToDraft(kind: AgentKind) {
|
|
46
|
+
insertAt(draft.value.length, kind)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeFromDraft(index: number) {
|
|
50
|
+
draft.value.splice(index, 1)
|
|
51
|
+
draftGates.value.splice(index, 1)
|
|
52
|
+
draftEnabled.value.splice(index, 1)
|
|
53
|
+
draftThresholds.value.splice(index, 1)
|
|
54
|
+
draftConsensus.value.splice(index, 1)
|
|
55
|
+
draftGating.value.splice(index, 1)
|
|
56
|
+
draftFollowUps.value.splice(index, 1)
|
|
57
|
+
draftTesterQuality.value.splice(index, 1)
|
|
58
|
+
draftStepOptions.value.splice(index, 1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function moveInDraft(from: number, to: number) {
|
|
62
|
+
if (to < 0 || to >= draft.value.length) return
|
|
63
|
+
const [item] = draft.value.splice(from, 1)
|
|
64
|
+
if (item) draft.value.splice(to, 0, item)
|
|
65
|
+
const [gate] = draftGates.value.splice(from, 1)
|
|
66
|
+
draftGates.value.splice(to, 0, gate ?? false)
|
|
67
|
+
const [on] = draftEnabled.value.splice(from, 1)
|
|
68
|
+
draftEnabled.value.splice(to, 0, on ?? true)
|
|
69
|
+
const [th] = draftThresholds.value.splice(from, 1)
|
|
70
|
+
draftThresholds.value.splice(to, 0, th ?? null)
|
|
71
|
+
const [cons] = draftConsensus.value.splice(from, 1)
|
|
72
|
+
draftConsensus.value.splice(to, 0, cons ?? null)
|
|
73
|
+
const [gat] = draftGating.value.splice(from, 1)
|
|
74
|
+
draftGating.value.splice(to, 0, gat ?? null)
|
|
75
|
+
const [fu] = draftFollowUps.value.splice(from, 1)
|
|
76
|
+
draftFollowUps.value.splice(to, 0, fu ?? null)
|
|
77
|
+
const [tq] = draftTesterQuality.value.splice(from, 1)
|
|
78
|
+
draftTesterQuality.value.splice(to, 0, tq ?? null)
|
|
79
|
+
const [so] = draftStepOptions.value.splice(from, 1)
|
|
80
|
+
draftStepOptions.value.splice(to, 0, so ?? null)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether the producer step at `index` currently has its companion attached after it. */
|
|
84
|
+
function hasCompanion(index: number): boolean {
|
|
85
|
+
const companion = companionForProducer(draft.value[index] ?? '')
|
|
86
|
+
return companion !== undefined && draft.value[index + 1] === companion
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Toggle the dependent companion on the producer step at `index`: insert it immediately
|
|
91
|
+
* after (turn on) or remove it (turn off). A no-op for a kind that has no companion.
|
|
92
|
+
*/
|
|
93
|
+
function toggleCompanion(index: number) {
|
|
94
|
+
const companion = companionForProducer(draft.value[index] ?? '')
|
|
95
|
+
if (!companion) return
|
|
96
|
+
if (draft.value[index + 1] === companion) removeFromDraft(index + 1)
|
|
97
|
+
else insertAt(index + 1, companion)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Toggle estimate gating on/off for the (companion) step at `index`. */
|
|
101
|
+
function toggleDraftGating(index: number) {
|
|
102
|
+
draftGating.value[index] = draftGating.value[index]?.enabled
|
|
103
|
+
? null
|
|
104
|
+
: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The draft as a list of "units" for rendering: each step is one unit, EXCEPT a companion
|
|
109
|
+
* that sits immediately after its producer — that companion is folded into the producer's
|
|
110
|
+
* unit (`companionIndex`) and surfaced as a toggle on it, not a standalone row. The backend
|
|
111
|
+
* now REJECTS a companion that is not immediately after its producer (strict adjacency in
|
|
112
|
+
* `validatePipelineShape`), so a saved pipeline never has one — but a stray companion that
|
|
113
|
+
* still shows up in the draft (e.g. a pre-existing pipeline saved before adjacency was
|
|
114
|
+
* enforced) is emitted as its own standalone unit so it stays visible and removable/
|
|
115
|
+
* reorderable into a valid shape rather than being silently dropped — and, crucially, so
|
|
116
|
+
* every `draft` index belongs to exactly one unit, which is what lets {@link moveUnit}
|
|
117
|
+
* reorder by unit boundaries without ever dropping a step.
|
|
118
|
+
* `index`/`companionIndex` are positions in the raw `draft` arrays.
|
|
119
|
+
*/
|
|
120
|
+
const units = computed(() => {
|
|
121
|
+
const out: { index: number; kind: AgentKind; companionIndex: number | null }[] = []
|
|
122
|
+
let folded = -1 // draft index already consumed as the previous unit's adjacent companion
|
|
123
|
+
for (let i = 0; i < draft.value.length; i++) {
|
|
124
|
+
const kind = draft.value[i]
|
|
125
|
+
if (kind === undefined || i === folded) continue
|
|
126
|
+
const companion = companionForProducer(kind)
|
|
127
|
+
const companionIndex = companion && draft.value[i + 1] === companion ? i + 1 : null
|
|
128
|
+
if (companionIndex !== null) folded = companionIndex
|
|
129
|
+
out.push({ index: i, kind, companionIndex })
|
|
130
|
+
}
|
|
131
|
+
return out
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Move the unit at visible position `from` to `to`, carrying its attached companion. Rebuilds
|
|
136
|
+
* every parallel array by the SAME unit boundaries so they stay index-aligned.
|
|
137
|
+
*/
|
|
138
|
+
function moveUnit(from: number, to: number) {
|
|
139
|
+
const u = units.value
|
|
140
|
+
if (to < 0 || to >= u.length || from === to) return
|
|
141
|
+
const reorder = <T>(arr: T[]): T[] => {
|
|
142
|
+
const chunks = u.map((unit) =>
|
|
143
|
+
arr.slice(unit.index, unit.index + (unit.companionIndex !== null ? 2 : 1)),
|
|
144
|
+
)
|
|
145
|
+
const [moved] = chunks.splice(from, 1)
|
|
146
|
+
if (moved) chunks.splice(to, 0, moved)
|
|
147
|
+
return chunks.flat()
|
|
148
|
+
}
|
|
149
|
+
draft.value = reorder(draft.value)
|
|
150
|
+
draftGates.value = reorder(draftGates.value)
|
|
151
|
+
draftEnabled.value = reorder(draftEnabled.value)
|
|
152
|
+
draftThresholds.value = reorder(draftThresholds.value)
|
|
153
|
+
draftConsensus.value = reorder(draftConsensus.value)
|
|
154
|
+
draftGating.value = reorder(draftGating.value)
|
|
155
|
+
draftFollowUps.value = reorder(draftFollowUps.value)
|
|
156
|
+
draftTesterQuality.value = reorder(draftTesterQuality.value)
|
|
157
|
+
draftStepOptions.value = reorder(draftStepOptions.value)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
|
|
161
|
+
function toggleDraftConsensus(index: number) {
|
|
162
|
+
draftConsensus.value[index] = draftConsensus.value[index] ? null : defaultConsensusConfig()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Replace the consensus config of the draft step at `index` (builder editor edits). */
|
|
166
|
+
function setDraftConsensus(index: number, config: ConsensusStepConfig | null) {
|
|
167
|
+
draftConsensus.value[index] = config
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Toggle the approval gate on the draft step at `index`. */
|
|
171
|
+
function toggleDraftGate(index: number) {
|
|
172
|
+
draftGates.value[index] = !draftGates.value[index]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Toggle the Follow-up companion on the draft (coder) step at `index` (default on → off). */
|
|
176
|
+
function toggleDraftFollowUps(index: number) {
|
|
177
|
+
// Default (null/true) is enabled, so the first toggle disables it (false); toggle back to null.
|
|
178
|
+
draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Toggle the test quality-control companion on the draft (Tester) step at `index`. The
|
|
183
|
+
* companion is enabled by default (a `null` entry), so the first toggle disables it
|
|
184
|
+
* (`{ enabled: false }`, dropping any gating) and the next restores the default.
|
|
185
|
+
*/
|
|
186
|
+
function toggleDraftTesterQuality(index: number) {
|
|
187
|
+
draftTesterQuality.value[index] =
|
|
188
|
+
draftTesterQuality.value[index]?.enabled === false ? null : { enabled: false }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Toggle estimate gating on/off for the QC companion on the draft (Tester) step at `index`.
|
|
193
|
+
* A no-op while the companion is disabled (nothing to gate). Enabling gating pins the config
|
|
194
|
+
* to `{ enabled: true, gating }` so the thresholds are editable; disabling drops back to the
|
|
195
|
+
* default `null` (enabled, ungated).
|
|
196
|
+
*/
|
|
197
|
+
function toggleDraftTesterQualityGating(index: number) {
|
|
198
|
+
const cur = draftTesterQuality.value[index]
|
|
199
|
+
if (cur?.enabled === false) return
|
|
200
|
+
draftTesterQuality.value[index] = cur?.gating?.enabled
|
|
201
|
+
? null
|
|
202
|
+
: {
|
|
203
|
+
enabled: true,
|
|
204
|
+
gating: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' },
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Enable/disable the draft step at `index` without removing it. */
|
|
209
|
+
function toggleDraftEnabled(index: number) {
|
|
210
|
+
draftEnabled.value[index] = draftEnabled.value[index] === false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether auto-recommendation is on for the draft (requirements-review) step at `index`. */
|
|
214
|
+
function draftAutoRecommendEnabled(index: number): boolean {
|
|
215
|
+
return draftStepOptions.value[index]?.autoRecommend !== false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Toggle the requirements-review auto-recommendation on the draft step at `index`. It is on by
|
|
220
|
+
* default, so we store ONLY the opt-out (`{ autoRecommend: false }`); toggling back drops the
|
|
221
|
+
* flag. Merges with any other future StepOptions fields rather than clobbering the whole bag.
|
|
222
|
+
*/
|
|
223
|
+
function toggleDraftAutoRecommend(index: number) {
|
|
224
|
+
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
225
|
+
if (draftAutoRecommendEnabled(index)) next.autoRecommend = false
|
|
226
|
+
else delete next.autoRecommend
|
|
227
|
+
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The skill picked for the draft `skill` step at `index` (its `stepOptions.skillId`). */
|
|
231
|
+
function draftSkillId(index: number): string | undefined {
|
|
232
|
+
return draftStepOptions.value[index]?.skillId
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Set (or clear) the picked skill on the draft `skill` step at `index`. Merges into the
|
|
237
|
+
* step's `StepOptions` bag rather than clobbering it; clearing drops the field and, if the
|
|
238
|
+
* bag empties, the whole entry (so it normalizes away like the other options).
|
|
239
|
+
*/
|
|
240
|
+
function setDraftSkillId(index: number, skillId: string | undefined) {
|
|
241
|
+
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
242
|
+
if (skillId) next.skillId = skillId
|
|
243
|
+
else delete next.skillId
|
|
244
|
+
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function clearDraft() {
|
|
248
|
+
draft.value = []
|
|
249
|
+
draftGates.value = []
|
|
250
|
+
draftEnabled.value = []
|
|
251
|
+
draftThresholds.value = []
|
|
252
|
+
draftConsensus.value = []
|
|
253
|
+
draftGating.value = []
|
|
254
|
+
draftFollowUps.value = []
|
|
255
|
+
draftTesterQuality.value = []
|
|
256
|
+
draftStepOptions.value = []
|
|
257
|
+
draftLabels.value = []
|
|
258
|
+
draftPurpose.value = null
|
|
259
|
+
draftName.value = 'New pipeline'
|
|
260
|
+
draftDescription.value = ''
|
|
261
|
+
editingId.value = null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Load an existing (custom) pipeline into the draft so it can be edited in place. */
|
|
265
|
+
function loadForEdit(pipeline: Pipeline) {
|
|
266
|
+
draft.value = [...pipeline.agentKinds]
|
|
267
|
+
draftGates.value = pipeline.agentKinds.map((_, i) => pipeline.gates?.[i] ?? false)
|
|
268
|
+
draftEnabled.value = pipeline.agentKinds.map((_, i) => pipeline.enabled?.[i] ?? true)
|
|
269
|
+
draftThresholds.value = pipeline.agentKinds.map((_, i) => pipeline.thresholds?.[i] ?? null)
|
|
270
|
+
draftConsensus.value = pipeline.agentKinds.map((_, i) => pipeline.consensus?.[i] ?? null)
|
|
271
|
+
draftGating.value = pipeline.agentKinds.map((_, i) => pipeline.gating?.[i] ?? null)
|
|
272
|
+
draftFollowUps.value = pipeline.agentKinds.map((_, i) => pipeline.followUps?.[i] ?? null)
|
|
273
|
+
draftTesterQuality.value = pipeline.agentKinds.map(
|
|
274
|
+
(_, i) => pipeline.testerQuality?.[i] ?? null,
|
|
275
|
+
)
|
|
276
|
+
draftStepOptions.value = pipeline.agentKinds.map((_, i) => pipeline.stepOptions?.[i] ?? null)
|
|
277
|
+
draftLabels.value = [...(pipeline.labels ?? [])]
|
|
278
|
+
draftPurpose.value = pipeline.purpose ?? null
|
|
279
|
+
draftName.value = pipeline.name
|
|
280
|
+
draftDescription.value = pipeline.description ?? ''
|
|
281
|
+
editingId.value = pipeline.id
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
addToDraft,
|
|
286
|
+
removeFromDraft,
|
|
287
|
+
moveInDraft,
|
|
288
|
+
hasCompanion,
|
|
289
|
+
toggleCompanion,
|
|
290
|
+
toggleDraftGating,
|
|
291
|
+
units,
|
|
292
|
+
moveUnit,
|
|
293
|
+
toggleDraftConsensus,
|
|
294
|
+
setDraftConsensus,
|
|
295
|
+
toggleDraftGate,
|
|
296
|
+
toggleDraftFollowUps,
|
|
297
|
+
toggleDraftTesterQuality,
|
|
298
|
+
toggleDraftTesterQualityGating,
|
|
299
|
+
toggleDraftEnabled,
|
|
300
|
+
draftAutoRecommendEnabled,
|
|
301
|
+
toggleDraftAutoRecommend,
|
|
302
|
+
draftSkillId,
|
|
303
|
+
setDraftSkillId,
|
|
304
|
+
clearDraft,
|
|
305
|
+
loadForEdit,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Pipeline } from '~/types/domain'
|
|
2
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
3
|
+
import type { PipelinesContext } from './context'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The pipeline persistence operations — building the draft payload and the create/update/clone/
|
|
7
|
+
* remove/reseed/organize API calls. Extracted from the store setup into a factory closing over the
|
|
8
|
+
* shared {@link PipelinesContext} plus the draft-lifecycle helpers it drives (`clearDraft` /
|
|
9
|
+
* `loadForEdit`, owned by {@link createPipelineDraftActions}), so behaviour is byte-identical to the
|
|
10
|
+
* former in-closure functions — the split is purely to keep the setup within the size budget.
|
|
11
|
+
*/
|
|
12
|
+
export function createPipelinePersistence(
|
|
13
|
+
ctx: PipelinesContext,
|
|
14
|
+
draft: { clearDraft: () => void; loadForEdit: (pipeline: Pipeline) => void },
|
|
15
|
+
) {
|
|
16
|
+
const { api, upsertPipeline, dropPipeline, editingId } = ctx
|
|
17
|
+
const { clearDraft, loadForEdit } = draft
|
|
18
|
+
|
|
19
|
+
/** The optional arrays to send, omitting the ones that are at their defaults. */
|
|
20
|
+
function draftPayload() {
|
|
21
|
+
return {
|
|
22
|
+
name: ctx.draftName.value.trim() || 'Untitled pipeline',
|
|
23
|
+
// ALWAYS send description (like stepOptions) so an update can CLEAR it — an omitted field
|
|
24
|
+
// reads as "keep existing", so toggling the last of the text away must send the empty string.
|
|
25
|
+
// The backend trims + drops a blank one, so this is a no-op on create / an empty description.
|
|
26
|
+
description: ctx.draftDescription.value.trim(),
|
|
27
|
+
agentKinds: [...ctx.draft.value],
|
|
28
|
+
// Only send gates when at least one step is gated.
|
|
29
|
+
...(ctx.draftGates.value.some(Boolean) ? { gates: [...ctx.draftGates.value] } : {}),
|
|
30
|
+
// Only send enabled when at least one step is disabled (default is all-on).
|
|
31
|
+
...(ctx.draftEnabled.value.some((e) => e === false)
|
|
32
|
+
? { enabled: [...ctx.draftEnabled.value] }
|
|
33
|
+
: {}),
|
|
34
|
+
// Only send thresholds when at least one step pins an explicit value.
|
|
35
|
+
...(ctx.draftThresholds.value.some((t) => t != null)
|
|
36
|
+
? { thresholds: [...ctx.draftThresholds.value] }
|
|
37
|
+
: {}),
|
|
38
|
+
// Only send consensus when at least one step is consensus-enabled.
|
|
39
|
+
...(ctx.draftConsensus.value.some((c) => c?.enabled)
|
|
40
|
+
? { consensus: [...ctx.draftConsensus.value] }
|
|
41
|
+
: {}),
|
|
42
|
+
// Only send gating when at least one step has gating enabled.
|
|
43
|
+
...(ctx.draftGating.value.some((g) => g?.enabled)
|
|
44
|
+
? { gating: [...ctx.draftGating.value] }
|
|
45
|
+
: {}),
|
|
46
|
+
// Only send followUps when at least one step disables it (default is on, so only the
|
|
47
|
+
// explicit `false` opt-outs are worth persisting).
|
|
48
|
+
...(ctx.draftFollowUps.value.some((f) => f === false)
|
|
49
|
+
? { followUps: [...ctx.draftFollowUps.value] }
|
|
50
|
+
: {}),
|
|
51
|
+
// Only send testerQuality when at least one Tester step deviates from the default
|
|
52
|
+
// (companion disabled, or an estimate gate configured) — the default (null/enabled,
|
|
53
|
+
// ungated) is not worth persisting.
|
|
54
|
+
...(ctx.draftTesterQuality.value.some((q) => q?.enabled === false || q?.gating?.enabled)
|
|
55
|
+
? { testerQuality: [...ctx.draftTesterQuality.value] }
|
|
56
|
+
: {}),
|
|
57
|
+
// ALWAYS send stepOptions, unlike the legacy per-step arrays above. Those omit-when-default,
|
|
58
|
+
// which means an update can never CLEAR them (an omitted field reads as "keep existing"), so
|
|
59
|
+
// toggling the last opt-out back to its default on a saved pipeline would silently not
|
|
60
|
+
// persist. Sending the aligned array always lets `update` overwrite; the backend normalizes
|
|
61
|
+
// an all-default array away (stores nothing), so this is a no-op on create / all-default.
|
|
62
|
+
stepOptions: ctx.draftStepOptions.value.map((o) => o ?? null),
|
|
63
|
+
// Only send labels when there are any.
|
|
64
|
+
...(ctx.draftLabels.value.length ? { labels: [...ctx.draftLabels.value] } : {}),
|
|
65
|
+
// Only send purpose when the pipeline is classified (null ⇒ leave unclassified). Like the
|
|
66
|
+
// legacy per-step arrays, an omitted `purpose` on update reads as "keep existing"; clearing
|
|
67
|
+
// a classification back to unclassified is not a supported edit (every built-in ships one).
|
|
68
|
+
...(ctx.draftPurpose.value ? { purpose: ctx.draftPurpose.value } : {}),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Persist the draft: update the pipeline being edited, else create a new one. */
|
|
73
|
+
async function saveDraft(): Promise<Pipeline | null> {
|
|
74
|
+
if (ctx.draft.value.length === 0) return null
|
|
75
|
+
const wsId = useWorkspaceStore().requireId()
|
|
76
|
+
const payload = draftPayload()
|
|
77
|
+
if (editingId.value) {
|
|
78
|
+
const updated = await api.updatePipeline(wsId, editingId.value, payload)
|
|
79
|
+
upsertPipeline(updated)
|
|
80
|
+
clearDraft()
|
|
81
|
+
return updated
|
|
82
|
+
}
|
|
83
|
+
const pipeline = await api.createPipeline(wsId, payload)
|
|
84
|
+
upsertPipeline(pipeline)
|
|
85
|
+
clearDraft()
|
|
86
|
+
return pipeline
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Clone any pipeline (built-in or custom) into an editable copy, ready to edit. */
|
|
90
|
+
async function clonePipeline(id: string): Promise<Pipeline> {
|
|
91
|
+
const clone = await api.clonePipeline(useWorkspaceStore().requireId(), id)
|
|
92
|
+
upsertPipeline(clone)
|
|
93
|
+
loadForEdit(clone)
|
|
94
|
+
return clone
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function removePipeline(id: string) {
|
|
98
|
+
await api.removePipeline(useWorkspaceStore().requireId(), id)
|
|
99
|
+
dropPipeline(id)
|
|
100
|
+
if (editingId.value === id) clearDraft()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reseed a built-in pipeline from the backend's current catalog definition: restores its
|
|
105
|
+
* canonical structure + version (adopting an update, or repairing a drifted/invalid copy)
|
|
106
|
+
* while preserving its labels/archive state. Replaces the pipeline in the cache.
|
|
107
|
+
*/
|
|
108
|
+
async function reseed(id: string): Promise<Pipeline> {
|
|
109
|
+
const updated = await api.reseedPipeline(useWorkspaceStore().requireId(), id)
|
|
110
|
+
upsertPipeline(updated)
|
|
111
|
+
if (editingId.value === id) clearDraft()
|
|
112
|
+
return updated
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Set a pipeline's organizational metadata (labels / archive). Works on built-ins too. */
|
|
116
|
+
async function organize(id: string, body: { labels?: string[]; archived?: boolean }) {
|
|
117
|
+
const updated = await api.organizePipeline(useWorkspaceStore().requireId(), id, body)
|
|
118
|
+
upsertPipeline(updated)
|
|
119
|
+
return updated
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const archive = (id: string) => organize(id, { archived: true })
|
|
123
|
+
const unarchive = (id: string) => organize(id, { archived: false })
|
|
124
|
+
const setLabels = (id: string, labels: string[]) => organize(id, { labels })
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
saveDraft,
|
|
128
|
+
clonePipeline,
|
|
129
|
+
removePipeline,
|
|
130
|
+
reseed,
|
|
131
|
+
organize,
|
|
132
|
+
archive,
|
|
133
|
+
unarchive,
|
|
134
|
+
setLabels,
|
|
135
|
+
}
|
|
136
|
+
}
|