@cat-factory/app 0.205.0 → 0.207.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 +33 -0
- package/app/components/pipeline/BinaryOutputStepPicker.logic.spec.ts +58 -0
- package/app/components/pipeline/BinaryOutputStepPicker.logic.ts +55 -0
- package/app/components/pipeline/BinaryOutputStepPicker.vue +169 -5
- package/app/composables/usePipelineErrorToast.ts +31 -3
- package/app/stores/agents.ts +15 -1
- package/app/stores/workspace/hydrate.ts +6 -1
- package/app/utils/binaryOutput.spec.ts +169 -0
- package/app/utils/binaryOutput.ts +167 -12
- package/i18n/locales/de.json +20 -2
- package/i18n/locales/en.json +21 -2
- package/i18n/locales/es.json +20 -2
- package/i18n/locales/fr.json +20 -2
- package/i18n/locales/he.json +20 -2
- package/i18n/locales/it.json +20 -2
- package/i18n/locales/ja.json +20 -2
- package/i18n/locales/pl.json +20 -2
- package/i18n/locales/tr.json +20 -2
- package/i18n/locales/uk.json +20 -2
- package/package.json +2 -2
|
@@ -253,6 +253,41 @@ describe('the generative half of the read model', () => {
|
|
|
253
253
|
expect(binaryOutputHasWarnings(view!)).toBe(true)
|
|
254
254
|
})
|
|
255
255
|
|
|
256
|
+
it('surfaces an UNCHECKED generative verdict, and does not let it read as a clean one', () => {
|
|
257
|
+
// The settlement-side twin of the picker's `generators_unavailable`. An empty
|
|
258
|
+
// `unknownDeclaredGenerators` normally means every claimed id checked out, so a reader
|
|
259
|
+
// deciding whether these artifacts are real would take silence here as confirmation. The
|
|
260
|
+
// flag has to reach both the line AND the collapsed summary's tone, or the one place it is
|
|
261
|
+
// stated is behind a section that looks like it has nothing to say.
|
|
262
|
+
const view = binaryOutputView(
|
|
263
|
+
step({
|
|
264
|
+
stepOptions: { binaryOutput: { storageServiceId: 'files', generatorIds: ['retro'] } },
|
|
265
|
+
binaryOutputs: report({
|
|
266
|
+
stored: [{ ...artifact('files', 'a.png'), generator: 'retro' }],
|
|
267
|
+
generatorsUnverified: true,
|
|
268
|
+
}),
|
|
269
|
+
}),
|
|
270
|
+
)
|
|
271
|
+
expect(view?.generatorsUnverified).toBe(true)
|
|
272
|
+
expect(view?.unknownDeclaredGenerators).toEqual([])
|
|
273
|
+
// The artifacts themselves survived the outage — that is the whole point of recording them.
|
|
274
|
+
expect(view?.rows).toHaveLength(1)
|
|
275
|
+
expect(binaryOutputHasWarnings(view!)).toBe(true)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('reads a checked-and-clean report as clean, which is what makes the flag mean anything', () => {
|
|
279
|
+
const view = binaryOutputView(
|
|
280
|
+
step({
|
|
281
|
+
stepOptions: { binaryOutput: { storageServiceId: 'files', generatorIds: ['retro'] } },
|
|
282
|
+
binaryOutputs: report({
|
|
283
|
+
stored: [{ ...artifact('files', 'a.png'), generator: 'retro' }],
|
|
284
|
+
}),
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
expect(view?.generatorsUnverified).toBe(false)
|
|
288
|
+
expect(binaryOutputHasWarnings(view!)).toBe(false)
|
|
289
|
+
})
|
|
290
|
+
|
|
256
291
|
it('carries the step selection through, and treats empty as a real state', () => {
|
|
257
292
|
const configured = binaryOutputView(
|
|
258
293
|
step({
|
|
@@ -276,6 +311,66 @@ describe('the generative half of the read model', () => {
|
|
|
276
311
|
})
|
|
277
312
|
})
|
|
278
313
|
|
|
314
|
+
// The one judgement this surface can make that admission cannot: admission checked what the
|
|
315
|
+
// selected integrations CAN emit, this checks what the run actually came back with.
|
|
316
|
+
describe('the delivered-format check', () => {
|
|
317
|
+
const required = (mediaTypes: string[], stored: { location: string; contentType?: string }[]) =>
|
|
318
|
+
binaryOutputView(
|
|
319
|
+
step({
|
|
320
|
+
stepOptions: { binaryOutput: { storageServiceId: 'files', mediaTypes } },
|
|
321
|
+
binaryOutputs: report({
|
|
322
|
+
stored: stored.map((entry) => ({ service: 'files', ...entry })),
|
|
323
|
+
}),
|
|
324
|
+
}),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
it('names a required format no declared artifact reports', () => {
|
|
328
|
+
const view = required(
|
|
329
|
+
['model/gltf-binary', 'model/fbx'],
|
|
330
|
+
[{ location: 'a.glb', contentType: 'model/gltf-binary' }],
|
|
331
|
+
)
|
|
332
|
+
expect(view?.undeliveredMediaTypes).toEqual(['model/fbx'])
|
|
333
|
+
expect(binaryOutputHasWarnings(view!)).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('reduces the agent’s own spelling before comparing, and only then', () => {
|
|
337
|
+
// The requirement came through `mediaTypeSchema`; the artifact's content type is the model's
|
|
338
|
+
// prose. Comparing them raw reports a format as undelivered while the file sits where it was
|
|
339
|
+
// asked for.
|
|
340
|
+
expect(
|
|
341
|
+
required(['model/gltf-binary'], [{ location: 'a.glb', contentType: 'Model/GLTF-Binary' }])
|
|
342
|
+
?.undeliveredMediaTypes,
|
|
343
|
+
).toEqual([])
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('does not accept a near neighbour of the required format', () => {
|
|
347
|
+
// The entire point of requiring a format rather than a content type: both of these are 3D.
|
|
348
|
+
expect(
|
|
349
|
+
required(['model/gltf-binary'], [{ location: 'a.fbx', contentType: 'model/fbx' }])
|
|
350
|
+
?.undeliveredMediaTypes,
|
|
351
|
+
).toEqual(['model/gltf-binary'])
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('counts an artifact that reports no content type as covering nothing', () => {
|
|
355
|
+
expect(required(['model/gltf-binary'], [{ location: 'a.glb' }])?.undeliveredMediaTypes).toEqual(
|
|
356
|
+
['model/gltf-binary'],
|
|
357
|
+
)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('stays silent when there are no artifacts, because the state line already said so', () => {
|
|
361
|
+
// "It did not deliver a GLB" on top of "it declared nothing" is one fact stated twice as if
|
|
362
|
+
// it were two, and the second one adds nothing a reader can act on.
|
|
363
|
+
const view = binaryOutputView(
|
|
364
|
+
step({
|
|
365
|
+
stepOptions: { binaryOutput: { storageServiceId: 'files', mediaTypes: ['model/obj'] } },
|
|
366
|
+
binaryOutputs: report({ undeclared: true }),
|
|
367
|
+
}),
|
|
368
|
+
)
|
|
369
|
+
expect(view?.mediaTypes).toEqual(['model/obj'])
|
|
370
|
+
expect(view?.undeliveredMediaTypes).toEqual([])
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
279
374
|
describe('binaryOutputPickIssues, generative half', () => {
|
|
280
375
|
const catalog = [{ id: 'files', capabilities: ['asset-storage'] }]
|
|
281
376
|
const generators = [
|
|
@@ -332,6 +427,80 @@ describe('binaryOutputPickIssues, generative half', () => {
|
|
|
332
427
|
const pick = binaryOutputPickIssues({ storageServiceId: 'files' }, catalog, true, generators)
|
|
333
428
|
expect(pick.issues).toEqual([])
|
|
334
429
|
})
|
|
430
|
+
|
|
431
|
+
// The FORMAT half, mirroring kernel's `binaryFormatCoverage` — and its three outcomes, which
|
|
432
|
+
// are what a second copy of the rule most easily loses.
|
|
433
|
+
const meshy = {
|
|
434
|
+
id: 'meshy',
|
|
435
|
+
modalities: ['3d-model' as const],
|
|
436
|
+
mediaTypes: ['model/gltf-binary'],
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
it('mirrors the refusal for a format no DECLARING integration emits', () => {
|
|
440
|
+
const pick = binaryOutputPickIssues(
|
|
441
|
+
{ storageServiceId: 'files', generatorIds: ['meshy'], mediaTypes: ['model/fbx'] },
|
|
442
|
+
catalog,
|
|
443
|
+
true,
|
|
444
|
+
[meshy],
|
|
445
|
+
)
|
|
446
|
+
expect(pick.issues).toContain('media_type_uncovered')
|
|
447
|
+
expect(pick.uncoveredMediaTypes).toEqual(['model/fbx'])
|
|
448
|
+
expect(pick.unverifiableMediaTypes).toEqual([])
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('keeps an UNCHECKABLE format apart from a refused one, because the step still starts', () => {
|
|
452
|
+
// `retro` declares no formats — "only my modality is known". Flagging this as a refusal would
|
|
453
|
+
// send someone editing a selection the backend admits; saying nothing would present an
|
|
454
|
+
// unchecked requirement as a checked one.
|
|
455
|
+
const pick = binaryOutputPickIssues(
|
|
456
|
+
{ storageServiceId: 'files', generatorIds: ['retro'], mediaTypes: ['image/webp'] },
|
|
457
|
+
catalog,
|
|
458
|
+
true,
|
|
459
|
+
generators,
|
|
460
|
+
)
|
|
461
|
+
expect(pick.issues).toContain('media_type_unverifiable')
|
|
462
|
+
expect(pick.issues).not.toContain('media_type_uncovered')
|
|
463
|
+
expect(pick.unverifiableMediaTypes).toEqual(['image/webp'])
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('accepts a format the selection covers, however many other formats it emits', () => {
|
|
467
|
+
const pick = binaryOutputPickIssues(
|
|
468
|
+
{ storageServiceId: 'files', generatorIds: ['meshy'], mediaTypes: ['model/gltf-binary'] },
|
|
469
|
+
catalog,
|
|
470
|
+
true,
|
|
471
|
+
[meshy],
|
|
472
|
+
)
|
|
473
|
+
expect(pick.issues).toEqual([])
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('reports an UNREADABLE set as an outage and makes no claim about the selection', () => {
|
|
477
|
+
// The picker's half of the mothership-mode disposition. A failed read arrives as the same
|
|
478
|
+
// empty list an unregistering deployment produces, so judging the selection against it would
|
|
479
|
+
// tell someone their step names an integration nobody registered — about an id that is very
|
|
480
|
+
// likely fine, and with the remedy pointing at the wrong repository.
|
|
481
|
+
const pick = binaryOutputPickIssues(
|
|
482
|
+
{ storageServiceId: 'files', generatorIds: ['retro'], modalities: ['audio'] },
|
|
483
|
+
catalog,
|
|
484
|
+
true,
|
|
485
|
+
[],
|
|
486
|
+
true,
|
|
487
|
+
)
|
|
488
|
+
expect(pick.issues).toEqual(['generators_unavailable'])
|
|
489
|
+
expect(pick.unknownGeneratorIds).toEqual([])
|
|
490
|
+
expect(pick.uncoveredModalities).toEqual([])
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('still judges an EMPTY set, which is a real answer about the deployment', () => {
|
|
494
|
+
// The distinction the flag exists for: same empty list, opposite fact, opposite message.
|
|
495
|
+
const pick = binaryOutputPickIssues(
|
|
496
|
+
{ storageServiceId: 'files', generatorIds: ['retro'] },
|
|
497
|
+
catalog,
|
|
498
|
+
true,
|
|
499
|
+
[],
|
|
500
|
+
false,
|
|
501
|
+
)
|
|
502
|
+
expect(pick.issues).toContain('unknown_generator')
|
|
503
|
+
})
|
|
335
504
|
})
|
|
336
505
|
|
|
337
506
|
describe('binaryOutputPickIssues', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ASSET_STORAGE_CAPABILITY } from '@cat-factory/contracts'
|
|
1
|
+
import { ASSET_STORAGE_CAPABILITY, normalizeMediaType } from '@cat-factory/contracts'
|
|
2
2
|
import type { BinaryModality, RegisteredBinaryGenerator } from '@cat-factory/contracts'
|
|
3
3
|
import type {
|
|
4
4
|
BinaryOutputArtifact,
|
|
@@ -118,6 +118,28 @@ export interface BinaryOutputView {
|
|
|
118
118
|
* Empty ⇒ the step imposes no requirement, so nothing is uncovered by construction.
|
|
119
119
|
*/
|
|
120
120
|
modalities: readonly BinaryModality[]
|
|
121
|
+
/**
|
|
122
|
+
* The concrete FORMATS the step declares it must deliver
|
|
123
|
+
* (`stepOptions.binaryOutput.mediaTypes`), for the deliverables where the container is the
|
|
124
|
+
* requirement rather than a preference — a mesh the engine can import.
|
|
125
|
+
*/
|
|
126
|
+
mediaTypes: readonly string[]
|
|
127
|
+
/**
|
|
128
|
+
* Required formats no DECLARED artifact reports a matching `contentType` for.
|
|
129
|
+
*
|
|
130
|
+
* The one judgement this surface can make that admission cannot: admission checked what the
|
|
131
|
+
* selected integrations CAN emit, and this checks what the run actually came back with. It is
|
|
132
|
+
* derived in code from the two records the step already carries — never read off the agent's
|
|
133
|
+
* prose — and it is the question a human opens this panel to answer once a mesh is supposed to
|
|
134
|
+
* load in a build.
|
|
135
|
+
*
|
|
136
|
+
* Computed only when there ARE artifacts to compare against: with none, the state line above
|
|
137
|
+
* already says nothing was recorded, and "it did not deliver a GLB" on top of "it declared
|
|
138
|
+
* nothing" is the same fact stated twice as if it were two. An artifact that reports no
|
|
139
|
+
* `contentType` covers nothing — the platform does not guess a format from a filename — so a
|
|
140
|
+
* report with formats required and none reported says so rather than passing.
|
|
141
|
+
*/
|
|
142
|
+
undeliveredMediaTypes: readonly string[]
|
|
121
143
|
/**
|
|
122
144
|
* Integration ids the AGENT named that the deployment does not register. The generative twin of
|
|
123
145
|
* {@link unknownDeclaredServices}, and it needs no exclusion to stay disjoint from anything —
|
|
@@ -126,6 +148,14 @@ export interface BinaryOutputView {
|
|
|
126
148
|
* would leave an artifact attributed to something nobody can look up, with nothing saying so.
|
|
127
149
|
*/
|
|
128
150
|
unknownDeclaredGenerators: readonly string[]
|
|
151
|
+
/**
|
|
152
|
+
* True when the deployment's integrations could not be READ at settlement, so no claimed id was
|
|
153
|
+
* checked against them. Rendered as its own line and never as an empty
|
|
154
|
+
* {@link unknownDeclaredGenerators}: that list being empty otherwise means "every id checked
|
|
155
|
+
* out", and a reader deciding whether these artifacts are real must not be shown a clean bill
|
|
156
|
+
* of health nobody actually issued.
|
|
157
|
+
*/
|
|
158
|
+
generatorsUnverified: boolean
|
|
129
159
|
/** Entries dropped because they were not `{ service, location }` objects. */
|
|
130
160
|
invalidEntries: number
|
|
131
161
|
/** Valid entries dropped past the report's cap — so {@link rows} is a PREFIX. */
|
|
@@ -160,6 +190,7 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
160
190
|
const contextServices = config?.contextServiceIds ?? []
|
|
161
191
|
const generators = config?.generatorIds ?? []
|
|
162
192
|
const modalities = config?.modalities ?? []
|
|
193
|
+
const mediaTypes = config?.mediaTypes ?? []
|
|
163
194
|
if (!report) {
|
|
164
195
|
return {
|
|
165
196
|
// A step still queued has not had the chance to record anything, which is a different
|
|
@@ -172,7 +203,10 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
172
203
|
unknownDeclaredServices: [],
|
|
173
204
|
generators,
|
|
174
205
|
modalities,
|
|
206
|
+
mediaTypes,
|
|
207
|
+
undeliveredMediaTypes: [],
|
|
175
208
|
unknownDeclaredGenerators: [],
|
|
209
|
+
generatorsUnverified: false,
|
|
176
210
|
invalidEntries: 0,
|
|
177
211
|
omitted: 0,
|
|
178
212
|
misdirected: 0,
|
|
@@ -199,13 +233,40 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
199
233
|
unknownDeclaredServices: report.unknownServices.filter((id) => id !== target),
|
|
200
234
|
generators,
|
|
201
235
|
modalities,
|
|
236
|
+
mediaTypes,
|
|
237
|
+
undeliveredMediaTypes: undeliveredMediaTypes(mediaTypes, rows),
|
|
202
238
|
unknownDeclaredGenerators: report.unknownGenerators,
|
|
239
|
+
generatorsUnverified: report.generatorsUnverified === true,
|
|
203
240
|
invalidEntries: report.invalidEntries,
|
|
204
241
|
omitted: report.omitted,
|
|
205
242
|
misdirected: rows.filter((row) => row.misdirected).length,
|
|
206
243
|
}
|
|
207
244
|
}
|
|
208
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The required formats {@link BinaryOutputRow.contentType} does not account for.
|
|
248
|
+
*
|
|
249
|
+
* Compared through `normalizeMediaType` on the DECLARED side only: the step's requirement already
|
|
250
|
+
* came through `mediaTypeSchema` at the write boundary, while the artifact's content type is the
|
|
251
|
+
* agent's own prose and matches nothing until it is reduced the same way. Exact match after that,
|
|
252
|
+
* never a modality fallback — an artifact reported as `model/fbx` does not satisfy a requirement
|
|
253
|
+
* for `model/gltf-binary` just because both are 3D, and that is the entire point of requiring a
|
|
254
|
+
* format rather than a content type.
|
|
255
|
+
*/
|
|
256
|
+
function undeliveredMediaTypes(
|
|
257
|
+
required: readonly string[],
|
|
258
|
+
rows: readonly BinaryOutputRow[],
|
|
259
|
+
): string[] {
|
|
260
|
+
if (required.length === 0 || rows.length === 0) return []
|
|
261
|
+
const delivered = new Set(
|
|
262
|
+
rows.flatMap((row) => {
|
|
263
|
+
const normalized = row.contentType ? normalizeMediaType(row.contentType) : null
|
|
264
|
+
return normalized ? [normalized] : []
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
return required.filter((mediaType) => !delivered.has(mediaType))
|
|
268
|
+
}
|
|
269
|
+
|
|
209
270
|
/**
|
|
210
271
|
* Which failure the report records, in the order the parser can produce them. `parseFailed`
|
|
211
272
|
* and `undeclared` are checked BEFORE the (always empty in those cases) `stored` list, so a
|
|
@@ -275,6 +336,10 @@ export const BINARY_OUTPUT_STATE_KEYS: Record<
|
|
|
275
336
|
* unknown service ids, dropped entries, a truncated list, or a misdirected artifact. Drives
|
|
276
337
|
* the collapsed summary row's tone, so a report with losses can't read as a clean one from
|
|
277
338
|
* the outside of a collapsed section.
|
|
339
|
+
*
|
|
340
|
+
* An UNCHECKED verdict counts as one of those qualifications, and it is the only member here
|
|
341
|
+
* that is not itself a loss: nothing went wrong with the run, but the report is quieter than it
|
|
342
|
+
* looks, and a collapsed section that renders it as clean would hide the one line saying so.
|
|
278
343
|
*/
|
|
279
344
|
export function binaryOutputHasWarnings(view: BinaryOutputView): boolean {
|
|
280
345
|
return (
|
|
@@ -283,6 +348,8 @@ export function binaryOutputHasWarnings(view: BinaryOutputView): boolean {
|
|
|
283
348
|
view.targetUnknown ||
|
|
284
349
|
view.unknownDeclaredServices.length > 0 ||
|
|
285
350
|
view.unknownDeclaredGenerators.length > 0 ||
|
|
351
|
+
view.generatorsUnverified ||
|
|
352
|
+
view.undeliveredMediaTypes.length > 0 ||
|
|
286
353
|
view.invalidEntries > 0 ||
|
|
287
354
|
view.omitted > 0 ||
|
|
288
355
|
view.misdirected > 0
|
|
@@ -310,6 +377,11 @@ export type BinaryOutputPickIssue =
|
|
|
310
377
|
| 'catalog_unavailable'
|
|
311
378
|
/** The catalog resolved, but nothing in it declares the `asset-storage` capability. */
|
|
312
379
|
| 'no_storage_service'
|
|
380
|
+
/** The deployment's registered integrations could not be READ (a mothership-mode node whose
|
|
381
|
+
* mothership is unreachable). Kept apart from an empty set for the same reason
|
|
382
|
+
* `catalog_unavailable` is: an empty picker is a claim about the deployment's BUILD, and
|
|
383
|
+
* acting on it during an outage sends someone looking in the wrong repository. */
|
|
384
|
+
| 'generators_unavailable'
|
|
313
385
|
/** An enabled generator step with no storage selection — refused at save AND at start. */
|
|
314
386
|
| 'not_selected'
|
|
315
387
|
/** The selected storage id is not in the resolved catalog (kernel's `unknown_service`). */
|
|
@@ -325,6 +397,21 @@ export type BinaryOutputPickIssue =
|
|
|
325
397
|
| 'unknown_generator'
|
|
326
398
|
/** A content type the step declares it delivers is produced by NO selected integration. */
|
|
327
399
|
| 'modality_uncovered'
|
|
400
|
+
/**
|
|
401
|
+
* A concrete FORMAT the step declares it delivers is emitted by no selected integration that
|
|
402
|
+
* declared its formats (kernel's `media_type_uncovered` spelling verbatim). A refusal, like the
|
|
403
|
+
* two above it.
|
|
404
|
+
*/
|
|
405
|
+
| 'media_type_uncovered'
|
|
406
|
+
/**
|
|
407
|
+
* A declared format nothing selected claims, where a selected integration declares no formats
|
|
408
|
+
* at all — so it MIGHT be met and nothing may say otherwise. ADVISORY: unlike every other
|
|
409
|
+
* member here it is not a refusal and must not be styled as one, or a step that is going to
|
|
410
|
+
* start perfectly well reads as broken. It is here rather than nowhere because the alternative
|
|
411
|
+
* is silence about a requirement the platform could not check, which is how "nobody looked"
|
|
412
|
+
* comes to look exactly like "this is fine".
|
|
413
|
+
*/
|
|
414
|
+
| 'media_type_unverifiable'
|
|
328
415
|
|
|
329
416
|
/** What the builder found wrong with one step's selection, and which ids to name. */
|
|
330
417
|
export interface BinaryOutputPickState {
|
|
@@ -335,6 +422,10 @@ export interface BinaryOutputPickState {
|
|
|
335
422
|
unknownGeneratorIds: readonly string[]
|
|
336
423
|
/** The declared content types nothing selected can produce, for the message that names them. */
|
|
337
424
|
uncoveredModalities: readonly BinaryModality[]
|
|
425
|
+
/** The declared formats no DECLARING integration emits — the refusal's own list. */
|
|
426
|
+
uncoveredMediaTypes: readonly string[]
|
|
427
|
+
/** The declared formats that could not be judged, kept apart from the refusal above. */
|
|
428
|
+
unverifiableMediaTypes: readonly string[]
|
|
338
429
|
}
|
|
339
430
|
|
|
340
431
|
/**
|
|
@@ -342,28 +433,84 @@ export interface BinaryOutputPickState {
|
|
|
342
433
|
* `binaryGeneratorSelectionIssues` so the builder surfaces the `binary_output_generator_invalid`
|
|
343
434
|
* refusal before the round trip rather than inventing a second opinion.
|
|
344
435
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
436
|
+
* `unavailable` is the one state that is NOT derivable from the list, which is why the snapshot
|
|
437
|
+
* carries it as its own flag. An empty list normally IS a real empty — this deployment registers
|
|
438
|
+
* none — and that is exactly why a selected id in that state is `unknown_generator` rather than
|
|
439
|
+
* silence. But on a mothership-mode deployment the set is read from the mothership, and a failed
|
|
440
|
+
* read is the same empty list about a completely different fact. Reporting `unknown_generator`
|
|
441
|
+
* there would tell someone their step names an integration nobody registered, about an id that
|
|
442
|
+
* is very likely fine — the same misattribution the backend refuses to make at admission. So an
|
|
443
|
+
* unavailable set reports THAT and stops: every other judgement below is a claim about a list
|
|
444
|
+
* nobody managed to read.
|
|
350
445
|
*/
|
|
351
446
|
function generatorPickIssues(
|
|
352
447
|
config: BinaryOutputConfig | undefined,
|
|
353
|
-
generators: readonly Pick<RegisteredBinaryGenerator, 'id' | 'modalities'>[],
|
|
354
|
-
|
|
448
|
+
generators: readonly Pick<RegisteredBinaryGenerator, 'id' | 'modalities' | 'mediaTypes'>[],
|
|
449
|
+
unavailable: boolean,
|
|
450
|
+
): {
|
|
451
|
+
issues: BinaryOutputPickIssue[]
|
|
452
|
+
unknownGeneratorIds: string[]
|
|
453
|
+
uncovered: BinaryModality[]
|
|
454
|
+
uncoveredMediaTypes: string[]
|
|
455
|
+
unverifiableMediaTypes: string[]
|
|
456
|
+
} {
|
|
457
|
+
const none = {
|
|
458
|
+
unknownGeneratorIds: [],
|
|
459
|
+
uncovered: [],
|
|
460
|
+
uncoveredMediaTypes: [],
|
|
461
|
+
unverifiableMediaTypes: [],
|
|
462
|
+
}
|
|
463
|
+
if (unavailable) return { issues: ['generators_unavailable'], ...none }
|
|
355
464
|
const byId = new Map(generators.map((g) => [g.id, g]))
|
|
356
465
|
const selectedIds = config?.generatorIds ?? []
|
|
357
466
|
const unknownGeneratorIds = selectedIds.filter((id) => !byId.has(id))
|
|
358
467
|
// Coverage is judged against what RESOLVED, exactly as admission judges it: an unknown id
|
|
359
468
|
// contributes no content types, so a step whose only audio generator is unregistered is told
|
|
360
469
|
// BOTH things — the id is gone, and the requirement it was covering is now uncovered.
|
|
361
|
-
const
|
|
470
|
+
const selected = selectedIds.flatMap((id) => byId.get(id) ?? [])
|
|
471
|
+
const covered = new Set(selected.flatMap((g) => g.modalities))
|
|
362
472
|
const uncovered = (config?.modalities ?? []).filter((m) => !covered.has(m))
|
|
473
|
+
const format = formatCoverage(config?.mediaTypes ?? [], selected)
|
|
363
474
|
const issues: BinaryOutputPickIssue[] = []
|
|
364
475
|
if (unknownGeneratorIds.length) issues.push('unknown_generator')
|
|
365
476
|
if (uncovered.length) issues.push('modality_uncovered')
|
|
366
|
-
|
|
477
|
+
if (format.uncovered.length) issues.push('media_type_uncovered')
|
|
478
|
+
if (format.unverifiable.length) issues.push('media_type_unverifiable')
|
|
479
|
+
return {
|
|
480
|
+
issues,
|
|
481
|
+
unknownGeneratorIds,
|
|
482
|
+
uncovered,
|
|
483
|
+
uncoveredMediaTypes: format.uncovered,
|
|
484
|
+
unverifiableMediaTypes: format.unverifiable,
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The SPA's copy of kernel's `binaryFormatCoverage`, restated for the reason the two `*_service`
|
|
490
|
+
* members above are: the builder cannot see kernel, and the wire vocabulary that does cross
|
|
491
|
+
* (`@cat-factory/contracts`) carries the schema, not the rule.
|
|
492
|
+
*
|
|
493
|
+
* The THIRD outcome is what must not be lost in the copying. A generator that declares no formats
|
|
494
|
+
* has said "only my modality is known" — a documented state, not an empty answer — so a
|
|
495
|
+
* requirement it cannot be judged against is unverifiable and the step still starts. Collapsing
|
|
496
|
+
* that into `uncovered` would flag steps the backend admits (and send someone editing a selection
|
|
497
|
+
* that is fine); collapsing it into silence would present an unchecked requirement as a checked
|
|
498
|
+
* one.
|
|
499
|
+
*/
|
|
500
|
+
function formatCoverage(
|
|
501
|
+
required: readonly string[],
|
|
502
|
+
selected: readonly Pick<RegisteredBinaryGenerator, 'mediaTypes'>[],
|
|
503
|
+
): { uncovered: string[]; unverifiable: string[] } {
|
|
504
|
+
const emitted = new Set(selected.flatMap((g) => g.mediaTypes ?? []))
|
|
505
|
+
const undeclared = selected.some((g) => (g.mediaTypes ?? []).length === 0)
|
|
506
|
+
const uncovered: string[] = []
|
|
507
|
+
const unverifiable: string[] = []
|
|
508
|
+
for (const mediaType of required) {
|
|
509
|
+
if (emitted.has(mediaType)) continue
|
|
510
|
+
if (undeclared) unverifiable.push(mediaType)
|
|
511
|
+
else uncovered.push(mediaType)
|
|
512
|
+
}
|
|
513
|
+
return { uncovered, unverifiable }
|
|
367
514
|
}
|
|
368
515
|
|
|
369
516
|
/**
|
|
@@ -397,7 +544,11 @@ export function binaryOutputPickIssues(
|
|
|
397
544
|
// that registers no integrations cannot satisfy a step that selects one. So a call site that
|
|
398
545
|
// omits this FLAGS a selection rather than passing it — the loud direction — and the default
|
|
399
546
|
// stays a legitimate value rather than a hole.
|
|
400
|
-
generators: readonly Pick<RegisteredBinaryGenerator, 'id' | 'modalities'>[] = [],
|
|
547
|
+
generators: readonly Pick<RegisteredBinaryGenerator, 'id' | 'modalities' | 'mediaTypes'>[] = [],
|
|
548
|
+
// Whether the deployment's integrations could not be READ. Defaulted to `false` — the honest
|
|
549
|
+
// default, since every deployment but a mothership-mode node reads them in-process and cannot
|
|
550
|
+
// fail — so an omitting call site judges the list it was given rather than claiming an outage.
|
|
551
|
+
generatorsUnavailable = false,
|
|
401
552
|
): BinaryOutputPickState {
|
|
402
553
|
const resolved = available === true
|
|
403
554
|
const issues: BinaryOutputPickIssue[] = []
|
|
@@ -405,7 +556,7 @@ export function binaryOutputPickIssues(
|
|
|
405
556
|
// resolve against different registries and a step missing its storage pick routinely has a
|
|
406
557
|
// generative fault too. Reporting them one round at a time is exactly the fix-and-retry cycle
|
|
407
558
|
// this function returns every issue to avoid.
|
|
408
|
-
const generative = generatorPickIssues(config, generators)
|
|
559
|
+
const generative = generatorPickIssues(config, generators, generatorsUnavailable)
|
|
409
560
|
const noStorageService =
|
|
410
561
|
resolved && !catalog.some((s) => s.capabilities.includes(ASSET_STORAGE_CAPABILITY))
|
|
411
562
|
if (available === false) issues.push('catalog_unavailable')
|
|
@@ -419,6 +570,8 @@ export function binaryOutputPickIssues(
|
|
|
419
570
|
unknownContextIds: [],
|
|
420
571
|
unknownGeneratorIds: generative.unknownGeneratorIds,
|
|
421
572
|
uncoveredModalities: generative.uncovered,
|
|
573
|
+
uncoveredMediaTypes: generative.uncoveredMediaTypes,
|
|
574
|
+
unverifiableMediaTypes: generative.unverifiableMediaTypes,
|
|
422
575
|
}
|
|
423
576
|
}
|
|
424
577
|
|
|
@@ -440,5 +593,7 @@ export function binaryOutputPickIssues(
|
|
|
440
593
|
unknownContextIds,
|
|
441
594
|
unknownGeneratorIds: generative.unknownGeneratorIds,
|
|
442
595
|
uncoveredModalities: generative.uncovered,
|
|
596
|
+
uncoveredMediaTypes: generative.uncoveredMediaTypes,
|
|
597
|
+
unverifiableMediaTypes: generative.unverifiableMediaTypes,
|
|
443
598
|
}
|
|
444
599
|
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -3744,16 +3744,25 @@
|
|
|
3744
3744
|
"binaryOutputContextMissing": "Diese Kontextdienste sind nicht mehr im Katalog: {ids}",
|
|
3745
3745
|
"binaryOutputUnavailable": "Der Katalog der grundlegenden Dienste ist nicht erreichbar, deshalb lässt sich hier noch nichts wählen.",
|
|
3746
3746
|
"binaryOutputGenerators": "Erzeugen mit",
|
|
3747
|
+
"binaryOutputGeneratorsUnavailable": "Die generativen Integrationen dieser Installation konnten nicht gelesen werden, daher lässt sich hier noch nichts auswählen. Das ist ein Verbindungsproblem, keine fehlende Registrierung.",
|
|
3747
3748
|
"binaryOutputGeneratorsPlaceholder": "Keine Integration ausgewählt",
|
|
3748
3749
|
"binaryOutputModalities": "Muss liefern",
|
|
3749
3750
|
"binaryOutputModalitiesPlaceholder": "Keine Anforderung",
|
|
3751
|
+
"binaryOutputMediaTypes": "Exakte Formate",
|
|
3752
|
+
"binaryOutputMediaTypesPlaceholder": "Keine Formatanforderung",
|
|
3753
|
+
"binaryOutputDeclaredFormats": "Ausgewählte Integrationen geben an: {formats}",
|
|
3754
|
+
"binaryOutputMediaTypeUncovered": "Keine ausgewählte Integration erzeugt {formats}, was dieser Schritt liefern soll. Wähle eine, die das kann, oder entferne die Anforderung.",
|
|
3755
|
+
"binaryOutputMediaTypeUnverifiable": "Keine ausgewählte Integration gibt {formats} an, aber eine von ihnen gibt überhaupt keine Formate an, daher konnte das nicht geprüft werden. Der Schritt startet trotzdem; prüfe in der API der Integration, ob sie das erzeugen kann.",
|
|
3756
|
+
"binaryOutputMediaTypeUnusable": "Nicht gespeichert, denn dies sind keine Medientypen: {entries}. Schreibe jeden als type/subtype.",
|
|
3750
3757
|
"binaryOutputGeneratorMissing": "Diese Installation registriert diese generativen Integrationen nicht: {ids}. Sie werden im Code der Installation registriert, nicht in diesem Workspace.",
|
|
3751
3758
|
"binaryOutputModalityUncovered": "Keine ausgewählte Integration erzeugt {modalities}, was dieser Schritt liefern soll.",
|
|
3759
|
+
"binaryOutputModalityRetired": "{modality} (nicht mehr verfügbar — wähle die Inhaltstypen dieses Schritts neu)",
|
|
3752
3760
|
"binaryOutputModality": {
|
|
3753
3761
|
"image": "Bilder",
|
|
3754
3762
|
"audio": "Audio",
|
|
3755
3763
|
"video": "Video",
|
|
3756
|
-
"3d": "3D-Modelle",
|
|
3764
|
+
"3d-model": "3D-Modelle",
|
|
3765
|
+
"3d-scene": "3D-Szenen",
|
|
3757
3766
|
"document": "Dokumente"
|
|
3758
3767
|
}
|
|
3759
3768
|
},
|
|
@@ -4411,6 +4420,7 @@
|
|
|
4411
4420
|
"target": "Speicherdienst",
|
|
4412
4421
|
"targetNone": "Für diesen Schritt nicht erfasst",
|
|
4413
4422
|
"contextServices": "Kontext der Erzeugung",
|
|
4423
|
+
"mediaTypes": "Geforderte Formate",
|
|
4414
4424
|
"misdirectedBadge": "Anderer Dienst",
|
|
4415
4425
|
"unknownBadge": "Nicht im Katalog",
|
|
4416
4426
|
"storedCount": "{outcome}, 1 Artefakt | {outcome}, {count} Artefakte",
|
|
@@ -4442,10 +4452,12 @@
|
|
|
4442
4452
|
"warning": {
|
|
4443
4453
|
"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.",
|
|
4444
4454
|
"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.",
|
|
4455
|
+
"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.",
|
|
4445
4456
|
"misdirected": "1 Artefakt ging an einen anderen Dienst als {target}. | {count} Artefakte gingen an einen anderen Dienst als {target}.",
|
|
4446
4457
|
"invalidEntries": "1 angegebener Eintrag wurde verworfen: er nannte weder Dienst noch Ablageort. | {count} angegebene Einträge wurden verworfen: sie nannten weder Dienst noch Ablageort.",
|
|
4447
4458
|
"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.",
|
|
4448
|
-
"unknownGenerators": "Eine generative Integration genannt, die diese Installation nicht registriert: {ids}. Der Eintrag bleibt wie angegeben erhalten; die Integration wird im Code der Installation registriert, nicht in diesem Workspace. | Generative Integrationen genannt, die diese Installation nicht registriert: {ids}. Ihre Einträge bleiben wie angegeben erhalten; Integrationen werden im Code der Installation registriert, nicht in diesem Workspace."
|
|
4459
|
+
"unknownGenerators": "Eine generative Integration genannt, die diese Installation nicht registriert: {ids}. Der Eintrag bleibt wie angegeben erhalten; die Integration wird im Code der Installation registriert, nicht in diesem Workspace. | Generative Integrationen genannt, die diese Installation nicht registriert: {ids}. Ihre Einträge bleiben wie angegeben erhalten; Integrationen werden im Code der Installation registriert, nicht in diesem Workspace.",
|
|
4460
|
+
"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."
|
|
4449
4461
|
},
|
|
4450
4462
|
"unknownGeneratorBadge": "Nicht registriert"
|
|
4451
4463
|
},
|
|
@@ -4909,6 +4921,12 @@
|
|
|
4909
4921
|
"unexpected": "Der Server hat eine unerwartete Antwort zurückgegeben. Versuche es erneut und gib die Details an den Betreiber des Deployments weiter, wenn es weiterhin auftritt."
|
|
4910
4922
|
}
|
|
4911
4923
|
},
|
|
4924
|
+
"unavailable": {
|
|
4925
|
+
"description": {
|
|
4926
|
+
"binary_generators_unreachable": "Die generativen Integrationen dieser Installation konnten gerade nicht gelesen werden, deshalb wurde der Lauf nicht gestartet. Es ist nichts falsch konfiguriert und keine Änderung nötig: versuchen Sie es erneut, sobald die Verbindung wieder steht.",
|
|
4927
|
+
"foundational_builtins_unreachable": "Die integrierten Basisdienste dieser Installation konnten gerade nicht gelesen werden. Es ist nichts falsch konfiguriert und keine Änderung nötig: versuchen Sie es erneut, sobald die Verbindung wieder steht."
|
|
4928
|
+
}
|
|
4929
|
+
},
|
|
4912
4930
|
"action": {
|
|
4913
4931
|
"retryFailed": "Wiederholung fehlgeschlagen",
|
|
4914
4932
|
"startFailed": "Start fehlgeschlagen",
|
package/i18n/locales/en.json
CHANGED
|
@@ -548,6 +548,12 @@
|
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
550
|
},
|
|
551
|
+
"unavailable": {
|
|
552
|
+
"description": {
|
|
553
|
+
"binary_generators_unreachable": "This deployment's generative integrations could not be read just now, so the run was not started. Nothing is misconfigured and no change is needed: try again once the connection recovers.",
|
|
554
|
+
"foundational_builtins_unreachable": "This deployment's built-in foundational services could not be read just now. Nothing is misconfigured and no change is needed: try again once the connection recovers."
|
|
555
|
+
}
|
|
556
|
+
},
|
|
551
557
|
"action": {
|
|
552
558
|
"retryFailed": "Retry failed",
|
|
553
559
|
"startFailed": "Failed to start",
|
|
@@ -4223,16 +4229,26 @@
|
|
|
4223
4229
|
"binaryOutputContextMissing": "These context services are no longer in the catalog: {ids}",
|
|
4224
4230
|
"binaryOutputUnavailable": "The foundational services catalog is unreachable, so nothing can be picked here yet.",
|
|
4225
4231
|
"binaryOutputGenerators": "Generate with",
|
|
4232
|
+
"binaryOutputGeneratorsUnavailable": "This deployment's generative integrations could not be read, so nothing can be picked here yet. This is a connection problem, not a missing registration.",
|
|
4226
4233
|
"binaryOutputGeneratorsPlaceholder": "No integration selected",
|
|
4227
4234
|
"binaryOutputModalities": "Must deliver",
|
|
4228
4235
|
"binaryOutputModalitiesPlaceholder": "No requirement",
|
|
4236
|
+
"binaryOutputMediaTypes": "Exact formats",
|
|
4237
|
+
"binaryOutputMediaTypesPlaceholder": "No format requirement",
|
|
4238
|
+
"binaryOutputDeclaredFormats": "Selected integrations declare: {formats}",
|
|
4239
|
+
"binaryOutputMediaTypeUncovered": "No selected integration emits {formats}, which this step is set to deliver. Pick one that does, or drop the requirement.",
|
|
4240
|
+
"binaryOutputMediaTypeUnverifiable": "No selected integration declares {formats}, but one of them declares no formats at all, so this could not be checked. The step still starts; confirm from the integration's API that it can emit this.",
|
|
4241
|
+
"binaryOutputMediaTypeUnusable": "Not saved, because these are not media types: {entries}. Write each one as type/subtype.",
|
|
4229
4242
|
"binaryOutputGeneratorMissing": "This deployment does not register these generative integrations: {ids}. They are registered in the deployment's code, not in this workspace.",
|
|
4230
4243
|
"binaryOutputModalityUncovered": "No selected integration produces {modalities}, which this step is set to deliver.",
|
|
4244
|
+
"binaryOutputModalityRetired": "{modality} (no longer offered — re-pick this step's content types)",
|
|
4245
|
+
"@binaryOutputModalityRetired": "A content type saved on a step that this build no longer defines. {modality} is a raw machine value (e.g. 3d) and must not be translated. Appears inside the binaryOutputModalityUncovered sentence, so keep it a short noun phrase, not a sentence.",
|
|
4231
4246
|
"binaryOutputModality": {
|
|
4232
4247
|
"image": "Images",
|
|
4233
4248
|
"audio": "Audio",
|
|
4234
4249
|
"video": "Video",
|
|
4235
|
-
"3d": "3D models",
|
|
4250
|
+
"3d-model": "3D models",
|
|
4251
|
+
"3d-scene": "3D scenes",
|
|
4236
4252
|
"document": "Documents"
|
|
4237
4253
|
}
|
|
4238
4254
|
},
|
|
@@ -5623,6 +5639,7 @@
|
|
|
5623
5639
|
"target": "Storage service",
|
|
5624
5640
|
"targetNone": "None recorded on this step",
|
|
5625
5641
|
"contextServices": "Generation context",
|
|
5642
|
+
"mediaTypes": "Required formats",
|
|
5626
5643
|
"misdirectedBadge": "Other service",
|
|
5627
5644
|
"unknownBadge": "Not in catalog",
|
|
5628
5645
|
"storedCount": "{outcome}, 1 artifact | {outcome}, {count} artifacts",
|
|
@@ -5654,10 +5671,12 @@
|
|
|
5654
5671
|
"warning": {
|
|
5655
5672
|
"unknownServices": "Named a service the catalog does not contain: {ids}. The entry is kept as claimed; check the id against the workspace catalog. | Named services the catalog does not contain: {ids}. Their entries are kept as claimed; check the ids against the workspace catalog.",
|
|
5656
5673
|
"targetUnknown": "The catalog no longer contains this step's own storage service ({id}), so nothing below could be checked against it. Register it again, or point the step at another service.",
|
|
5674
|
+
"undeliveredMediaTypes": "This step was set to deliver {formats}, and no artifact below reports that format. | This step was set to deliver {formats}, and no artifact below reports those formats.",
|
|
5657
5675
|
"misdirected": "1 artifact went to a service other than {target}. | {count} artifacts went to a service other than {target}.",
|
|
5658
5676
|
"invalidEntries": "1 declared entry was dropped: it named no service and location. | {count} declared entries were dropped: they named no service and location.",
|
|
5659
5677
|
"omitted": "1 more artifact was declared beyond the report's limit and is not listed. | {count} more artifacts were declared beyond the report's limit and are not listed.",
|
|
5660
|
-
"unknownGenerators": "Named a generative integration this deployment does not register: {ids}. The entry is kept as claimed; the integration is registered in the deployment's code, not in this workspace. | Named generative integrations this deployment does not register: {ids}. Their entries are kept as claimed; integrations are registered in the deployment's code, not in this workspace."
|
|
5678
|
+
"unknownGenerators": "Named a generative integration this deployment does not register: {ids}. The entry is kept as claimed; the integration is registered in the deployment's code, not in this workspace. | Named generative integrations this deployment does not register: {ids}. Their entries are kept as claimed; integrations are registered in the deployment's code, not in this workspace.",
|
|
5679
|
+
"generatorsUnverified": "This deployment's generative integrations could not be read when the step settled, so the integrations named below were not checked against them. The entries are kept as claimed."
|
|
5661
5680
|
},
|
|
5662
5681
|
"unknownGeneratorBadge": "Not registered"
|
|
5663
5682
|
},
|