@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.
@@ -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
- * It needs no `available` tri-state, unlike the catalog half: the integrations ride the workspace
346
- * SNAPSHOT rather than their own probe, so there is no "not read yet" state distinct from the
347
- * board not having loaded if the caller has a snapshot at all, this list is the whole truth. An
348
- * empty list is therefore a real EMPTY (this deployment registers none), which is exactly why a
349
- * selected id in that state is `unknown_generator` and not silence.
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
- ): { issues: BinaryOutputPickIssue[]; unknownGeneratorIds: string[]; uncovered: BinaryModality[] } {
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 covered = new Set(selectedIds.flatMap((id) => byId.get(id)?.modalities ?? []))
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
- return { issues, unknownGeneratorIds, uncovered }
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
  }
@@ -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",
@@ -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
  },