@cat-factory/app 0.257.0 → 0.258.0
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/binaryOutput/BinaryOutputReport.vue +42 -0
- package/app/components/board/AddTaskModal.vue +38 -12
- package/app/components/board/RecurringPipelineModal.vue +24 -12
- package/app/components/board/nodes/TaskCard.vue +34 -7
- package/app/components/panels/InspectorPanel.vue +39 -0
- package/app/components/pipeline/BinaryOutputStepPicker.logic.spec.ts +29 -0
- package/app/components/pipeline/BinaryOutputStepPicker.logic.ts +23 -0
- package/app/components/pipeline/BinaryOutputStepPicker.vue +109 -0
- package/app/components/pipeline/PipelineBuilder.vue +44 -0
- package/app/components/pipeline/PipelinePreview.vue +20 -1
- package/app/components/pipeline/PipelineProgress.vue +13 -0
- package/app/composables/api/execution.ts +19 -0
- package/app/composables/usePipelineHealth.spec.ts +42 -5
- package/app/composables/usePipelineHealth.ts +109 -48
- package/app/modular/agent-kinds.ts +5 -0
- package/app/stores/environmentWizard/context.ts +0 -2
- package/app/stores/environmentWizard/flow.ts +11 -6
- package/app/stores/environmentWizard.ts +12 -11
- package/app/stores/execution/commands.ts +26 -1
- package/app/stores/pipelines/draftActions.ts +2 -0
- package/app/stores/pipelines/draftStepConfig.ts +4 -161
- package/app/stores/pipelines/draftStepOptions.ts +204 -0
- package/app/types/domain.ts +6 -0
- package/app/utils/agentPalette.spec.ts +26 -0
- package/app/utils/agentPalette.ts +9 -4
- package/app/utils/binaryOutput.spec.ts +122 -1
- package/app/utils/binaryOutput.ts +101 -0
- package/app/utils/catalog.spec.ts +24 -0
- package/app/utils/catalog.ts +21 -4
- package/app/utils/pipeline.spec.ts +35 -3
- package/app/utils/pipeline.ts +54 -3
- package/app/utils/pipelineRender.spec.ts +78 -2
- package/app/utils/pipelineRender.ts +43 -0
- package/i18n/locales/de.json +29 -1
- package/i18n/locales/en.json +29 -1
- package/i18n/locales/es.json +29 -1
- package/i18n/locales/fr.json +29 -1
- package/i18n/locales/he.json +29 -1
- package/i18n/locales/it.json +29 -1
- package/i18n/locales/ja.json +29 -1
- package/i18n/locales/pl.json +29 -1
- package/i18n/locales/tr.json +29 -1
- package/i18n/locales/uk.json +29 -1
- package/package.json +2 -2
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
binaryCapabilityCoverage,
|
|
4
4
|
binaryFormatCoverage,
|
|
5
5
|
binaryModalityOverlaps,
|
|
6
|
+
conflictingOutputSizeOptions,
|
|
7
|
+
isBinaryModality,
|
|
8
|
+
modalityCarriesPixelDimensions,
|
|
6
9
|
normalizeMediaType,
|
|
7
10
|
requiredBinaryCapabilities,
|
|
8
11
|
} from '@cat-factory/contracts'
|
|
@@ -10,6 +13,7 @@ import type {
|
|
|
10
13
|
BinaryGeneratorCapability,
|
|
11
14
|
BinaryModality,
|
|
12
15
|
BinaryModalityOverlap,
|
|
16
|
+
ConflictingOutputSizeOption,
|
|
13
17
|
RegisteredBinaryGenerator,
|
|
14
18
|
} from '@cat-factory/contracts'
|
|
15
19
|
import type {
|
|
@@ -85,6 +89,35 @@ export interface BinaryOutputRow extends BinaryOutputArtifact {
|
|
|
85
89
|
* state (a model with native image output generates without a registered integration).
|
|
86
90
|
*/
|
|
87
91
|
generatorUnknown: boolean
|
|
92
|
+
/**
|
|
93
|
+
* The artifact reports pixel dimensions that are not the exact size the step asked for.
|
|
94
|
+
*
|
|
95
|
+
* FALSE when the step asked for no size, false when the size does not cover this artifact (see
|
|
96
|
+
* {@link sizeRequirementCovers}), and false when the artifact reports no dimensions: the last
|
|
97
|
+
* is "not stated", which is why {@link BinaryOutputView.sizeUnreported} counts it separately
|
|
98
|
+
* instead of letting an absent measurement read as a passing one.
|
|
99
|
+
*/
|
|
100
|
+
missized: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether the step's exact size is a statement about THIS artifact.
|
|
105
|
+
*
|
|
106
|
+
* A size covers what is measured in pixels, which is the contracts rule
|
|
107
|
+
* {@link modalityCarriesPixelDimensions} and not a judgement made here: a step selecting an image
|
|
108
|
+
* generator beside an audio one states one size, means it about the images, and counting the audio
|
|
109
|
+
* against it would warn permanently about a step that delivered exactly what was asked.
|
|
110
|
+
*
|
|
111
|
+
* An artifact whose content type the platform could not classify is COVERED, because absent is not
|
|
112
|
+
* "not an image": the row may well be one, and excluding it would turn an unclassifiable artifact
|
|
113
|
+
* into a silent pass on the one axis this requirement exists to check. A RETIRED modality reads the
|
|
114
|
+
* same way for the same reason, which is why the membership guard runs before the lookup rather
|
|
115
|
+
* than a bare index that would throw on it.
|
|
116
|
+
*/
|
|
117
|
+
function sizeRequirementCovers(artifact: BinaryOutputArtifact): boolean {
|
|
118
|
+
const modality = artifact.modality
|
|
119
|
+
if (modality === undefined || !isBinaryModality(modality)) return true
|
|
120
|
+
return modalityCarriesPixelDimensions(modality)
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
/** The whole surface's read model: one state, the join, and every loss the report counted. */
|
|
@@ -152,6 +185,31 @@ export interface BinaryOutputView {
|
|
|
152
185
|
* report with formats required and none reported says so rather than passing.
|
|
153
186
|
*/
|
|
154
187
|
undeliveredMediaTypes: readonly string[]
|
|
188
|
+
/**
|
|
189
|
+
* The exact pixel size the step asked its pixel-measured generations to be delivered at
|
|
190
|
+
* (`stepOptions.binaryOutput.generation.outputSize`), or null when it asked for none, which
|
|
191
|
+
* stays the ordinary case. Which artifacts it covers is {@link sizeRequirementCovers}.
|
|
192
|
+
*/
|
|
193
|
+
requiredSize: { width: number; height: number } | null
|
|
194
|
+
/**
|
|
195
|
+
* How many of {@link rows} reported dimensions OTHER than {@link requiredSize}.
|
|
196
|
+
*
|
|
197
|
+
* The delivery-side half of the size requirement, and the reason the requirement is worth
|
|
198
|
+
* declaring at all. Admission checks what the selected integrations can be ASKED for; only
|
|
199
|
+
* this checks what came back, exactly as {@link undeliveredMediaTypes} does one axis over and
|
|
200
|
+
* from the same kind of self-report. Derived in code from the step's own two records, never
|
|
201
|
+
* read off the agent's prose.
|
|
202
|
+
*/
|
|
203
|
+
missized: number
|
|
204
|
+
/**
|
|
205
|
+
* How many of the COVERED {@link rows} reported no dimensions on a step that required a size.
|
|
206
|
+
*
|
|
207
|
+
* Its own number rather than folded into {@link missized}, for the rule this whole feature
|
|
208
|
+
* runs on: an unmeasured artifact and a wrong-sized one are the same value and opposite facts.
|
|
209
|
+
* Counting them together would let a run that reported nothing read as a run that delivered
|
|
210
|
+
* everything wrong, and hiding them would let it read as a clean one.
|
|
211
|
+
*/
|
|
212
|
+
sizeUnreported: number
|
|
155
213
|
/**
|
|
156
214
|
* Integration ids the AGENT named that the deployment does not register. The generative twin of
|
|
157
215
|
* {@link unknownDeclaredServices}, and it needs no exclusion to stay disjoint from anything —
|
|
@@ -203,6 +261,7 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
203
261
|
const generators = config?.generatorIds ?? []
|
|
204
262
|
const modalities = config?.modalities ?? []
|
|
205
263
|
const mediaTypes = config?.mediaTypes ?? []
|
|
264
|
+
const requiredSize = config?.generation?.outputSize ?? null
|
|
206
265
|
if (!report) {
|
|
207
266
|
return {
|
|
208
267
|
// A step still queued has not had the chance to record anything, which is a different
|
|
@@ -217,6 +276,9 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
217
276
|
modalities,
|
|
218
277
|
mediaTypes,
|
|
219
278
|
undeliveredMediaTypes: [],
|
|
279
|
+
requiredSize,
|
|
280
|
+
missized: 0,
|
|
281
|
+
sizeUnreported: 0,
|
|
220
282
|
unknownDeclaredGenerators: [],
|
|
221
283
|
generatorsUnverified: false,
|
|
222
284
|
invalidEntries: 0,
|
|
@@ -234,6 +296,14 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
234
296
|
unknown: unknown.has(artifact.service),
|
|
235
297
|
// An UNATTRIBUTED row (no `generator` claimed) is not unknown — see the field's own note.
|
|
236
298
|
generatorUnknown: artifact.generator !== undefined && unknownGenerators.has(artifact.generator),
|
|
299
|
+
// An UNMEASURED row is not missized either: absent dimensions are counted by
|
|
300
|
+
// `sizeUnreported`, never folded in here.
|
|
301
|
+
missized:
|
|
302
|
+
requiredSize !== null &&
|
|
303
|
+
sizeRequirementCovers(artifact) &&
|
|
304
|
+
artifact.dimensions !== undefined &&
|
|
305
|
+
(artifact.dimensions.width !== requiredSize.width ||
|
|
306
|
+
artifact.dimensions.height !== requiredSize.height),
|
|
237
307
|
}))
|
|
238
308
|
|
|
239
309
|
return {
|
|
@@ -247,6 +317,12 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
247
317
|
modalities,
|
|
248
318
|
mediaTypes,
|
|
249
319
|
undeliveredMediaTypes: undeliveredMediaTypes(mediaTypes, rows),
|
|
320
|
+
requiredSize,
|
|
321
|
+
missized: rows.filter((row) => row.missized).length,
|
|
322
|
+
sizeUnreported:
|
|
323
|
+
requiredSize === null
|
|
324
|
+
? 0
|
|
325
|
+
: rows.filter((row) => row.dimensions === undefined && sizeRequirementCovers(row)).length,
|
|
250
326
|
unknownDeclaredGenerators: report.unknownGenerators,
|
|
251
327
|
generatorsUnverified: report.generatorsUnverified === true,
|
|
252
328
|
invalidEntries: report.invalidEntries,
|
|
@@ -362,6 +438,8 @@ export function binaryOutputHasWarnings(view: BinaryOutputView): boolean {
|
|
|
362
438
|
view.unknownDeclaredGenerators.length > 0 ||
|
|
363
439
|
view.generatorsUnverified ||
|
|
364
440
|
view.undeliveredMediaTypes.length > 0 ||
|
|
441
|
+
view.missized > 0 ||
|
|
442
|
+
view.sizeUnreported > 0 ||
|
|
365
443
|
view.invalidEntries > 0 ||
|
|
366
444
|
view.omitted > 0 ||
|
|
367
445
|
view.misdirected > 0
|
|
@@ -454,6 +532,17 @@ export type BinaryOutputPickIssue =
|
|
|
454
532
|
* flag most working selections in the product.
|
|
455
533
|
*/
|
|
456
534
|
| 'capability_unverifiable'
|
|
535
|
+
/**
|
|
536
|
+
* The step states an exact output size AND another option that restates the delivered
|
|
537
|
+
* dimensions (`aspectRatio`, `upscale`). A refusal, mirroring `assertUnambiguousOutputSize` at
|
|
538
|
+
* pipeline save.
|
|
539
|
+
*
|
|
540
|
+
* Unlike every member above it this needs no catalog and no registry: it is a fact about the
|
|
541
|
+
* step's own fields, which is exactly why it belongs here. The builder offers all three controls
|
|
542
|
+
* at once, so without this line the only report of the conflict is a failed round trip carrying
|
|
543
|
+
* backend prose, on a surface where the fix is deleting one of two visible fields.
|
|
544
|
+
*/
|
|
545
|
+
| 'output_size_ambiguous'
|
|
457
546
|
|
|
458
547
|
/** What the builder found wrong with one step's selection, and which ids to name. */
|
|
459
548
|
export interface BinaryOutputPickState {
|
|
@@ -478,6 +567,10 @@ export interface BinaryOutputPickState {
|
|
|
478
567
|
unsupportedCapabilities: readonly BinaryGeneratorCapability[]
|
|
479
568
|
/** The ones that could not be judged, kept apart from the refusal above. */
|
|
480
569
|
unverifiableCapabilities: readonly BinaryGeneratorCapability[]
|
|
570
|
+
/** The options restating the delivered dimensions beside an exact size, for the line that names
|
|
571
|
+
* which field to delete. Computed through contracts' own rule, so this cannot come to a
|
|
572
|
+
* different answer from the save that refuses it. */
|
|
573
|
+
conflictingSizeOptions: readonly ConflictingOutputSizeOption[]
|
|
481
574
|
}
|
|
482
575
|
|
|
483
576
|
/**
|
|
@@ -620,6 +713,12 @@ export function binaryOutputPickIssues(
|
|
|
620
713
|
// generative fault too. Reporting them one round at a time is exactly the fix-and-retry cycle
|
|
621
714
|
// this function returns every issue to avoid.
|
|
622
715
|
const generative = generatorPickIssues(config, generators, generatorsUnavailable)
|
|
716
|
+
// Judged beside the generative half and before the storage early return, because it depends on
|
|
717
|
+
// neither registry: a step holding two statements of its own dimensions is mis-configured whether
|
|
718
|
+
// or not its storage pick resolved, and reporting it only on the second pass would cost the
|
|
719
|
+
// fix-and-retry cycle every other issue here is returned together to avoid.
|
|
720
|
+
const conflictingSizeOptions = conflictingOutputSizeOptions(config?.generation)
|
|
721
|
+
if (conflictingSizeOptions.length) issues.push('output_size_ambiguous')
|
|
623
722
|
const noStorageService =
|
|
624
723
|
resolved && !catalog.some((s) => s.capabilities.includes(ASSET_STORAGE_CAPABILITY))
|
|
625
724
|
if (available === false) issues.push('catalog_unavailable')
|
|
@@ -638,6 +737,7 @@ export function binaryOutputPickIssues(
|
|
|
638
737
|
generatorOverlaps: generative.overlaps,
|
|
639
738
|
unsupportedCapabilities: generative.unsupportedCapabilities,
|
|
640
739
|
unverifiableCapabilities: generative.unverifiableCapabilities,
|
|
740
|
+
conflictingSizeOptions,
|
|
641
741
|
}
|
|
642
742
|
}
|
|
643
743
|
|
|
@@ -664,5 +764,6 @@ export function binaryOutputPickIssues(
|
|
|
664
764
|
generatorOverlaps: generative.overlaps,
|
|
665
765
|
unsupportedCapabilities: generative.unsupportedCapabilities,
|
|
666
766
|
unverifiableCapabilities: generative.unverifiableCapabilities,
|
|
767
|
+
conflictingSizeOptions,
|
|
667
768
|
}
|
|
668
769
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SYSTEM_AGENT_META,
|
|
13
13
|
agentKindMeta,
|
|
14
14
|
blockTypeMeta,
|
|
15
|
+
mayCarrySkipAxis,
|
|
15
16
|
uid,
|
|
16
17
|
} from '~/utils/catalog'
|
|
17
18
|
|
|
@@ -213,3 +214,26 @@ describe('catalog', () => {
|
|
|
213
214
|
expect(uid('blk')).not.toBe(uid('blk'))
|
|
214
215
|
})
|
|
215
216
|
})
|
|
217
|
+
|
|
218
|
+
describe('mayCarrySkipAxis', () => {
|
|
219
|
+
it('refuses the kinds the run structurally needs', () => {
|
|
220
|
+
// The builder offers a run condition off this predicate, and the engine refuses the same set
|
|
221
|
+
// (`assertValidRunConditions`). A condition on `merger` would drop the merge on every run
|
|
222
|
+
// outside its scope while the pipeline still finished reporting success.
|
|
223
|
+
for (const kind of ['merger', 'coder', 'ci', 'conflicts', 'deployer']) {
|
|
224
|
+
expect(mayCarrySkipAxis(kind), kind).toBe(false)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('allows the kinds whose result later steps read as context', () => {
|
|
229
|
+
for (const kind of ['tester-ui', 'tester-api', 'architect', 'reviewer']) {
|
|
230
|
+
expect(mayCarrySkipAxis(kind), kind).toBe(true)
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('allows a DEPLOYMENT-registered kind, whose flag this build cannot see', () => {
|
|
235
|
+
// Over-offering costs a 422 with an explanatory message; under-offering silently removes a
|
|
236
|
+
// capability the deployment declared, with nothing on screen to say why.
|
|
237
|
+
expect(mayCarrySkipAxis('org:auditor')).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
})
|
package/app/utils/catalog.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
TaskTypeMeta,
|
|
9
9
|
} from '~/types/domain'
|
|
10
10
|
import type { BadgeColor } from '~/utils/badge'
|
|
11
|
+
import { isBuiltinGatableKind } from '@cat-factory/contracts'
|
|
11
12
|
|
|
12
13
|
/** Simple unique id helper (fine for a client-only prototype). */
|
|
13
14
|
export function uid(prefix = 'id'): string {
|
|
@@ -153,8 +154,8 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
|
|
|
153
154
|
// Authors the service's in-repo specification from the clarified requirements, so it sits
|
|
154
155
|
// beside the design kinds and ahead of the architect that reads what it wrote. Registered on
|
|
155
156
|
// the backend so it also arrives via the workspace manifest, and modelled statically here for
|
|
156
|
-
// the same reason `pr-reviewer` is: a `pl_bugfix`
|
|
157
|
-
//
|
|
157
|
+
// the same reason `pr-reviewer` is: a `pl_bugfix` timeline must name the step before the
|
|
158
|
+
// manifest hydrates. Mirrors the backend `presentation` in `spec-blueprints.ts`.
|
|
158
159
|
kind: 'spec-writer',
|
|
159
160
|
tier: 'intermediate',
|
|
160
161
|
label: 'Spec Writer',
|
|
@@ -180,8 +181,8 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
|
|
|
180
181
|
},
|
|
181
182
|
{
|
|
182
183
|
// Refreshes the service → modules map the board projects. Statically modelled beside its
|
|
183
|
-
// backend `presentation` for the same reason the Spec Writer is:
|
|
184
|
-
//
|
|
184
|
+
// backend `presentation` for the same reason the Spec Writer is: the single-kind run behind
|
|
185
|
+
// the board's "Map service" action renders its timeline before the manifest hydrates.
|
|
185
186
|
kind: 'blueprints',
|
|
186
187
|
tier: 'intermediate',
|
|
187
188
|
label: 'Blueprinter',
|
|
@@ -864,6 +865,22 @@ export function agentKindMeta(kind: string): AgentArchetype {
|
|
|
864
865
|
)
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Whether the builder may offer a SKIP AXIS (an estimate gate, a run condition) on this kind:
|
|
870
|
+
* false only where this build KNOWS the answer is no.
|
|
871
|
+
*
|
|
872
|
+
* A built-in kind is answered by the shared `BUILTIN_GATABLE_KINDS`. A DEPLOYMENT-registered kind
|
|
873
|
+
* carries its own `gatable` flag in the agent-kind registry, which the SPA cannot see, so it is
|
|
874
|
+
* offered rather than withheld — the same direction the pipeline-health advisory takes the
|
|
875
|
+
* asymmetry, and for the sharper reason: over-offering costs a 422 with an explanatory message at
|
|
876
|
+
* save, while under-offering silently removes a capability the deployment declared, with no route
|
|
877
|
+
* to it and nothing on screen to say why.
|
|
878
|
+
*/
|
|
879
|
+
export function mayCarrySkipAxis(kind: string): boolean {
|
|
880
|
+
const isBuiltin = kind in AGENT_BY_KIND || kind in SYSTEM_AGENT_META
|
|
881
|
+
return isBuiltin ? isBuiltinGatableKind(kind) : true
|
|
882
|
+
}
|
|
883
|
+
|
|
867
884
|
/**
|
|
868
885
|
* Whether an agent kind is actually known to this build — a built-in palette
|
|
869
886
|
* archetype or companion ({@link AGENT_BY_KIND}), an engine system/gate kind
|
|
@@ -15,6 +15,7 @@ import type { Block, Pipeline } from '~/types/domain'
|
|
|
15
15
|
import {
|
|
16
16
|
pipelineAllowedForManualStart,
|
|
17
17
|
pipelineAllowedForSchedule,
|
|
18
|
+
pipelineConditionalCount,
|
|
18
19
|
pipelineDisplaySteps,
|
|
19
20
|
pipelineGateCount,
|
|
20
21
|
} from '~/utils/pipeline'
|
|
@@ -43,12 +44,43 @@ describe('pipelineDisplaySteps', () => {
|
|
|
43
44
|
gates: [false, true, false],
|
|
44
45
|
})
|
|
45
46
|
expect(pipelineDisplaySteps(p)).toEqual([
|
|
46
|
-
{ kind: 'task-estimator', gated: false },
|
|
47
|
-
{ kind: 'coder', gated: true },
|
|
48
|
-
{ kind: 'reviewer', gated: false },
|
|
47
|
+
{ kind: 'task-estimator', gated: false, conditions: [] },
|
|
48
|
+
{ kind: 'coder', gated: true, conditions: [] },
|
|
49
|
+
{ kind: 'reviewer', gated: false, conditions: [] },
|
|
49
50
|
])
|
|
50
51
|
})
|
|
51
52
|
|
|
53
|
+
it('names every reason a step may be skipped, on the step itself', () => {
|
|
54
|
+
// The two causes are reported separately because a reader acts on them differently: an
|
|
55
|
+
// estimate gate is a knob on the pipeline, a service condition is a fact about the task.
|
|
56
|
+
const p = pipeline({
|
|
57
|
+
agentKinds: ['task-estimator', 'tester-api', 'tester-ui'],
|
|
58
|
+
gating: [null, { enabled: true, minRisk: 0.3, onMissingEstimate: 'run' }, null],
|
|
59
|
+
stepOptions: [
|
|
60
|
+
null,
|
|
61
|
+
{ condition: { serviceScope: 'backend' } },
|
|
62
|
+
{ condition: { serviceScope: 'frontend' } },
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
expect(pipelineDisplaySteps(p).map((s) => s.conditions)).toEqual([
|
|
66
|
+
[],
|
|
67
|
+
['estimate', 'backend'],
|
|
68
|
+
['frontend'],
|
|
69
|
+
])
|
|
70
|
+
expect(pipelineConditionalCount(p)).toBe(2)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('LISTS a conditional step rather than filtering it out', () => {
|
|
74
|
+
// Which conditional steps run is a fact about the TASK, and this preview is read while
|
|
75
|
+
// choosing a pipeline — before there is a task to answer it. Hiding them understates the
|
|
76
|
+
// pipeline; listing them silently overstates it, which is what `conditions` fixes.
|
|
77
|
+
const p = pipeline({
|
|
78
|
+
agentKinds: ['coder', 'tester-ui'],
|
|
79
|
+
stepOptions: [null, { condition: { serviceScope: 'frontend' } }],
|
|
80
|
+
})
|
|
81
|
+
expect(pipelineDisplaySteps(p).map((s) => s.kind)).toEqual(['coder', 'tester-ui'])
|
|
82
|
+
})
|
|
83
|
+
|
|
52
84
|
it('drops steps disabled by default — they never run, so listing them would misdescribe it', () => {
|
|
53
85
|
// The short `enabled` array also pins "no entry ⇒ enabled": `tester` has none and stays.
|
|
54
86
|
const p = pipeline({ agentKinds: ['architect', 'coder', 'tester'], enabled: [false, true] })
|
package/app/utils/pipeline.ts
CHANGED
|
@@ -6,18 +6,50 @@ import {
|
|
|
6
6
|
} from '@cat-factory/contracts'
|
|
7
7
|
import type { AgentKind, Block, BlockLevel, Pipeline } from '~/types/domain'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Why a step in a preview might NOT run on a given task. A step is unconditional when both are
|
|
11
|
+
* absent, which is the ordinary case.
|
|
12
|
+
*
|
|
13
|
+
* - `estimate` — an estimate gate (`gating[i]`): the step runs only on a task the earlier
|
|
14
|
+
* `task-estimator` scores above its thresholds.
|
|
15
|
+
* - `frontend` / `backend` — a run condition (`stepOptions[i].condition`): the step runs only
|
|
16
|
+
* where the task changes a service of that kind.
|
|
17
|
+
*
|
|
18
|
+
* They are DISTINCT rather than one `conditional` flag because a reader acts on them differently:
|
|
19
|
+
* an estimate gate is a knob on the pipeline (raise the bar, or clear it), while a service
|
|
20
|
+
* condition is a fact about the task, and nothing about the pipeline will change it.
|
|
21
|
+
*/
|
|
22
|
+
export type StepConditionKind = 'estimate' | 'frontend' | 'backend'
|
|
23
|
+
|
|
9
24
|
/** One agent step of a pipeline as shown in a preview: its kind + whether it's a human-gated step. */
|
|
10
25
|
export interface PipelineDisplayStep {
|
|
11
26
|
kind: AgentKind
|
|
12
27
|
/** A human approval gate pauses the run after this step (`gates[i]`). */
|
|
13
28
|
gated: boolean
|
|
29
|
+
/** Every reason this step may be skipped on a given task; empty ⇒ it always runs. */
|
|
30
|
+
conditions: StepConditionKind[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The reasons the step at `i` may be skipped, in the order a reader meets them. */
|
|
34
|
+
export function stepConditionsAt(pipeline: Pipeline, i: number): StepConditionKind[] {
|
|
35
|
+
const conditions: StepConditionKind[] = []
|
|
36
|
+
if (pipeline.gating?.[i]?.enabled) conditions.push('estimate')
|
|
37
|
+
const scope = pipeline.stepOptions?.[i]?.condition?.serviceScope
|
|
38
|
+
if (scope) conditions.push(scope)
|
|
39
|
+
return conditions
|
|
14
40
|
}
|
|
15
41
|
|
|
16
42
|
/**
|
|
17
43
|
* The steps a pipeline preview should render: the ENABLED steps in order (a step disabled by
|
|
18
44
|
* default — `enabled[i] === false` — is skipped at run, so it would misrepresent the pipeline to
|
|
19
|
-
* list it), each flagged when it carries a human approval gate
|
|
20
|
-
* own chips, mirroring how the run
|
|
45
|
+
* list it), each flagged when it carries a human approval gate and with every reason it may be
|
|
46
|
+
* skipped on a given task. Companions are included as their own chips, mirroring how the run
|
|
47
|
+
* timeline lists every step.
|
|
48
|
+
*
|
|
49
|
+
* A CONDITIONAL step is listed like any other, and says so, rather than being filtered out: which
|
|
50
|
+
* of them run is a fact about the task, and this preview is read while choosing a pipeline —
|
|
51
|
+
* before there is a task to answer it. Hiding them would understate what the pipeline does; a
|
|
52
|
+
* silent full list would overstate it.
|
|
21
53
|
*/
|
|
22
54
|
export function pipelineDisplaySteps(pipeline: Pipeline): PipelineDisplayStep[] {
|
|
23
55
|
return pipeline.agentKinds
|
|
@@ -25,9 +57,28 @@ export function pipelineDisplaySteps(pipeline: Pipeline): PipelineDisplayStep[]
|
|
|
25
57
|
kind,
|
|
26
58
|
enabled: pipeline.enabled?.[i] !== false,
|
|
27
59
|
gated: pipeline.gates?.[i] === true,
|
|
60
|
+
conditions: stepConditionsAt(pipeline, i),
|
|
28
61
|
}))
|
|
29
62
|
.filter((s) => s.enabled)
|
|
30
|
-
.map(({ kind, gated }) => ({ kind, gated }))
|
|
63
|
+
.map(({ kind, gated, conditions }) => ({ kind, gated, conditions }))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The marker each condition renders as: its own icon and its own i18n key. Not one shared
|
|
68
|
+
* "conditional" badge, because the two causes send a reader to different places — an estimate gate
|
|
69
|
+
* is a knob on the pipeline, a service condition is a fact about the task — and a merged badge
|
|
70
|
+
* would name neither. Lives here rather than in a component so the builder library and the picker
|
|
71
|
+
* preview cannot label the same step differently.
|
|
72
|
+
*/
|
|
73
|
+
export const CONDITION_MARKERS: Record<StepConditionKind, { icon: string; key: string }> = {
|
|
74
|
+
estimate: { icon: 'i-lucide-gauge', key: 'pipeline.preview.conditionEstimate' },
|
|
75
|
+
frontend: { icon: 'i-lucide-monitor', key: 'pipeline.preview.conditionFrontend' },
|
|
76
|
+
backend: { icon: 'i-lucide-server', key: 'pipeline.preview.conditionBackend' },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** How many of a pipeline's displayed steps are conditional (the preview's headline count). */
|
|
80
|
+
export function pipelineConditionalCount(pipeline: Pipeline): number {
|
|
81
|
+
return pipelineDisplaySteps(pipeline).filter((s) => s.conditions.length > 0).length
|
|
31
82
|
}
|
|
32
83
|
|
|
33
84
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { binaryCandidateStatusSchema } from '@cat-factory/contracts'
|
|
2
|
+
import { binaryCandidateStatusSchema, stepSkipReasonSchema } from '@cat-factory/contracts'
|
|
3
3
|
import type { ExecutionInstance, PipelineStep } from '~/types/execution'
|
|
4
4
|
import { missingI18nKeys } from '../../test/i18nKeys'
|
|
5
|
-
import { REDIRECT_PARK_PRESENTATION, dedicatedParkView } from './pipelineRender'
|
|
5
|
+
import { REDIRECT_PARK_PRESENTATION, dedicatedParkView, stepSkipReasonKey } from './pipelineRender'
|
|
6
6
|
|
|
7
7
|
/** A minimal coder step; the predicate only reads approval/followUps/forkDecision. */
|
|
8
8
|
const step = (over: Partial<PipelineStep>): PipelineStep =>
|
|
@@ -153,3 +153,79 @@ describe('REDIRECT_PARK_PRESENTATION', () => {
|
|
|
153
153
|
expect(new Set(notices).size).toBe(notices.length)
|
|
154
154
|
})
|
|
155
155
|
})
|
|
156
|
+
|
|
157
|
+
describe('stepSkipReasonKey', () => {
|
|
158
|
+
const skipped = (over: Partial<PipelineStep>): PipelineStep =>
|
|
159
|
+
({ agentKind: 'tester-ui', state: 'done', skipped: true, ...over }) as PipelineStep
|
|
160
|
+
|
|
161
|
+
it('answers null for a step that ran', () => {
|
|
162
|
+
expect(stepSkipReasonKey(step({ state: 'done' }))).toBeNull()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('names the axis, and narrows a condition by the scope still on the step', () => {
|
|
166
|
+
expect(stepSkipReasonKey(skipped({ skipReason: 'gated' }))).toBe(
|
|
167
|
+
'pipeline.progress.skipped.gated',
|
|
168
|
+
)
|
|
169
|
+
expect(stepSkipReasonKey(skipped({ skipReason: 'producer_skipped' }))).toBe(
|
|
170
|
+
'pipeline.progress.skipped.producerSkipped',
|
|
171
|
+
)
|
|
172
|
+
// The condition case reads the scope off the step's own `stepOptions`, so the copy and the
|
|
173
|
+
// scope it names cannot disagree.
|
|
174
|
+
expect(
|
|
175
|
+
stepSkipReasonKey(
|
|
176
|
+
skipped({
|
|
177
|
+
skipReason: 'condition',
|
|
178
|
+
stepOptions: { condition: { serviceScope: 'frontend' } },
|
|
179
|
+
} as Partial<PipelineStep>),
|
|
180
|
+
),
|
|
181
|
+
).toBe('pipeline.progress.skipped.conditionFrontend')
|
|
182
|
+
expect(
|
|
183
|
+
stepSkipReasonKey(
|
|
184
|
+
skipped({
|
|
185
|
+
skipReason: 'condition',
|
|
186
|
+
stepOptions: { condition: { serviceScope: 'backend' } },
|
|
187
|
+
} as Partial<PipelineStep>),
|
|
188
|
+
),
|
|
189
|
+
).toBe('pipeline.progress.skipped.conditionBackend')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('still states the SKIP for a reason this build does not know', () => {
|
|
193
|
+
// A stored run can name a member since retired, and a browser can be older than the member it
|
|
194
|
+
// reads. Losing the reason is acceptable; rendering nothing (so the step reads as one that ran
|
|
195
|
+
// and said nothing) is not, and neither is guessing onto a current member.
|
|
196
|
+
// Cast through `unknown`: the type is CLOSED, so a retired member is unrepresentable at compile
|
|
197
|
+
// time and only reachable from persisted data — which is exactly the case being pinned.
|
|
198
|
+
expect(
|
|
199
|
+
stepSkipReasonKey(
|
|
200
|
+
skipped({ skipReason: 'retired_axis' } as unknown as Partial<PipelineStep>),
|
|
201
|
+
),
|
|
202
|
+
).toBe('pipeline.progress.skipped.unknown')
|
|
203
|
+
expect(stepSkipReasonKey(skipped({}))).toBe('pipeline.progress.skipped.unknown')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('every reason it can name has copy in the catalog', () => {
|
|
207
|
+
// Derived from the vocabulary the engine writes rather than a hand-listed set, so a member
|
|
208
|
+
// added to the picklist is covered here the day it lands instead of falling outside a stale
|
|
209
|
+
// literal list. The `condition` member fans out into two keys (one per service scope).
|
|
210
|
+
const keys = stepSkipReasonSchema.options.flatMap((reason) =>
|
|
211
|
+
reason === 'condition'
|
|
212
|
+
? [
|
|
213
|
+
stepSkipReasonKey(
|
|
214
|
+
skipped({
|
|
215
|
+
skipReason: reason,
|
|
216
|
+
stepOptions: { condition: { serviceScope: 'frontend' } },
|
|
217
|
+
} as unknown as Partial<PipelineStep>),
|
|
218
|
+
)!,
|
|
219
|
+
stepSkipReasonKey(
|
|
220
|
+
skipped({
|
|
221
|
+
skipReason: reason,
|
|
222
|
+
stepOptions: { condition: { serviceScope: 'backend' } },
|
|
223
|
+
} as unknown as Partial<PipelineStep>),
|
|
224
|
+
)!,
|
|
225
|
+
]
|
|
226
|
+
: [stepSkipReasonKey(skipped({ skipReason: reason }))!],
|
|
227
|
+
)
|
|
228
|
+
expect(keys).toHaveLength(stepSkipReasonSchema.options.length + 1)
|
|
229
|
+
expect(missingI18nKeys([...keys, 'pipeline.progress.skipped.unknown'])).toEqual([])
|
|
230
|
+
})
|
|
231
|
+
})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// in one place rather than being re-derived as inline ternaries per component.
|
|
4
4
|
|
|
5
5
|
import type { AgentState, ExecutionInstance, PipelineStep } from '~/types/execution'
|
|
6
|
+
import { isStepSkipReason } from '@cat-factory/contracts'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Visual state of a conditionally-run companion attached to a gate step (today the
|
|
@@ -266,6 +267,48 @@ export function containerPhaseLabel(
|
|
|
266
267
|
return i18n.te(key) ? i18n.t(key) : phase
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The i18n key naming WHY a skipped step was skipped, or null when the step ran.
|
|
272
|
+
*
|
|
273
|
+
* The engine records a machine-readable {@link StepSkipReason} rather than a sentence, so the
|
|
274
|
+
* sentence is composed here where it can be translated. The `condition` case narrows further off
|
|
275
|
+
* the step's own `stepOptions.condition.serviceScope` — the condition stays on the step, so the
|
|
276
|
+
* copy and the scope it names are read from one place and cannot drift.
|
|
277
|
+
*
|
|
278
|
+
* An UNRECOGNISED reason (a stored run naming a member this bundle no longer knows, or a browser
|
|
279
|
+
* older than the member it reads) falls back to the bare "skipped" line rather than rendering
|
|
280
|
+
* nothing or guessing onto a current member: what a reader must not lose is that the step did not
|
|
281
|
+
* run. A `skipped` step with NO reason is the same case — runs predating the field.
|
|
282
|
+
*/
|
|
283
|
+
export function stepSkipReasonKey(step: PipelineStep): string | null {
|
|
284
|
+
if (!step.skipped) return null
|
|
285
|
+
if (!isStepSkipReason(step.skipReason)) return 'pipeline.progress.skipped.unknown'
|
|
286
|
+
switch (step.skipReason) {
|
|
287
|
+
case 'gated':
|
|
288
|
+
return 'pipeline.progress.skipped.gated'
|
|
289
|
+
case 'producer_skipped':
|
|
290
|
+
return 'pipeline.progress.skipped.producerSkipped'
|
|
291
|
+
case 'run_complete':
|
|
292
|
+
return 'pipeline.progress.skipped.runComplete'
|
|
293
|
+
case 'condition':
|
|
294
|
+
return step.stepOptions?.condition?.serviceScope === 'frontend'
|
|
295
|
+
? 'pipeline.progress.skipped.conditionFrontend'
|
|
296
|
+
: 'pipeline.progress.skipped.conditionBackend'
|
|
297
|
+
default:
|
|
298
|
+
return describeUnhandledSkipReason(step.skipReason)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The `never` sink that keeps {@link stepSkipReasonKey}'s switch total: adding a member to
|
|
304
|
+
* `stepSkipReasonSchema` fails the build here until it has copy, while the runtime narrowing above
|
|
305
|
+
* still renders a RETIRED member honestly.
|
|
306
|
+
*/
|
|
307
|
+
function describeUnhandledSkipReason(reason: never): string {
|
|
308
|
+
void reason
|
|
309
|
+
return 'pipeline.progress.skipped.unknown'
|
|
310
|
+
}
|
|
311
|
+
|
|
269
312
|
/**
|
|
270
313
|
* Tailwind classes for a subtask-item status icon. An in-progress item spins only
|
|
271
314
|
* while the run is live: once the run has failed, a step left mid-flight (its item
|
package/i18n/locales/de.json
CHANGED
|
@@ -2116,6 +2116,8 @@
|
|
|
2116
2116
|
"priorDocHint": "Aus der vorherigen Prüfung. Verwende es als Basis: bearbeite die Beschreibung oben und reiche erneut ein.",
|
|
2117
2117
|
"bootstrapping": "Wird initialisiert…",
|
|
2118
2118
|
"viewRequirements": "Anforderungen anzeigen",
|
|
2119
|
+
"mapService": "Service kartieren",
|
|
2120
|
+
"mapServiceNoRepo": "Verknüpfen Sie zuerst ein Repository mit diesem Service — es gibt noch nichts zu kartieren.",
|
|
2119
2121
|
"reRun": "Erneut ausführen",
|
|
2120
2122
|
"run": "Ausführen",
|
|
2121
2123
|
"focus": "Fokussieren",
|
|
@@ -4164,7 +4166,11 @@
|
|
|
4164
4166
|
"preview": {
|
|
4165
4167
|
"stepCount": "{count} Schritt | {count} Schritte",
|
|
4166
4168
|
"gateCount": "{count} Freigabe | {count} Freigaben",
|
|
4167
|
-
"gated": "Manuelle Freigabe nach diesem Schritt"
|
|
4169
|
+
"gated": "Manuelle Freigabe nach diesem Schritt",
|
|
4170
|
+
"conditionalCount": "{count} bedingter Schritt | {count} bedingte Schritte",
|
|
4171
|
+
"conditionEstimate": "Läuft nur, wenn die Aufwandsschätzung die Schwellenwerte dieses Schritts erreicht",
|
|
4172
|
+
"conditionFrontend": "Läuft nur, wenn die Aufgabe einen Frontend-Service ändert",
|
|
4173
|
+
"conditionBackend": "Läuft nur, wenn die Aufgabe einen Nicht-Frontend-Service ändert"
|
|
4168
4174
|
},
|
|
4169
4175
|
"picker": {
|
|
4170
4176
|
"noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus.",
|
|
@@ -4254,6 +4260,11 @@
|
|
|
4254
4260
|
"body": "\"{name}\" wird entfernt. Dies kann nicht rückgängig gemacht werden."
|
|
4255
4261
|
},
|
|
4256
4262
|
"disabledStepTooltip": "Deaktiviert, wird beim Lauf übersprungen",
|
|
4263
|
+
"condition": {
|
|
4264
|
+
"always": "Läuft bei jeder Aufgabe — klicken, um eine Bedingung zu setzen",
|
|
4265
|
+
"frontend": "Läuft nur bei Aufgaben, die einen Frontend-Service ändern",
|
|
4266
|
+
"backend": "Läuft nur bei Aufgaben, die einen Nicht-Frontend-Service ändern"
|
|
4267
|
+
},
|
|
4257
4268
|
"cancelEdit": "Bearbeitung abbrechen",
|
|
4258
4269
|
"clear": "Leeren",
|
|
4259
4270
|
"update": "Pipeline aktualisieren",
|
|
@@ -4339,6 +4350,7 @@
|
|
|
4339
4350
|
"negative-prompt": "Negativer Prompt",
|
|
4340
4351
|
"seed": "Fester Seed",
|
|
4341
4352
|
"aspect-ratio": "Seitenverhältnis",
|
|
4353
|
+
"exact-size": "Exakte Größe",
|
|
4342
4354
|
"candidate-batch": "Mehrere Kandidaten pro Aufruf",
|
|
4343
4355
|
"upscale": "Hochskalierung",
|
|
4344
4356
|
"transparent-background": "Transparenter Hintergrund",
|
|
@@ -4360,6 +4372,11 @@
|
|
|
4360
4372
|
"binaryNegativePromptPlaceholder": "was vermieden werden soll",
|
|
4361
4373
|
"binaryAspectRatio": "Seitenverhältnis",
|
|
4362
4374
|
"binaryAspectRatioPlaceholder": "16:9",
|
|
4375
|
+
"binaryOutputSize": "Ausgabegröße",
|
|
4376
|
+
"binaryOutputSizeWidth": "Breite",
|
|
4377
|
+
"binaryOutputSizeHeight": "Höhe",
|
|
4378
|
+
"binaryOutputSizeIncomplete": "Nicht gespeichert: eine exakte Größe braucht Breite und Höhe.",
|
|
4379
|
+
"binaryOutputSizeAmbiguous": "Eine exakte Ausgabegröße lässt sich nicht mit {options} kombinieren: beides gibt die gelieferten Maße ein zweites Mal an. Behalte das, was die Anforderung ist.",
|
|
4363
4380
|
"binarySeed": "Seed",
|
|
4364
4381
|
"binarySeedPlaceholder": "fester Seed",
|
|
4365
4382
|
"binaryTransparent": "Transparenter Hintergrund",
|
|
@@ -4421,6 +4438,14 @@
|
|
|
4421
4438
|
"elapsedTooltip": "Verstrichene Zeit für diesen Schritt",
|
|
4422
4439
|
"prReview": {
|
|
4423
4440
|
"review": "Befunde prüfen"
|
|
4441
|
+
},
|
|
4442
|
+
"skipped": {
|
|
4443
|
+
"gated": "Übersprungen: Die Aufgabenschätzung lag unter den Schwellenwerten dieses Schritts.",
|
|
4444
|
+
"conditionFrontend": "Übersprungen: Diese Aufgabe ändert keinen Frontend-Dienst, es gibt also keine Oberfläche zu prüfen.",
|
|
4445
|
+
"conditionBackend": "Übersprungen: Diese Aufgabe ändert nur einen Frontend-Dienst, es gibt also keine API dahinter zu prüfen.",
|
|
4446
|
+
"producerSkipped": "Übersprungen: Der geprüfte Schritt wurde übersprungen, es gibt nichts zu bewerten.",
|
|
4447
|
+
"runComplete": "Übersprungen: Der Lauf endete vor diesem Schritt, es blieb nichts zu tun.",
|
|
4448
|
+
"unknown": "Übersprungen: Dieser Schritt wurde nicht ausgeführt."
|
|
4424
4449
|
}
|
|
4425
4450
|
},
|
|
4426
4451
|
"health": {
|
|
@@ -5061,6 +5086,8 @@
|
|
|
5061
5086
|
"unknownServices": "Nennt einen Dienst, den der Katalog nicht enthält: {ids}. Der Eintrag bleibt wie angegeben erhalten; prüfe die Kennung gegen den Katalog des Boards. | Nennt Dienste, die der Katalog nicht enthält: {ids}. Die Einträge bleiben wie angegeben erhalten; prüfe die Kennungen gegen den Katalog des Boards.",
|
|
5062
5087
|
"targetUnknown": "Der Katalog enthält den eigenen Speicherdienst dieses Schritts nicht mehr ({id}), deshalb konnte nichts unten dagegen geprüft werden. Registriere ihn erneut, oder verweise den Schritt auf einen anderen Dienst.",
|
|
5063
5088
|
"undeliveredMediaTypes": "Dieser Schritt sollte {formats} liefern, und kein Artefakt unten meldet dieses Format. | Dieser Schritt sollte {formats} liefern, und kein Artefakt unten meldet diese Formate.",
|
|
5089
|
+
"missized": "1 Artefakt wurde in einer anderen Größe als {width}×{height} geliefert. | {count} Artefakte wurden in einer anderen Größe als {width}×{height} geliefert.",
|
|
5090
|
+
"sizeUnreported": "1 Artefakt meldet keine Abmessungen, daher konnte seine Größe nicht geprüft werden. | {count} Artefakte melden keine Abmessungen, daher konnten ihre Größen nicht geprüft werden.",
|
|
5064
5091
|
"misdirected": "1 Artefakt ging an einen anderen Dienst als {target}. | {count} Artefakte gingen an einen anderen Dienst als {target}.",
|
|
5065
5092
|
"invalidEntries": "1 angegebener Eintrag wurde verworfen: er nannte weder Dienst noch Ablageort. | {count} angegebene Einträge wurden verworfen: sie nannten weder Dienst noch Ablageort.",
|
|
5066
5093
|
"omitted": "1 weiteres Artefakt wurde jenseits der Berichtsgrenze angegeben und ist nicht aufgeführt. | {count} weitere Artefakte wurden jenseits der Berichtsgrenze angegeben und sind nicht aufgeführt.",
|
|
@@ -5068,6 +5095,7 @@
|
|
|
5068
5095
|
"generatorsUnverified": "Die generativen Integrationen dieser Installation konnten beim Abschluss des Schritts nicht gelesen werden, daher wurden die unten genannten Integrationen nicht dagegen geprüft. Die Einträge bleiben wie angegeben erhalten."
|
|
5069
5096
|
},
|
|
5070
5097
|
"unknownGeneratorBadge": "Nicht registriert",
|
|
5098
|
+
"missizedBadge": "Falsche Größe",
|
|
5071
5099
|
"candidates": {
|
|
5072
5100
|
"automatic": "{total} Kandidat wurde erzeugt und ohne Prüfung automatisch behalten.",
|
|
5073
5101
|
"chosen": "{kept} von {total} erzeugten Kandidaten wurden behalten.",
|