@cat-factory/app 0.152.0 → 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/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/package.json +1 -1
|
@@ -6,14 +6,11 @@ import {
|
|
|
6
6
|
type MergedRecipeDraft,
|
|
7
7
|
type PreflightResult,
|
|
8
8
|
type ProvisioningRecommendation,
|
|
9
|
-
type ProvisioningSeedDumpCandidate,
|
|
10
9
|
type StackRecipe,
|
|
11
10
|
analystRecipeDraftSchema,
|
|
12
11
|
mergeAnalystRecipeDraft,
|
|
13
|
-
stackRecipeSchema,
|
|
14
12
|
} from '@cat-factory/contracts'
|
|
15
13
|
import type { Block } from '~/types/domain'
|
|
16
|
-
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
17
14
|
import { useBoardStore } from '~/stores/board'
|
|
18
15
|
import { useExecutionStore } from '~/stores/execution'
|
|
19
16
|
import { useGitHubStore } from '~/stores/github'
|
|
@@ -21,7 +18,10 @@ import { useInfraConfigStore } from '~/stores/infraConfig'
|
|
|
21
18
|
import { usePipelinesStore } from '~/stores/pipelines'
|
|
22
19
|
import { usePreflightsStore } from '~/stores/preflights'
|
|
23
20
|
import { useServicesStore } from '~/stores/services'
|
|
24
|
-
import {
|
|
21
|
+
import type { WizardContext } from '~/stores/environmentWizard/context'
|
|
22
|
+
import { createFlowActions } from '~/stores/environmentWizard/flow'
|
|
23
|
+
import { createRecipeActions } from '~/stores/environmentWizard/recipe'
|
|
24
|
+
import { createSaveActions } from '~/stores/environmentWizard/save'
|
|
25
25
|
|
|
26
26
|
// The environment setup wizard's cross-step DATA + actions (shared-stacks slice 7). It backs the
|
|
27
27
|
// guided flow — pick a service frame → review the recommended `docker-compose` recipe (detector
|
|
@@ -38,6 +38,10 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
38
38
|
// here — this store no longer holds a `step` / `STEP_ORDER` / `goToStep`. It is purely the per-frame
|
|
39
39
|
// data+action layer the journey's step components drive; `beginForFrame` seeds it when a step first
|
|
40
40
|
// targets a frame.
|
|
41
|
+
//
|
|
42
|
+
// The cross-step actions live in cohesive factories under `stores/environmentWizard/` (flow /
|
|
43
|
+
// recipe / save) that close over the shared reactive {@link WizardContext} assembled here — a
|
|
44
|
+
// size-only extraction following the `board` store idiom, behaviour is unchanged.
|
|
41
45
|
|
|
42
46
|
/** The seeded analyst-only pipeline the "run deep analysis" trigger starts against the frame. */
|
|
43
47
|
const ANALYSIS_PIPELINE_ID = 'pl_environment_analysis'
|
|
@@ -47,22 +51,6 @@ const ANALYST_AGENT_KIND = 'environment-analyst'
|
|
|
47
51
|
/** The analyst run's lifecycle as the wizard surfaces it. */
|
|
48
52
|
export type AnalysisStatus = 'idle' | 'running' | 'ready' | 'failed'
|
|
49
53
|
|
|
50
|
-
/** Drop empty arrays / undefined so the persisted recipe stays minimal and schema-valid
|
|
51
|
-
* (`composeFiles` etc. are `minLength(1)`, so an empty array would 422). */
|
|
52
|
-
function pruneRecipe(recipe: StackRecipe): StackRecipe {
|
|
53
|
-
const out: Record<string, unknown> = {}
|
|
54
|
-
for (const [key, value] of Object.entries(recipe)) {
|
|
55
|
-
if (value === undefined || value === null) continue
|
|
56
|
-
if (Array.isArray(value) && value.length === 0) continue
|
|
57
|
-
out[key] = value
|
|
58
|
-
}
|
|
59
|
-
return out as StackRecipe
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function cloneRecipe(recipe: StackRecipe): StackRecipe {
|
|
63
|
-
return JSON.parse(JSON.stringify(recipe)) as StackRecipe
|
|
64
|
-
}
|
|
65
|
-
|
|
66
54
|
export const useEnvironmentWizardStore = defineStore('environmentWizard', () => {
|
|
67
55
|
const board = useBoardStore()
|
|
68
56
|
const github = useGitHubStore()
|
|
@@ -189,254 +177,42 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
|
|
|
189
177
|
)
|
|
190
178
|
|
|
191
179
|
// ---- Actions ------------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* recipe edits. Selecting a different frame resets the flow for it.
|
|
225
|
-
*/
|
|
226
|
-
function beginForFrame(id: string | null) {
|
|
227
|
-
if (frameId.value === id) return
|
|
228
|
-
frameId.value = id
|
|
229
|
-
resetFlowState()
|
|
230
|
-
if (id) void detect()
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Re-seed the working recipe from the current merge (detector-only, or +analyst after apply). */
|
|
234
|
-
function seedFromMerged() {
|
|
235
|
-
if (merged.value) recipe.value = cloneRecipe(merged.value.recipe)
|
|
236
|
-
// Default the exposed service to the detector's recommended compose service, when known.
|
|
237
|
-
const recommended = recommendation.value?.composeServiceCandidates?.find((c) => c.recommended)
|
|
238
|
-
if (recommended && !composeService.value) composeService.value = recommended.service
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Run checkout-free detection for the frame's repo (non-binding; seeds the working recipe). */
|
|
242
|
-
async function detect() {
|
|
243
|
-
const ctx = repoContext.value
|
|
244
|
-
if (!ctx) {
|
|
245
|
-
detectError.value = true
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
const repo = github.repoFor(ctx.githubId)
|
|
249
|
-
if (!repo) {
|
|
250
|
-
detectError.value = true
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
detecting.value = true
|
|
254
|
-
detectError.value = false
|
|
255
|
-
try {
|
|
256
|
-
const rec = await infra.detectProvisioning({
|
|
257
|
-
owner: repo.owner,
|
|
258
|
-
repo: repo.name,
|
|
259
|
-
...(ctx.directory ? { directory: ctx.directory } : {}),
|
|
260
|
-
prefer: 'docker-compose',
|
|
261
|
-
})
|
|
262
|
-
recommendation.value = rec
|
|
263
|
-
// Seed the exposed port + build flag from the detected provisioning where present.
|
|
264
|
-
seedFromMerged()
|
|
265
|
-
} catch {
|
|
266
|
-
detectError.value = true
|
|
267
|
-
} finally {
|
|
268
|
-
detecting.value = false
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Fire the analyst-only pipeline against the frame (mirrors how bootstrap runs pl_blueprint). */
|
|
273
|
-
async function startAnalysis() {
|
|
274
|
-
const id = frameId.value
|
|
275
|
-
const pipeline = analysisPipeline.value
|
|
276
|
-
if (!id || !pipeline) {
|
|
277
|
-
analysisError.value = true
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
analysisError.value = false
|
|
281
|
-
try {
|
|
282
|
-
await execution.start(id, pipeline)
|
|
283
|
-
analysisRequested.value = true
|
|
284
|
-
} catch {
|
|
285
|
-
analysisError.value = true
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Fold the (now-ready) analyst draft into the working recipe (re-seed from the merge). */
|
|
290
|
-
function applyAnalystDraft() {
|
|
291
|
-
seedFromMerged()
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Toggle an OS-override / extra compose file into the working recipe's ordered `composeFiles`. */
|
|
295
|
-
function toggleComposeFile(path: string) {
|
|
296
|
-
const files = recipe.value.composeFiles ? [...recipe.value.composeFiles] : []
|
|
297
|
-
const idx = files.indexOf(path)
|
|
298
|
-
if (idx >= 0) files.splice(idx, 1)
|
|
299
|
-
else files.push(path)
|
|
300
|
-
recipe.value = { ...recipe.value, composeFiles: files }
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Toggle a `COMPOSE_PROFILES` label into the working recipe. */
|
|
304
|
-
function toggleProfile(profile: string) {
|
|
305
|
-
const profiles = recipe.value.composeProfiles ? [...recipe.value.composeProfiles] : []
|
|
306
|
-
const idx = profiles.indexOf(profile)
|
|
307
|
-
if (idx >= 0) profiles.splice(idx, 1)
|
|
308
|
-
else profiles.push(profile)
|
|
309
|
-
recipe.value = { ...recipe.value, composeProfiles: profiles }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Convert a confirmed seed-dump candidate into a `compose-exec` step that pipes the dump via
|
|
314
|
-
* stdin. The service + command are a best-effort default (the exposed/db service + a `cat`
|
|
315
|
-
* placeholder) the operator refines in the recipe editor — detection can't know the DB client.
|
|
316
|
-
*/
|
|
317
|
-
function addSeedStep(candidate: ProvisioningSeedDumpCandidate) {
|
|
318
|
-
const setupSteps = recipe.value.setupSteps ? [...recipe.value.setupSteps] : []
|
|
319
|
-
setupSteps.push({
|
|
320
|
-
kind: 'compose-exec',
|
|
321
|
-
name: `Import seed ${candidate.name}`,
|
|
322
|
-
service: composeService.value || 'db',
|
|
323
|
-
command: ['sh', '-c', 'cat'],
|
|
324
|
-
stdinFile: candidate.path,
|
|
325
|
-
})
|
|
326
|
-
recipe.value = { ...recipe.value, setupSteps }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** Replace the working recipe from a raw-JSON edit; returns an error message or null on success. */
|
|
330
|
-
function setRecipeFromJson(text: string): string | null {
|
|
331
|
-
let parsedJson: unknown
|
|
332
|
-
try {
|
|
333
|
-
parsedJson = JSON.parse(text)
|
|
334
|
-
} catch (err) {
|
|
335
|
-
return err instanceof Error ? err.message : 'Invalid JSON'
|
|
336
|
-
}
|
|
337
|
-
const result = v.safeParse(stackRecipeSchema, parsedJson)
|
|
338
|
-
if (!result.success) return result.issues.map((i) => i.message).join('; ')
|
|
339
|
-
recipe.value = result.output
|
|
340
|
-
return null
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Run the working recipe's declared preflight checks (host-bound; degrades on a non-local facade). */
|
|
344
|
-
async function runPreflight() {
|
|
345
|
-
preflightRunning.value = true
|
|
346
|
-
preflightError.value = null
|
|
347
|
-
try {
|
|
348
|
-
preflightResults.value = await preflights.run(recipe.value.prerequisites ?? [])
|
|
349
|
-
} catch (err) {
|
|
350
|
-
// A 503 is handled inside `preflights.run` (degraded note); anything else is a real failure
|
|
351
|
-
// that must be shown rather than swallowed into an unhandled rejection.
|
|
352
|
-
preflightError.value =
|
|
353
|
-
apiErrorEnvelope(err)?.message ?? (err instanceof Error ? err.message : String(err))
|
|
354
|
-
} finally {
|
|
355
|
-
preflightRunning.value = false
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Persist the confirmed config: register the workspace's `docker-compose` handler (so the Deployer
|
|
361
|
-
* can provision it) AND write the recipe onto the service frame's provisioning. The handler carries
|
|
362
|
-
* only the daemon "how" (the exposed service + port); the recipe is the per-service "what/where".
|
|
363
|
-
*/
|
|
364
|
-
async function save() {
|
|
365
|
-
const id = frameId.value
|
|
366
|
-
const service = composeService.value.trim()
|
|
367
|
-
if (!id || !service) {
|
|
368
|
-
saveError.value = 'A frame and an exposed compose service are required.'
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
// `exposedPort` is a `v-model.number` field, which yields '' (not a number) when cleared. Guard
|
|
372
|
-
// here so an empty/out-of-range port can't reach the handler manifest.
|
|
373
|
-
const port = Number(exposedPort.value)
|
|
374
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
375
|
-
saveError.value = 'Enter a valid exposed port (1-65535).'
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
saving.value = true
|
|
379
|
-
saveError.value = null
|
|
380
|
-
const pruned = pruneRecipe(recipe.value)
|
|
381
|
-
const build = recommendation.value?.provisioning.composeBuild === true
|
|
382
|
-
const allowHostCommands = (pruned.setupSteps ?? []).some((s) => s.kind === 'host-command')
|
|
383
|
-
try {
|
|
384
|
-
await infra.registerHandler({
|
|
385
|
-
provisionType: 'docker-compose',
|
|
386
|
-
config: {
|
|
387
|
-
engine: 'local-docker',
|
|
388
|
-
manifest: {
|
|
389
|
-
providerId: 'compose',
|
|
390
|
-
label: handlerLabel.value.trim() || 'Docker Compose',
|
|
391
|
-
baseUrl: 'http://localhost',
|
|
392
|
-
auth: { type: 'none' },
|
|
393
|
-
provision: { method: 'POST', pathTemplate: '' },
|
|
394
|
-
response: {},
|
|
395
|
-
providerConfig: {
|
|
396
|
-
service,
|
|
397
|
-
port,
|
|
398
|
-
...(build ? { build: true } : {}),
|
|
399
|
-
...(allowHostCommands ? { allowHostCommands: true } : {}),
|
|
400
|
-
},
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
secrets: {},
|
|
404
|
-
})
|
|
405
|
-
await board.updateBlock(id, {
|
|
406
|
-
provisioning: {
|
|
407
|
-
type: 'docker-compose',
|
|
408
|
-
...(pruned.composeFiles?.[0] ? { composePath: pruned.composeFiles[0] } : {}),
|
|
409
|
-
...(build ? { composeBuild: true } : {}),
|
|
410
|
-
recipe: pruned,
|
|
411
|
-
},
|
|
412
|
-
})
|
|
413
|
-
saved.value = true
|
|
414
|
-
} catch (err) {
|
|
415
|
-
saveError.value =
|
|
416
|
-
apiErrorEnvelope(err)?.message ?? (err instanceof Error ? err.message : String(err))
|
|
417
|
-
} finally {
|
|
418
|
-
saving.value = false
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** Optional trial: provision the just-saved config for the frame (local-only; live logs shown). */
|
|
423
|
-
async function trialProvision() {
|
|
424
|
-
const id = frameId.value
|
|
425
|
-
if (!id || !saved.value) return
|
|
426
|
-
trialing.value = true
|
|
427
|
-
trialError.value = null
|
|
428
|
-
try {
|
|
429
|
-
const api = useApi()
|
|
430
|
-
const ws = useWorkspaceStore()
|
|
431
|
-
await api.provisionEnvironment(ws.requireId(), { blockId: id })
|
|
432
|
-
trialStarted.value = true
|
|
433
|
-
} catch (err) {
|
|
434
|
-
trialError.value =
|
|
435
|
-
apiErrorEnvelope(err)?.message ?? (err instanceof Error ? err.message : String(err))
|
|
436
|
-
} finally {
|
|
437
|
-
trialing.value = false
|
|
438
|
-
}
|
|
180
|
+
// The cross-step actions are split into cohesive factories sharing the reactive context above (a
|
|
181
|
+
// size-only extraction — behaviour is identical to the former in-closure functions). The internal
|
|
182
|
+
// `resetFlowState` / `seedFromMerged` helpers stay private to the flow factory (they were never
|
|
183
|
+
// part of the public store shape).
|
|
184
|
+
const context: WizardContext = {
|
|
185
|
+
board,
|
|
186
|
+
github,
|
|
187
|
+
infra,
|
|
188
|
+
execution,
|
|
189
|
+
preflights,
|
|
190
|
+
frameId,
|
|
191
|
+
detecting,
|
|
192
|
+
detectError,
|
|
193
|
+
recommendation,
|
|
194
|
+
analysisRequested,
|
|
195
|
+
analysisError,
|
|
196
|
+
recipe,
|
|
197
|
+
composeService,
|
|
198
|
+
preflightRunning,
|
|
199
|
+
preflightResults,
|
|
200
|
+
preflightError,
|
|
201
|
+
handlerLabel,
|
|
202
|
+
exposedPort,
|
|
203
|
+
saving,
|
|
204
|
+
saveError,
|
|
205
|
+
saved,
|
|
206
|
+
trialing,
|
|
207
|
+
trialError,
|
|
208
|
+
trialStarted,
|
|
209
|
+
repoContext,
|
|
210
|
+
analysisPipeline,
|
|
211
|
+
merged,
|
|
439
212
|
}
|
|
213
|
+
const flow = createFlowActions(context)
|
|
214
|
+
const recipeActions = createRecipeActions(context)
|
|
215
|
+
const saveActions = createSaveActions(context)
|
|
440
216
|
|
|
441
217
|
return {
|
|
442
218
|
// state
|
|
@@ -470,16 +246,8 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
|
|
|
470
246
|
analysisStatus,
|
|
471
247
|
merged,
|
|
472
248
|
// actions
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
applyAnalystDraft,
|
|
477
|
-
toggleComposeFile,
|
|
478
|
-
toggleProfile,
|
|
479
|
-
addSeedStep,
|
|
480
|
-
setRecipeFromJson,
|
|
481
|
-
runPreflight,
|
|
482
|
-
save,
|
|
483
|
-
trialProvision,
|
|
249
|
+
...flow,
|
|
250
|
+
...recipeActions,
|
|
251
|
+
...saveActions,
|
|
484
252
|
}
|
|
485
253
|
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { AgentKind, Pipeline } from '~/types/domain'
|
|
4
|
+
import type { ConsensusStepConfig, StepGating } from '~/types/consensus'
|
|
5
|
+
import type { PipelinePurpose, StepOptions, TesterQualityConfig } from '@cat-factory/contracts'
|
|
6
|
+
import { uid } from '~/utils/catalog'
|
|
7
|
+
|
|
8
|
+
/** A sensible default config when a step is first flipped to consensus in the builder. */
|
|
9
|
+
export function defaultConsensusConfig(): ConsensusStepConfig {
|
|
10
|
+
return {
|
|
11
|
+
enabled: true,
|
|
12
|
+
strategy: 'specialist-panel',
|
|
13
|
+
participants: [
|
|
14
|
+
{ id: uid('cp'), role: 'Pragmatist', systemFraming: 'Favour the simplest viable approach.' },
|
|
15
|
+
{
|
|
16
|
+
id: uid('cp'),
|
|
17
|
+
role: 'Skeptic',
|
|
18
|
+
systemFraming: 'Probe risks, edge cases and failure modes.',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The per-step, index-aligned draft arrays. Every one is kept the same length as `draft` and
|
|
26
|
+
* spliced/reordered in lockstep (see `insertAt` / `removeFromDraft` / `moveUnit`), so grouping
|
|
27
|
+
* their construction keeps that alignment contract — and its documentation — in one place.
|
|
28
|
+
*/
|
|
29
|
+
export function createDraftStepState() {
|
|
30
|
+
/** The chain currently being assembled in the builder. */
|
|
31
|
+
const draft = ref<AgentKind[]>([])
|
|
32
|
+
/** Per-step approval gates, kept index-aligned with `draft`. */
|
|
33
|
+
const draftGates = ref<boolean[]>([])
|
|
34
|
+
/** Per-step enable flags, kept index-aligned with `draft` (false ⇒ skipped at run). */
|
|
35
|
+
const draftEnabled = ref<boolean[]>([])
|
|
36
|
+
/**
|
|
37
|
+
* Per-step companion thresholds, kept index-aligned with `draft`. Not editable in the
|
|
38
|
+
* builder UI today, but carried through edits so an existing pipeline's thresholds
|
|
39
|
+
* survive a save.
|
|
40
|
+
*/
|
|
41
|
+
const draftThresholds = ref<(number | null)[]>([])
|
|
42
|
+
/** Per-step consensus configs, kept index-aligned with `draft` (null ⇒ standard agent). */
|
|
43
|
+
const draftConsensus = ref<(ConsensusStepConfig | null)[]>([])
|
|
44
|
+
/** Per-step estimate gating, kept index-aligned with `draft` (null ⇒ always run). */
|
|
45
|
+
const draftGating = ref<(StepGating | null)[]>([])
|
|
46
|
+
/**
|
|
47
|
+
* Per-step Follow-up companion toggle, kept index-aligned with `draft`. Only meaningful on
|
|
48
|
+
* a `coder` step; `false` disables the companion there (default/true ⇒ enabled).
|
|
49
|
+
*/
|
|
50
|
+
const draftFollowUps = ref<(boolean | null)[]>([])
|
|
51
|
+
/**
|
|
52
|
+
* Per-step test quality-control companion config, kept index-aligned with `draft`. Only
|
|
53
|
+
* meaningful on a Tester step (`tester-api`/`tester-ui`); `null`/absent means "enabled, no
|
|
54
|
+
* gating" (the QC companion is on by default), `{ enabled: false }` disables it, and an
|
|
55
|
+
* entry with `gating` makes it conditional on the task estimate.
|
|
56
|
+
*/
|
|
57
|
+
const draftTesterQuality = ref<(TesterQualityConfig | null)[]>([])
|
|
58
|
+
/**
|
|
59
|
+
* Per-step options bag, kept index-aligned with `draft`: the extensible home for new per-step
|
|
60
|
+
* parameters (see `StepOptions`). `null`/absent per step ⇒ that step's defaults. Today the only
|
|
61
|
+
* field is `autoRecommend` (requirements-review); by convention we store ONLY deviations from a
|
|
62
|
+
* default, so an entry exists only when a step opts out of something.
|
|
63
|
+
*/
|
|
64
|
+
const draftStepOptions = ref<(StepOptions | null)[]>([])
|
|
65
|
+
return {
|
|
66
|
+
draft,
|
|
67
|
+
draftGates,
|
|
68
|
+
draftEnabled,
|
|
69
|
+
draftThresholds,
|
|
70
|
+
draftConsensus,
|
|
71
|
+
draftGating,
|
|
72
|
+
draftFollowUps,
|
|
73
|
+
draftTesterQuality,
|
|
74
|
+
draftStepOptions,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Shared reactive state + injected dependencies the pipelines-store draft factories close over.
|
|
80
|
+
* Created once in the `pipelines` store setup and threaded into {@link createPipelineDraftActions}
|
|
81
|
+
* and {@link createPipelinePersistence} so the split operations stay behaviourally identical to the
|
|
82
|
+
* original single-closure store — a size-only extraction mirroring `stores/board/`, not a new seam.
|
|
83
|
+
*/
|
|
84
|
+
export interface PipelinesContext {
|
|
85
|
+
api: ReturnType<typeof useApi>
|
|
86
|
+
pipelines: Ref<Pipeline[]>
|
|
87
|
+
upsertPipeline: (pipeline: Pipeline) => void
|
|
88
|
+
dropPipeline: (id: string) => void
|
|
89
|
+
draft: Ref<AgentKind[]>
|
|
90
|
+
draftGates: Ref<boolean[]>
|
|
91
|
+
draftEnabled: Ref<boolean[]>
|
|
92
|
+
draftThresholds: Ref<(number | null)[]>
|
|
93
|
+
draftConsensus: Ref<(ConsensusStepConfig | null)[]>
|
|
94
|
+
draftGating: Ref<(StepGating | null)[]>
|
|
95
|
+
draftFollowUps: Ref<(boolean | null)[]>
|
|
96
|
+
draftTesterQuality: Ref<(TesterQualityConfig | null)[]>
|
|
97
|
+
draftStepOptions: Ref<(StepOptions | null)[]>
|
|
98
|
+
draftLabels: Ref<string[]>
|
|
99
|
+
draftPurpose: Ref<PipelinePurpose | null>
|
|
100
|
+
draftName: Ref<string>
|
|
101
|
+
draftDescription: Ref<string>
|
|
102
|
+
editingId: Ref<string | null>
|
|
103
|
+
}
|