editorbrain 0.12.0 → 0.18.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.
@@ -200,22 +200,703 @@ function checkStyle(files, id) {
200
200
  return finish2(documents);
201
201
  }
202
202
 
203
+ // packages/core/src/editorial-delivery.ts
204
+ var editorialDeliveryOptions = {
205
+ narration: ["original", "voiceover", "none"],
206
+ onScreenText: ["minimal", "key-points", "detailed"],
207
+ broll: ["footage", "stills", "mixed", "none"],
208
+ captionLanguage: ["source", "brand"],
209
+ captionTreatment: ["clean", "word-highlight", "karaoke", "boxed"],
210
+ captionPosition: ["lower", "center", "adaptive"],
211
+ ctaGoal: ["adaptive", "follow", "comment", "visit", "contact", "none"],
212
+ ctaTreatment: ["adaptive", "end-card", "overlay", "spoken"]
213
+ };
214
+ function editorialDeliveryIssues(value) {
215
+ if (value === void 0) return [];
216
+ if (!value || typeof value !== "object" || Array.isArray(value)) return ["editorialDelivery must be an object."];
217
+ const input = value;
218
+ if (Object.keys(input).some((key) => !Object.prototype.hasOwnProperty.call(editorialDeliveryOptions, key))) return [`editorialDelivery supports ${Object.keys(editorialDeliveryOptions).join(", ")} only.`];
219
+ return Object.entries(editorialDeliveryOptions).flatMap(([key, options]) => input[key] === void 0 || typeof input[key] === "string" && options.includes(input[key]) ? [] : [`Invalid editorialDelivery ${key}. Omit undecided choices.`]);
220
+ }
221
+ function resolveEditorialDelivery(value = {}, captionsEnabled) {
222
+ const issues = editorialDeliveryIssues(value);
223
+ if (issues.length) throw new Error(issues.join("\n"));
224
+ const active = Object.fromEntries(Object.entries(value).filter(([, choice]) => choice !== void 0));
225
+ if (captionsEnabled === false) {
226
+ delete active.captionLanguage;
227
+ delete active.captionTreatment;
228
+ delete active.captionPosition;
229
+ }
230
+ if (active.ctaGoal === "none") delete active.ctaTreatment;
231
+ return active;
232
+ }
233
+
234
+ // packages/core/src/media-styles.ts
235
+ var sourceByStyle = {
236
+ "editorial-photo": { title: "Adobe: Editorial photography", url: "https://www.adobe.com/creativecloud/photography/type/editorial-photography.html" },
237
+ "studio-product": { title: "Adobe: Product photography", url: "https://www.adobe.com/creativecloud/photography/type/product-photography.html" },
238
+ "paper-collage": { title: "Adobe: Collage composition", url: "https://www.adobe.com/products/photoshop/collage.html" },
239
+ "ink-diagram": { title: "Adobe: Line art", url: "https://www.adobe.com/creativecloud/illustration/discover/line-art.html" },
240
+ "soft-3d": { title: "Blender: Rendering", url: "https://www.blender.org/features/rendering/" },
241
+ "flat-vector": { title: "Adobe: Illustration", url: "https://www.adobe.com/creativecloud/illustration.html" }
242
+ };
243
+ var brandAdaptation = "Los tokens de marca gobiernan colores y tipograf\xEDas de los gr\xE1ficos. Conserva los colores aut\xE9nticos del material, producto y logos; no heredes la paleta de la muestra.";
244
+ var expansionSources = {
245
+ "cinematic-scene": [
246
+ { title: "Adobe: Cinematic lighting and depth", url: "https://helpx.adobe.com/ee/photoshop/how-to/standout-photos-lighting-vignette.html" },
247
+ { title: "Referencia visual: luis_.hermida \xB7 5.0 s", url: "https://www.instagram.com/p/DY0ZmQevH2J/" }
248
+ ],
249
+ "surreal-metaphor": [
250
+ { title: "Adobe: Surreal photography", url: "https://www.adobe.com/uk/creativecloud/photography/discover/surreal-photography.html" },
251
+ { title: "Referencia visual: russ.lockedin \xB7 19.0 y 28.5 s", url: "https://www.instagram.com/p/DbtWJOztrTO/" }
252
+ ],
253
+ "painterly-story": [
254
+ { title: "Adobe: Visual appearance of oil painting", url: "https://helpx.adobe.com/in/photoshop/using/oil-paint-filter.html" },
255
+ { title: "Referencia visual: russ.lockedin \xB7 14.5 y 22.5 s", url: "https://www.instagram.com/p/DbWkY9ZSXtQ/" }
256
+ ],
257
+ "vintage-engraving": [
258
+ { title: "Adobe: Line, hatching and volume", url: "https://www.adobe.com/creativecloud/illustration/discover/line-art.html" },
259
+ { title: "Referencia visual: russ.lockedin \xB7 17.5 s", url: "https://www.instagram.com/p/DbWkY9ZSXtQ/" }
260
+ ],
261
+ "riso-playful": [
262
+ { title: "RISO: Creative print, color and texture", url: "https://www.riso.co.uk/print-solutions/risograph-creative/" },
263
+ { title: "Referencia de ilustraci\xF3n na\xEFf: ryanbailey.cb \xB7 11.5 s", url: "https://www.instagram.com/p/DbZDnSpyilk/" }
264
+ ],
265
+ "sensory-macro": [
266
+ { title: "Adobe: Macro photography", url: "https://www.adobe.com/creativecloud/photography/discover/macro-photography.html" },
267
+ { title: "Referencia visual: ryanbailey.cb \xB7 15.5 y 24.5 s", url: "https://www.instagram.com/p/DbZDnSpyilk/" }
268
+ ]
269
+ };
270
+ var image = (id, name, description, purpose, recipe, parameters, requirements, avoidWhen) => ({
271
+ id,
272
+ version: "1.0.0",
273
+ category: "image",
274
+ name,
275
+ description,
276
+ purpose,
277
+ recipe,
278
+ parameters,
279
+ adaptationRules: [brandAdaptation, "Adapta sujeto, encuadre y espacio para texto al contenido real y al formato. Usa recursos autorizados; una preferencia permite proponer, no autoriza generaci\xF3n ni compras."],
280
+ requirements,
281
+ avoidWhen,
282
+ status: "concept",
283
+ sources: [sourceByStyle[id]],
284
+ preview: { kind: "illustration", description: "Muestras originales generadas con IA del tratamiento visual; no acreditan exactitud factual, un render de cliente ni una transferencia probada. Vector limpio es una apariencia raster, no un archivo SVG editable.", examples: ["square", "portrait", "landscape"].map((format, index) => ({ ratio: ["1:1", "9:16", "16:9"][index], url: `https://editorbrain.com/media-styles/v1/${id}-${format}.webp`, provenance: "ai-generated" })) }
285
+ });
286
+ var expandedImage = (...args) => {
287
+ const style = image(...args);
288
+ return {
289
+ ...style,
290
+ sources: expansionSources[style.id],
291
+ adaptationRules: [...style.adaptationRules, "Distingue hechos, reconstrucci\xF3n e imagen metaf\xF3rica: el tratamiento no acredita un evento, resultado o propiedad real. Los par\xE1metros son decisiones editoriales adaptables, no mediciones del referente."],
292
+ preview: {
293
+ kind: "illustration",
294
+ description: "Escenas originales generadas con IA, inspiradas en tratamientos observados. Son muestras de apariencia, no copias del referente, documentos hist\xF3ricos ni prueba de t\xE9cnica f\xEDsica, movimiento o sonido.",
295
+ examples: ["square", "portrait", "landscape"].map((format, index) => ({ ratio: ["1:1", "9:16", "16:9"][index], url: `https://editorbrain.com/media-styles/v2/${style.id}-${format}.webp`, provenance: "ai-generated" }))
296
+ }
297
+ };
298
+ };
299
+ var motion = (id, name, description, purpose, recipe, parameters, requirements, avoidWhen) => ({
300
+ id,
301
+ version: "1.0.0",
302
+ category: "motion",
303
+ name,
304
+ description,
305
+ purpose,
306
+ recipe,
307
+ parameters,
308
+ adaptationRules: [brandAdaptation, "Los tiempos son puntos de partida: ajusta a voz, acci\xF3n, lectura, resoluci\xF3n y fps reales. Con movimiento reducido, usa cortes o estados est\xE1ticos legibles. No deduzcas ni a\xF1adas SFX por esta animaci\xF3n."],
309
+ requirements,
310
+ avoidWhen,
311
+ status: "concept",
312
+ sources: [{ title: "Adobe: Six principles of animation in UX", url: "https://blog.adobe.com/en/publish/2019/06/19/designing-animation-six-principles-using-animation-ux" }],
313
+ preview: { kind: "animation", description: "Demostraci\xF3n del mecanismo temporal; no es una edici\xF3n de cliente ni prueba de calidad de transferencia." }
314
+ });
315
+ var composition = (args, source, formats) => {
316
+ const style = motion(...args);
317
+ return {
318
+ ...style,
319
+ sources: [source],
320
+ adaptationRules: [
321
+ ...style.adaptationRules,
322
+ "Los par\xE1metros propuestos son decisiones de adaptaci\xF3n, no tiempos ni movimientos medidos del referente. Conserva restricciones de recursos, formato de b-roll y permisos; elegir este dise\xF1o no autoriza generaci\xF3n ni gasto.",
323
+ ...formats,
324
+ "Texto, cifras y anotaciones exactas permanecen editables en c\xF3digo. Una imagen generada no prueba un hecho; verifica continuidad, recortes y legibilidad con los recursos reales antes del render."
325
+ ],
326
+ preview: { kind: "animation", description: "Composici\xF3n original con recursos ilustrativos y capas de c\xF3digo. La muestra demuestra una propuesta de organizaci\xF3n y secuencia; no reproduce al referente ni certifica hechos, sonido, herramientas disponibles o transferencia al material del cliente." }
327
+ };
328
+ };
329
+ var MEDIA_STYLE_CATALOG = {
330
+ schemaVersion: 1,
331
+ id: "editorbrain-media-styles",
332
+ version: "1.4.0",
333
+ styles: [
334
+ image(
335
+ "editorial-photo",
336
+ "Fotograf\xEDa editorial",
337
+ "Luz natural, un sujeto claro y contexto real.",
338
+ "Aportar contexto humano y credibilidad sin distraer del mensaje.",
339
+ ["Define una acci\xF3n o sujeto concreto relacionado con el beat.", "Comp\xF3n un plano principal con profundidad suave y fondo contextual.", "Reserva espacio libre solo donde habr\xE1 texto; conserva textura y tonos naturales."],
340
+ [{ key: "subject_share", value: [35, 65], unit: "% del encuadre", adaptation: "Ajusta al objeto y al recorte final; no cortes manos o informaci\xF3n relevante." }, { key: "light", value: "Una fuente amplia lateral, sombras suaves", adaptation: "Respeta la luz existente si usas fotograf\xEDa real." }],
341
+ ["Fotograf\xEDa propia/licenciada o herramienta de generaci\xF3n autorizada.", "Sujeto y contexto definidos por el proyecto."],
342
+ ["Se requiere mostrar con exactitud un evento o persona y solo hay material sint\xE9tico.", "La escena necesita demostrar datos o relaciones abstractas."]
343
+ ),
344
+ image(
345
+ "studio-product",
346
+ "Producto de estudio",
347
+ "Un producto protagonista con separaci\xF3n limpia del fondo.",
348
+ "Mostrar forma, material y detalles relevantes de un producto.",
349
+ ["Parte del producto real y conserva envase, etiquetas y proporciones.", "Usa una base simple y luz suave que separe los contornos.", "A\xF1ade como m\xE1ximo un detalle contextual que ayude a explicar el beneficio."],
350
+ [{ key: "product_share", value: [45, 75], unit: "% de altura", adaptation: "Deja margen para el recorte y la informaci\xF3n del producto." }, { key: "background", value: "Superficie simple con sombra de contacto", adaptation: "Usa roles de marca; no inventes materiales o claims." }],
351
+ ["Imagen precisa del producto y permisos de uso.", "Recorte o separaci\xF3n de sujeto si se cambia el fondo."],
352
+ ["No hay referencia fiable del producto.", "El acabado estilizado oculta caracter\xEDsticas importantes."]
353
+ ),
354
+ image(
355
+ "paper-collage",
356
+ "Collage de papel",
357
+ "Recortes, capas y textura discreta con jerarqu\xEDa editorial.",
358
+ "Relacionar ideas o fuentes distintas mediante una composici\xF3n tangible.",
359
+ ["Elige un elemento principal y hasta dos apoyos.", "Recorta siluetas y combina bordes rectos con un detalle de papel.", "Separa capas con sombra sutil; mantiene el texto fuera de las zonas con textura."],
360
+ [{ key: "support_layers", value: [1, 3], unit: "capas", adaptation: "Reduce las capas si el formato o el texto pierden legibilidad." }, { key: "rotation", value: [-4, 4], unit: "grados", adaptation: "No gires documentos o etiquetas cuya lectura sea esencial." }],
361
+ ["Im\xE1genes recortables con derechos y suficiente resoluci\xF3n.", "Capas editables para composici\xF3n."],
362
+ ["Hay muchos datos densos que deben leerse.", "La marca necesita una presentaci\xF3n fotogr\xE1fica sin intervenci\xF3n."]
363
+ ),
364
+ image(
365
+ "ink-diagram",
366
+ "Diagrama de tinta",
367
+ "Trazos claros, etiquetas breves y relaciones visibles.",
368
+ "Explicar un proceso, contraste o relaci\xF3n que la fotograf\xEDa no muestra.",
369
+ ["Reduce el concepto a nodos y relaciones verificables.", "Dibuja trazos consistentes y flechas solo con direcci\xF3n significativa.", "Usa etiquetas breves; diferencia el acento sem\xE1ntico del trazo principal."],
370
+ [{ key: "nodes", value: [2, 5], unit: "nodos por estado", adaptation: "Divide un sistema mayor en varios estados." }, { key: "stroke", value: [2, 4], unit: "px a 1080 de ancho", adaptation: "Escala con la salida y comprueba visibilidad en m\xF3vil." }],
371
+ ["Contenido y relaciones confirmados por el guion.", "Texto editable y motor de dibujo o vector."],
372
+ ["La relaci\xF3n a\xFAn es una hip\xF3tesis no etiquetada.", "La simplificaci\xF3n elimina informaci\xF3n necesaria."]
373
+ ),
374
+ image(
375
+ "soft-3d",
376
+ "Volumen suave",
377
+ "Objetos simples con profundidad, luz amplia y materiales mates.",
378
+ "Dar forma a conceptos abstractos mediante met\xE1foras visuales sencillas.",
379
+ ["Elige una met\xE1fora concreta y comprensible para el concepto.", "Modela pocas formas con bordes suaves y materiales mates.", "Usa una c\xE1mara estable, sombra de contacto y contraste legible."],
380
+ [{ key: "primary_objects", value: [1, 3], unit: "objetos", adaptation: "Prioriza una relaci\xF3n clara frente a decoraci\xF3n." }, { key: "material", value: "Mate, rugosidad media-alta", adaptation: "Si el objeto representa un producto real, respeta su material." }],
381
+ ["Motor 3D o herramienta de generaci\xF3n autorizada.", "Met\xE1fora que no confunda con evidencia de un producto real."],
382
+ ["Se necesita exactitud documental.", "La met\xE1fora introduce una promesa o caracter\xEDstica no confirmada."]
383
+ ),
384
+ image(
385
+ "flat-vector",
386
+ "Vector limpio",
387
+ "Formas planas, contornos precisos y contraste sem\xE1ntico.",
388
+ "Explicar categor\xEDas o pasos con gr\xE1ficos claros y reutilizables.",
389
+ ["Construye siluetas simples y consistentes.", "Limita cada panel a una acci\xF3n y un foco visual.", "Usa rellenos sem\xE1nticos de marca y texto editable separado."],
390
+ [{ key: "visual_roles", value: [2, 4], unit: "roles por panel", adaptation: "Los roles se asignan a tokens de marca; no fijan colores nuevos." }, { key: "detail", value: "Sin textura ni microdetalle decorativo", adaptation: "Conserva detalles que distinguen funciones o categor\xEDas." }],
391
+ ["Motor vectorial o composici\xF3n con formas.", "Etiquetas y conceptos definidos."],
392
+ ["La fotograf\xEDa del objeto real es necesaria para reconocerlo.", "Las diferencias importantes dependen de textura o acabado."]
393
+ ),
394
+ expandedImage(
395
+ "cinematic-scene",
396
+ "Cine atmosf\xE9rico",
397
+ "Luz dirigida, profundidad y una escena que concentra la emoci\xF3n.",
398
+ "Dar contexto emocional a una acci\xF3n o decisi\xF3n del guion.",
399
+ ["Define una acci\xF3n legible y el estado emocional que debe acompa\xF1ar.", "Comp\xF3n sujeto y entorno con una fuente de luz coherente y separaci\xF3n de planos.", "Conserva detalle en las sombras relevantes; ajusta el encuadre a cada formato sin copiar personajes del referente."],
400
+ [{ key: "focal_actions", value: 1, unit: "acci\xF3n principal", adaptation: "Puede ser un gesto, una espera o un descubrimiento; debe corresponder al beat." }, { key: "light", value: "Una fuente dominante y relleno seg\xFAn legibilidad", adaptation: "El contraste responde a la escena; no impongas \xE1mbar ni una LUT a material aut\xE9ntico." }, { key: "depth", value: "Sujeto, contexto y fondo diferenciados", adaptation: "Mant\xE9n enfocada la informaci\xF3n necesaria para comprender la acci\xF3n." }],
401
+ ["Acci\xF3n y contexto definidos; recursos propios o autorizados.", "Referencia fiel cuando se represente una persona o producto real."],
402
+ ["El dramatismo cambia el sentido de un testimonio.", "Una escena inventada podr\xEDa confundirse con evidencia del evento descrito."]
403
+ ),
404
+ expandedImage(
405
+ "surreal-metaphor",
406
+ "Met\xE1fora surrealista",
407
+ "Una relaci\xF3n imposible convierte una idea abstracta en una escena legible.",
408
+ "Representar tensi\xF3n, escala o cambio mediante una met\xE1fora vinculada al guion.",
409
+ ["Reduce el concepto a una relaci\xF3n visual comprensible.", "Introduce una sola contradicci\xF3n de escala, material o espacio en una escena coherente.", "Mant\xE9n luz y perspectiva consistentes; distingue la met\xE1fora de una afirmaci\xF3n factual."],
410
+ [{ key: "visual_contradictions", value: 1, unit: "relaci\xF3n central", adaptation: "Omite s\xEDmbolos adicionales que necesiten explicaci\xF3n o cambien el sentido." }, { key: "scale_anchor", value: "Un objeto o sujeto de escala reconocible", adaptation: "El ancla debe ayudar a entender la relaci\xF3n imposible sin obligar a incluir una persona." }, { key: "scene_continuity", value: "Luz y perspectiva compartidas", adaptation: "La imposibilidad es conceptual; evita errores de composici\xF3n que parezcan accidentales." }],
411
+ ["Concepto claro y met\xE1fora adecuada al p\xFAblico.", "Composici\xF3n o generaci\xF3n autorizada de una escena nueva."],
412
+ ["La imagen puede sugerir una capacidad real o resultado no demostrado.", "El contenido requiere mostrar hechos con exactitud documental."]
413
+ ),
414
+ expandedImage(
415
+ "painterly-story",
416
+ "Pintura narrativa",
417
+ "Pincelada visible, masas de color y una historia contenida en la escena.",
418
+ "Acompa\xF1ar relatos, recuerdos o ideas con una interpretaci\xF3n pict\xF3rica.",
419
+ ["Elige un momento y las relaciones entre sujeto y entorno.", "Construye luz y volumen con masas de color y bordes de distinta definici\xF3n.", "Reserva el detalle para el foco narrativo y deja pincelada visible sin convertirla en ruido."],
420
+ [{ key: "focal_groups", value: [1, 3], unit: "grupos visuales", adaptation: "Reduce personajes y objetos si se pierde la historia en m\xF3vil." }, { key: "brushwork", value: "Visible en formas y transiciones de luz", adaptation: "La escala de pincel responde a la salida; no ocultes rasgos relevantes de un objeto real." }, { key: "edge_hierarchy", value: "Foco definido y bordes secundarios m\xE1s suaves", adaptation: "Mant\xE9n reconocible aquello que sostiene la acci\xF3n." }],
421
+ ["Escena original o materiales cuyo uso est\xE9 autorizado.", "Una funci\xF3n narrativa que se beneficie de interpretaci\xF3n visual."],
422
+ ["Se necesita probar un detalle hist\xF3rico, m\xE9dico o t\xE9cnico.", "La estilizaci\xF3n cambia identidad o caracter\xEDsticas esenciales."]
423
+ ),
424
+ expandedImage(
425
+ "vintage-engraving",
426
+ "Grabado editorial",
427
+ "Rayado, contornos y papel aparente con volumen de estampa hist\xF3rica.",
428
+ "Dar car\xE1cter y profundidad a una escena u objeto ilustrado.",
429
+ ["Comp\xF3n una escena u objeto con silueta reconocible.", "Construye luces y sombras mediante l\xEDneas y rayado que sigan el volumen.", "Aplica una paleta corta y textura discreta; mant\xE9n etiquetas y datos verificables separados."],
430
+ [{ key: "ink_roles", value: [1, 3], unit: "roles de tinta aparentes", adaptation: "Asigna los roles a la marca; el aspecto hist\xF3rico no obliga a usar sepia." }, { key: "hatching", value: "Densidad mayor en sombra y menor en luz", adaptation: "Ampl\xEDa separaci\xF3n de l\xEDneas para evitar empastes o moir\xE9 en la salida final." }, { key: "paper_texture", value: "Secundaria respecto a contorno y volumen", adaptation: "Reduce textura donde compita con informaci\xF3n necesaria." }],
431
+ ["Ilustraci\xF3n original o recurso autorizado con resoluci\xF3n suficiente.", "Verificaci\xF3n independiente si se atribuye una identidad o hecho hist\xF3rico."],
432
+ ["La imagen sint\xE9tica podr\xEDa presentarse como documento de archivo aut\xE9ntico.", "El rayado oculta detalles que deben leerse con precisi\xF3n."]
433
+ ),
434
+ expandedImage(
435
+ "riso-playful",
436
+ "Estampa artesanal",
437
+ "Formas expresivas, pocas tintas aparentes y textura de impresi\xF3n.",
438
+ "Aportar calidez y personalidad a una ilustraci\xF3n clara.",
439
+ ["Reduce la escena a siluetas expresivas con un foco principal.", "Asigna pocos roles de color y construye cada forma con bordes ligeramente imperfectos.", "A\xF1ade textura de tinta y, si ayuda, un desregistro sutil; comprueba que el contorno conserve lectura."],
440
+ [{ key: "ink_roles", value: [2, 4], unit: "roles de color", adaptation: "Usa tokens compatibles con la marca y suficiente contraste; no heredes la paleta de la muestra." }, { key: "registration", value: "Ligera irregularidad opcional en bordes secundarios", adaptation: "No desplazar letras, logos o detalles que necesitan precisi\xF3n." }, { key: "texture", value: "Grano irregular visible sin cubrir la forma", adaptation: "La textura debe sobrevivir la compresi\xF3n sin convertir \xE1reas planas en ruido." }],
441
+ ["Ilustraci\xF3n propia o autorizada y jerarqu\xEDa visual definida.", "Texto y logos exactos en capas independientes si se necesitan."],
442
+ ["Se requiere una reproducci\xF3n fiel del acabado de un producto.", "El tono o la lectura exigen geometr\xEDa y contornos completamente precisos."]
443
+ ),
444
+ expandedImage(
445
+ "sensory-macro",
446
+ "Detalle sensorial",
447
+ "Materia, luz pr\xF3xima y texturas que ocupan el encuadre.",
448
+ "Hacer visible una cualidad material relevante mediante un detalle cercano.",
449
+ ["Elige una textura real o una materia relacionada con el beat.", "Acerca el encuadre y dirige la luz para revelar relieve, gotas, fibras o superficies.", "Mant\xE9n suficiente profundidad de foco para reconocer el detalle y recomp\xF3n para cada proporci\xF3n."],
450
+ [{ key: "material_share", value: [65, 95], unit: "% del encuadre", adaptation: "Deja contexto cuando haga falta reconocer la materia; no recortes un rasgo esencial." }, { key: "focus", value: "Plano de textura principal legible", adaptation: "La profundidad debe revelar la cualidad buscada, no disimular errores." }, { key: "light", value: "Rasante o especular seg\xFAn la superficie", adaptation: "Conserva el color y acabado aut\xE9nticos si la imagen representa un producto real." }],
451
+ ["Materia o detalle definido y recursos propios o autorizados.", "Referencia exacta cuando se anuncie una propiedad del producto."],
452
+ ["El detalle exagera calidad, composici\xF3n o resultados no demostrados.", "El encuadre impide identificar el objeto que el guion necesita mostrar."]
453
+ ),
454
+ motion(
455
+ "quiet-reveal",
456
+ "Entrada sutil",
457
+ "Aparici\xF3n breve con desplazamiento corto y lectura estable.",
458
+ "Introducir informaci\xF3n sin competir con la voz o el sujeto.",
459
+ ["Alinea la aparici\xF3n con la frase que presenta la informaci\xF3n.", "Combina opacidad de 0 a 1 con un desplazamiento corto.", "Mant\xE9n el estado final el tiempo que requiere la lectura; retira cuando deja de ser \xFAtil."],
460
+ [{ key: "entry_duration", value: [180, 320], unit: "ms", adaptation: "Ajusta a fps y velocidad de lectura." }, { key: "travel", value: [1, 3], unit: "% de altura", adaptation: "Reduce a cero cuando el sujeto o el fondo ya se mueven." }, { key: "easing", value: "ease-out sin rebote", adaptation: "La llegada debe estabilizarse antes de leer." }],
461
+ ["Texto o gr\xE1fico en capa independiente.", "Ancla temporal en voz o acci\xF3n."],
462
+ ["La informaci\xF3n ya est\xE1 visible y no necesita \xE9nfasis.", "La entrada retrasa una instrucci\xF3n urgente."]
463
+ ),
464
+ motion(
465
+ "kinetic-type",
466
+ "Tipograf\xEDa r\xEDtmica",
467
+ "\xC9nfasis de palabras con ritmo ligado al habla.",
468
+ "Destacar una frase clave sin convertir todas las palabras en est\xEDmulos.",
469
+ ["Selecciona una frase y las pocas palabras que realmente cambian su sentido.", "Sincroniza sus estados con la voz verificada.", "Usa un \xFAnico mecanismo de \xE9nfasis y conserva contexto legible."],
470
+ [{ key: "emphasis_scale", value: [1.04, 1.12], unit: "factor", adaptation: "Mant\xE9n la palabra dentro del \xE1rea segura." }, { key: "emphasis_duration", value: [120, 240], unit: "ms", adaptation: "Ajusta al fonema/frase real; no inventes alineaci\xF3n desde un resumen ASR." }, { key: "emphasis_count", value: [1, 3], unit: "palabras por frase", adaptation: "Puede ser cero si el \xE9nfasis no aporta sentido." }],
471
+ ["Texto confirmado y fuente disponible.", "Alineaci\xF3n temporal real de la frase o revisi\xF3n manual."],
472
+ ["El texto contiene demasiadas palabras para leer.", "El \xE9nfasis cambia el sentido de una cita o declaraci\xF3n."]
473
+ ),
474
+ motion(
475
+ "spring-callout",
476
+ "\xC9nfasis el\xE1stico",
477
+ "Una etiqueta llega con un rebote corto y controlado.",
478
+ "Se\xF1alar un detalle puntual de forma expresiva.",
479
+ ["Ancla la etiqueta al detalle que se est\xE1 explicando.", "Lleva escala y posici\xF3n al destino con un \xFAnico sobrepaso leve.", "Estabiliza la etiqueta; no mantengas vibraci\xF3n durante la lectura."],
480
+ [{ key: "overshoot", value: [2, 5], unit: "%", adaptation: "Reduce el rebote en marcas sobrias o contenido sensible." }, { key: "settle_duration", value: [250, 450], unit: "ms", adaptation: "Verifica el movimiento con playback al fps final." }],
481
+ ["Elemento o punto que justifica el \xE9nfasis.", "Motor con keyframes o resorte controlable."],
482
+ ["Compite con una acci\xF3n importante.", "El tono requiere calma o la audiencia necesita movimiento reducido."]
483
+ ),
484
+ motion(
485
+ "diagram-build",
486
+ "Diagrama progresivo",
487
+ "Las relaciones aparecen en el orden de la explicaci\xF3n.",
488
+ "Permitir entender un sistema una relaci\xF3n a la vez.",
489
+ ["Establece el primer nodo como estado legible.", "Revela cada relaci\xF3n despu\xE9s de mencionar su causa o funci\xF3n.", "Mant\xE9n el conjunto al final para que pueda revisarse."],
490
+ [{ key: "segment_reveal", value: [250, 500], unit: "ms", adaptation: "Usa voz o lectura como ancla; evita un ritmo autom\xE1tico uniforme." }, { key: "active_relation", value: 1, unit: "relaci\xF3n a la vez", adaptation: "Agrupa solo relaciones que se explican conjuntamente." }],
491
+ ["Diagrama v\xE1lido con nodos y trazos separados.", "Orden de explicaci\xF3n definido."],
492
+ ["La progresi\xF3n implicar\xEDa causalidad no demostrada.", "No hay tiempo suficiente para leer el estado final."]
493
+ ),
494
+ motion(
495
+ "match-cut",
496
+ "Corte por correspondencia",
497
+ "Dos planos se conectan por forma, posici\xF3n o acci\xF3n.",
498
+ "Dar continuidad a un cambio de contexto o reforzar una comparaci\xF3n.",
499
+ ["Encuentra una correspondencia real entre dos planos autorizados.", "Alinea el foco y el momento de acci\xF3n sin deformar el sujeto.", "Prueba primero un corte directo; comprueba ambos lados a velocidad real."],
500
+ [{ key: "transition_overlap", value: 0, unit: "frames de partida", adaptation: "A\xF1ade solape solo si tiene funci\xF3n editorial; no hace falta fundido." }, { key: "alignment", value: "Punto de atenci\xF3n, forma o direcci\xF3n de acci\xF3n", adaptation: "Elige una correspondencia; no fuerces todas a la vez." }],
501
+ ["Dos planos con correspondencia verificable.", "Material suficiente alrededor del punto de corte."],
502
+ ["La comparaci\xF3n inventa continuidad factual.", "Los recortes necesarios da\xF1an el contenido."]
503
+ ),
504
+ motion(
505
+ "parallax-depth",
506
+ "Profundidad por capas",
507
+ "Desplazamiento suave entre planos de una imagen.",
508
+ "A\xF1adir recorrido visual a una imagen est\xE1tica cuando aporta atenci\xF3n.",
509
+ ["Separa sujeto y fondo con bordes limpios.", "Extiende el fondo solo donde sea necesario y est\xE9 autorizado.", "Mueve las capas con velocidades ligeramente distintas sin revelar huecos."],
510
+ [{ key: "layer_travel", value: [1, 3], unit: "% de ancho", adaptation: "Reduce desplazamiento seg\xFAn recorte, bordes y \xE1rea segura." }, { key: "shot_duration", value: [2, 5], unit: "s de partida", adaptation: "Depende del beat y la lectura; una imagen puede permanecer inm\xF3vil." }],
511
+ ["Imagen con resoluci\xF3n y capas separables.", "Fondo disponible o reconstrucci\xF3n autorizada."],
512
+ ["Se deforman rostros, logos o detalles del producto.", "El movimiento crea una perspectiva imposible o distrae."]
513
+ ),
514
+ motion(
515
+ "workflow-scene",
516
+ "Flujo de trabajo",
517
+ "Una escena completa conecta solicitud, agente y resultado.",
518
+ "Explicar c\xF3mo avanza una tarea entre actores y estados mediante una secuencia visible.",
519
+ ["Define entradas, responsables, decisiones y resultados que correspondan al proceso real.", "Comp\xF3n la escena en capas editables de JavaScript, HTML/SVG o el motor disponible; separa nodos, conexiones y etiquetas.", "Representa una solicitud recorriendo el flujo; cambia cada estado cuando la narraci\xF3n explica su funci\xF3n.", "Sost\xE9n el resultado y comprueba que el recorrido sea legible en el formato final; una simulaci\xF3n no demuestra una automatizaci\xF3n ejecutada."],
520
+ [{ key: "visible_stages", value: [3, 5], unit: "estados por escena", adaptation: "Divide procesos mayores y conserva bifurcaciones necesarias; no inventes que todo flujo termina en un correo." }, { key: "state_hold", value: [800, 1800], unit: "ms de partida", adaptation: "Ajusta al contenido, voz, lectura y fps; el ritmo de la demo no mide la duraci\xF3n real del proceso." }, { key: "active_transition", value: 1, unit: "paso de atenci\xF3n", adaptation: "Permite ramas paralelas solo cuando el guion las explica; mant\xE9n etiquetas y valores exactos." }],
521
+ ["Proceso verificado y etiquetas editables.", "Herramienta de composici\xF3n y render disponible en la sesi\xF3n; el cat\xE1logo no instala ni acredita un motor."],
522
+ ["La simplificaci\xF3n inventa capacidades o resultados de un sistema.", "El proceso exige detalles que no se pueden leer en la duraci\xF3n disponible."]
523
+ ),
524
+ motion(
525
+ "product-assembly",
526
+ "Ensamble de producto",
527
+ "Un producto se separa en piezas y vuelve a formar una unidad.",
528
+ "Explicar construcci\xF3n, relaci\xF3n entre partes o un detalle de dise\xF1o en una escena continua.",
529
+ ["Identifica las piezas visibles con material del producto real o define un objeto ficticio claramente ilustrativo.", "Prepara capas 2D o geometr\xEDa 3D seg\xFAn los recursos y el motor de JavaScript disponible.", "Separa piezas manteniendo sus relaciones espaciales y re\xFAne el conjunto con un orden comprensible.", "Revisa encajes, oclusiones, r\xF3tulos y estado final; no presentes un despiece imaginado como ingenier\xEDa real."],
530
+ [{ key: "visible_parts", value: [3, 6], unit: "piezas principales", adaptation: "Incluye solo piezas justificadas por la explicaci\xF3n y evidencia; agrupa el resto." }, { key: "separation", value: [5, 16], unit: "% del encuadre", adaptation: "Adapta distancias al producto y evita que las partes crucen \xE1reas seguras o pierdan su relaci\xF3n." }, { key: "settle_duration", value: [500, 900], unit: "ms de partida", adaptation: "Ajusta al tama\xF1o y al relato; comprueba la lectura del conjunto a fps final." }],
531
+ ["Piezas autorizadas o modelo original y referencias del producto cuando se afirmen caracter\xEDsticas reales.", "Motor local verificado para las transformaciones 2D/3D elegidas."],
532
+ ["No se conoce la construcci\xF3n real y el despiece podr\xEDa inducir a error.", "El movimiento de piezas oculta medidas, etiquetas o detalles necesarios."]
533
+ ),
534
+ motion(
535
+ "interface-story",
536
+ "Interfaz en acci\xF3n",
537
+ "Una miniapp muestra una acci\xF3n y el cambio de estado que produce.",
538
+ "Explicar una interacci\xF3n mediante una interfaz breve con texto y estados controlables.",
539
+ ["Define la tarea, el estado inicial y el resultado verificable; distingue demo conceptual de captura real.", "Construye una interfaz propia con texto, componentes y estados editables en JavaScript.", "Anima una acci\xF3n clara y su respuesta, manteniendo continuidad espacial entre ambos estados.", "Deja leer el resultado y verifica que la transici\xF3n no invente velocidad, m\xE9tricas o funciones del producto."],
540
+ [{ key: "primary_actions", value: 1, unit: "acci\xF3n por escena", adaptation: "Divide tareas m\xE1s largas en escenas; evita automatismos que el producto no tiene." }, { key: "state_transition", value: [250, 600], unit: "ms de partida", adaptation: "Ajusta a legibilidad y al ritmo de la explicaci\xF3n; no representa latencia real." }, { key: "result_hold", value: [1, 3], unit: "s de partida", adaptation: "Aumenta tiempo o reduce contenido cuando el estado final requiera lectura." }],
541
+ ["Flujo de interacci\xF3n definido y contenido exacto revisado.", "Componentes y motor de composici\xF3n disponibles; uso autorizado de una marca si la UI la representa."],
542
+ ["La simulaci\xF3n se puede confundir con prueba real de funcionamiento o rendimiento.", "La simplificaci\xF3n altera el significado de una acci\xF3n importante."]
543
+ ),
544
+ composition(
545
+ [
546
+ "editorial-atlas",
547
+ "\xCDndice editorial",
548
+ "Una colecci\xF3n organizada se abre para descubrir un detalle.",
549
+ "Presentar una colecci\xF3n y mantener su \xEDndice visible mientras se destaca un elemento.",
550
+ ["Define los elementos reales de la colecci\xF3n y una clasificaci\xF3n que ayude al relato.", "Comp\xF3n tarjetas en una ret\xEDcula editable de JavaScript con n\xFAmeros y t\xEDtulos separados de sus im\xE1genes.", "Revela grupos conservando posiciones; abre una tarjeta protagonista mientras el \xEDndice mantiene la orientaci\xF3n.", "Sost\xE9n el detalle y regresa al conjunto solo si ayuda a comparar; no repitas im\xE1genes para aparentar una colecci\xF3n mayor."],
551
+ [{ key: "visible_cards", value: [3, 9], unit: "tarjetas por estado", adaptation: "Limita el grupo a recursos distintos disponibles; pagina o agrupa si las etiquetas no se leen." }, { key: "active_card", value: 1, unit: "foco principal", adaptation: "Mant\xE9n las otras tarjetas como contexto, sin animar todos los focos a la vez." }, { key: "detail_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta a voz y lectura; no representa un tiempo medido del referente." }],
552
+ ["Colecci\xF3n de im\xE1genes o clips autorizados, t\xEDtulos confirmados y criterio de agrupaci\xF3n.", "Motor de composici\xF3n disponible para ret\xEDcula, m\xE1scaras y texto editable."],
553
+ ["La ret\xEDcula vuelve ilegible la informaci\xF3n esencial.", "La colecci\xF3n o sus categor\xEDas sugieren variedad, ranking o resultados no acreditados."]
554
+ ],
555
+ { title: "Referencia de composici\xF3n: orate.x", url: "https://www.instagram.com/p/DaQCU16scia/" },
556
+ ["1:1: organiza una ret\xEDcula equilibrada y reserva una franja estable para el \xEDndice.", "9:16: usa una o dos columnas, agrupa por estados y ampl\xEDa el foco sin miniaturizar toda la colecci\xF3n.", "16:9: separa \xEDndice y detalle en zonas laterales; redistribuye tarjetas sin estirar im\xE1genes."]
557
+ ),
558
+ composition(
559
+ [
560
+ "object-headline",
561
+ "Objeto protagonista",
562
+ "Un objeto atraviesa un titular y organiza una portada editorial.",
563
+ "Dar protagonismo a un objeto manteniendo su identidad y una jerarqu\xEDa tipogr\xE1fica clara.",
564
+ ["Parte de una imagen fiel del objeto o identifica claramente un objeto ficticio ilustrativo.", "Separa el sujeto con alfa limpio y comp\xF3n el titular detr\xE1s, conservando las letras necesarias para comprenderlo.", "A\xF1ade pocas fichas editables en JavaScript; cambia el \xE9nfasis entre titular, silueta y detalle sin inventar un despiece o giro 3D.", "Comprueba bordes, sombra y estado de lectura; si no hay recorte fiable, utiliza la imagen \xEDntegra dentro de un marco deliberado."],
565
+ [{ key: "subject_height", value: [45, 75], unit: "% del encuadre", adaptation: "Respeta proporciones, detalles y \xE1rea segura del producto; no lo estires para llenar la portada." }, { key: "headline_lines", value: [1, 3], unit: "l\xEDneas", adaptation: "Recomp\xF3n con la fuente de marca y evita oclusiones que cambien el significado." }, { key: "support_labels", value: [0, 3], unit: "fichas", adaptation: "Incluye \xFAnicamente datos confirmados; omite decoraci\xF3n que parezca una especificaci\xF3n." }],
566
+ ["Imagen autorizada del objeto y recorte limpio o alternativa de encuadre completo.", "Titular confirmado, fuente disponible y capas editables."],
567
+ ["La oclusi\xF3n oculta caracter\xEDsticas relevantes o impide leer el titular.", "La puesta en escena sugiere propiedades, fabricante o resultados no confirmados."]
568
+ ],
569
+ { title: "Referencia de composici\xF3n: francogalvanads", url: "https://www.instagram.com/p/Ddb0qB1ArrV/" },
570
+ ["1:1: centra la relaci\xF3n entre silueta y titular; coloca fichas en m\xE1rgenes despejados.", "9:16: reparte el titular en altura y lleva las fichas debajo del objeto, conservando la silueta completa.", "16:9: ampl\xEDa la jerarqu\xEDa horizontal con columnas laterales; no agrandes el objeto a costa de cortar sus detalles."]
571
+ ),
572
+ composition(
573
+ [
574
+ "visual-correspondence",
575
+ "De idea a imagen",
576
+ "Dos representaciones conservan su lugar para poder compararse.",
577
+ "Explicar una relaci\xF3n entre un sujeto y su interpretaci\xF3n sin perder la referencia original.",
578
+ ["Selecciona dos representaciones del mismo sujeto y declara qu\xE9 relaci\xF3n est\xE1 realmente sustentada.", "Mant\xE9n visible la fuente mientras introduces la interpretaci\xF3n en un panel separado y de escala comparable.", "Comp\xF3n marcos, un detalle activo y anotaciones exactas en JavaScript; las conexiones describen lo visible, no una equivalencia t\xE9cnica imaginada.", "Sost\xE9n ambos paneles para comparar; distingue estudio visual, propuesta y documentaci\xF3n real."],
579
+ [{ key: "representations", value: 2, unit: "paneles", adaptation: "No sustituir la fuente durante la comparaci\xF3n; separa un tercer argumento en otro estado." }, { key: "active_detail", value: 1, unit: "correspondencia por estado", adaptation: "Apunta a un detalle comprobable en ambos recursos, sin convertir similitud en causalidad." }, { key: "comparison_hold", value: [2, 4], unit: "s de partida", adaptation: "Extiende la lectura o reduce anotaciones seg\xFAn voz y resoluci\xF3n." }],
580
+ ["Dos recursos autorizados con correspondencia revisada y escala comprensible.", "Anotaciones verificables en capas editables; motor disponible para la comparaci\xF3n."],
581
+ ["La interpretaci\xF3n se presenta como plano, diagn\xF3stico o mejora t\xE9cnicamente demostrada sin evidencia.", "Los recursos no corresponden al mismo sujeto y el montaje oculta esa diferencia."]
582
+ ],
583
+ { title: "Referencia de composici\xF3n: valen.bece", url: "https://www.instagram.com/p/DcuJnb6JUUK/" },
584
+ ["1:1: usa paneles equilibrados y una franja de anotaciones que no tape la correspondencia.", "9:16: apila origen e interpretaci\xF3n manteniendo ambos visibles; acorta las notas antes de reducir el sujeto.", "16:9: coloca los paneles lado a lado y reserva el centro o margen inferior para una relaci\xF3n por estado."]
585
+ ),
586
+ composition(
587
+ [
588
+ "context-triptych",
589
+ "Tres perspectivas",
590
+ "Tres ventanas conectan un tema desde contextos complementarios.",
591
+ "Mostrar perspectivas relacionadas de forma simult\xE1nea, con un foco que gu\xEDa la lectura.",
592
+ ["Elige tres contextos complementarios y confirma qu\xE9 v\xEDnculo puede afirmar el guion.", "Comp\xF3n tres ventanas con m\xE1scaras y r\xF3tulos independientes; respeta el formato de apoyo elegido y los recursos autorizados.", "Si hay clips, revisa sus acciones y da protagonismo a una ventana sin deformar las otras; las entradas y cambios de foco son decisiones nuevas de edici\xF3n.", "Deja leer el conjunto sin afirmar continuidad de producci\xF3n, sincron\xEDa o causalidad que las fuentes no demuestren."],
593
+ [{ key: "windows", value: 3, unit: "perspectivas", adaptation: "Cada ventana debe aportar informaci\xF3n distinta; evita duplicar un clip para llenar la composici\xF3n." }, { key: "active_window", value: 1, unit: "foco de lectura", adaptation: "Los otros paneles pueden permanecer en reposo; no es necesario reproducir tres acciones intensas simult\xE1neamente." }, { key: "focus_hold", value: [1, 3], unit: "s de partida", adaptation: "Ancla el foco a voz o acci\xF3n revisada y preserva tiempo para comprender el conjunto." }],
594
+ ["Tres recursos autorizados con relaci\xF3n editorial expl\xEDcita; clips reales disponibles cuando se requiera footage.", "Motor disponible para m\xE1scaras, recortes y control independiente de reproducci\xF3n."],
595
+ ["La simultaneidad inventa una relaci\xF3n causal o un proceso real no documentado.", "Los tres paneles ocultan acciones, rostros o detalles necesarios por falta de espacio."]
596
+ ],
597
+ { title: "Referencia de composici\xF3n: metromedia.house", url: "https://www.instagram.com/p/Dasq8-6tbt9/" },
598
+ ["1:1: distribuye tres ventanas con un foco mayor y dos apoyos; conserva lectura en cada recorte.", "9:16: apila tres franjas o un foco y dos apoyos; ajusta cada encuadre al sujeto sin estirar los clips.", "16:9: usa tres columnas legibles; ampliar la activa redistribuye las m\xE1scaras y conserva las proporciones del material."]
599
+ ),
600
+ composition(
601
+ [
602
+ "process-filmstrip",
603
+ "Historia de un proceso",
604
+ "Una tarjeta de la secuencia se convierte en la escena principal.",
605
+ "Recorrer fases verificadas de una historia conservando una referencia de progreso.",
606
+ ["Define las fases y su orden; utiliza nombres de etapa cuando no existan fechas confirmadas.", "Comp\xF3n un riel de tarjetas y una marca de fase en JavaScript, separados del material visual.", "Desplaza el foco hacia una tarjeta y ampl\xEDa su marco hasta la escena; conserva una ancla de progreso para orientar al espectador.", "Sost\xE9n la etapa y contin\xFAa solo cuando el relato lo justifique; identifica los resultados conceptuales y no los presentes como transformaci\xF3n documentada."],
607
+ [{ key: "stages", value: [3, 5], unit: "etapas por secuencia", adaptation: "El orden procede del guion y la evidencia, no de una cronolog\xEDa inventada." }, { key: "active_frame", value: 1, unit: "tarjeta protagonista", adaptation: "La expansi\xF3n cambia el encuadre, no las proporciones ni el contenido del recurso." }, { key: "stage_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta a la explicaci\xF3n y deja leer el nombre de cada fase." }],
608
+ ["Recursos autorizados por etapa, orden confirmado y distinci\xF3n entre estado real y propuesta.", "Motor de composici\xF3n disponible para riel, m\xE1scara y r\xF3tulos editables."],
609
+ ["La secuencia inventa fechas o presenta im\xE1genes sint\xE9ticas como evidencia de un resultado real.", "El encuadre expandido pierde el detalle que explica la etapa."]
610
+ ],
611
+ { title: "Referencia de composici\xF3n: metromedia.house", url: "https://www.instagram.com/p/Dasq8-6tbt9/" },
612
+ ["1:1: sit\xFAa el riel junto al borde inferior y reserva el centro para expandir la tarjeta activa.", "9:16: reorganiza el riel en columna o una franja breve; muestra las etapas pr\xF3ximas sin miniaturizar toda la historia.", "16:9: recorre las tarjetas horizontalmente y ampl\xEDa la activa dentro del \xE1rea segura, manteniendo un indicador de fase."]
613
+ ),
614
+ composition(
615
+ [
616
+ "tactile-notes",
617
+ "Mesa de ideas",
618
+ "Una superficie estable re\xFAne notas y detalles por estados.",
619
+ "Construir una idea mediante un espacio de trabajo tangible con continuidad visual.",
620
+ ["Define una superficie estable y los elementos que realmente forman parte de la explicaci\xF3n.", "Prepara el fondo y las capas independientes; si solo hay una foto plana, limita la animaci\xF3n a nuevas notas y trazos de c\xF3digo.", "Introduce fichas, anotaciones y marcas por estados con posiciones y escala consistentes, sin simular movimiento independiente de objetos incrustados.", "Sost\xE9n la idea completa; describe el tratamiento como composici\xF3n por estados salvo que una captura stop motion est\xE9 corroborada."],
621
+ [{ key: "note_layers", value: [2, 5], unit: "capas editables", adaptation: "Reduce las notas si compiten con el sujeto o requieren letra peque\xF1a." }, { key: "surface", value: "Encuadre y referencias espaciales estables", adaptation: "Conserva perspectiva, sombras y escala coherentes; no impongas madera o papel si contradicen el contenido." }, { key: "state_hold", value: [800, 1800], unit: "ms de partida", adaptation: "Decide la cadencia seg\xFAn lectura y voz; no es una medici\xF3n del referente ni prueba de captura cuadro a cuadro." }],
622
+ ["Superficie y recursos propios o autorizados; capas separadas para cualquier elemento que cambie de posici\xF3n.", "Texto confirmado, fuente disponible y motor de composici\xF3n para notas y estados."],
623
+ ["La animaci\xF3n fingir\xEDa que un objeto de una imagen plana puede moverse por separado.", "Las texturas, notas o capas ocultan informaci\xF3n esencial o simulan documentos aut\xE9nticos."]
624
+ ],
625
+ { title: "Referencia de composici\xF3n: anatta.ha", url: "https://www.instagram.com/p/DYV-3gEty9u/" },
626
+ ["1:1: agrupa notas alrededor de un foco estable y deja reposar el estado completo.", "9:16: recorre la idea de arriba abajo con pocas notas por estado; recomp\xF3n la mesa sin encoger toda la escena.", "16:9: organiza zonas de material y anotaciones con una secuencia clara; conserva escala y perspectiva al redistribuir capas."]
627
+ ),
628
+ composition(
629
+ [
630
+ "subject-depth",
631
+ "Sujeto en primer plano",
632
+ "Una persona conserva protagonismo mientras el t\xEDtulo ocupa un plano detr\xE1s.",
633
+ "Dar jerarqu\xEDa humana al mensaje mediante profundidad y oclusi\xF3n controladas.",
634
+ ["Parte de un retrato o clip autorizado y un titular breve que corresponda al mensaje.", "Separa sujeto, t\xEDtulo y fondo; al usar v\xEDdeo, revisa la m\xE1scara durante toda la acci\xF3n, incluidos pelo, manos y desenfoque.", "Ubica el t\xEDtulo detr\xE1s del sujeto con suficiente texto libre para reconocerlo; anima las capas editables sin reconstruir rasgos ni inventar movimiento del cuerpo.", "Sost\xE9n una lectura clara y reduce la oclusi\xF3n o usa un panel adyacente si el recorte pierde calidad."],
635
+ [{ key: "depth_planes", value: 3, unit: "planos", adaptation: "Fondo, t\xEDtulo y sujeto; no a\xF1ade perspectiva o geometr\xEDa real que no existe en la fuente." }, { key: "headline_words", value: [2, 6], unit: "palabras", adaptation: "Adapta tama\xF1o y saltos a la marca y evita tapar las letras que distinguen el mensaje." }, { key: "subject_share", value: [40, 70], unit: "% de altura", adaptation: "Conserva rostro, gestos y zonas seguras; la separaci\xF3n depende del recurso real." }],
636
+ ["Retrato propio o autorizado y m\xE1scara fiable por frame si hay movimiento.", "Motor disponible para capas, m\xE1scara y texto editable con las fuentes de marca."],
637
+ ["La oclusi\xF3n impide leer el t\xEDtulo o una m\xE1scara deficiente altera la identidad.", "El recorte elimina una acci\xF3n esencial, atribuye palabras al sujeto o modifica un testimonio."]
638
+ ],
639
+ { title: "Referencia de oclusi\xF3n entre sujeto y titular", url: "https://www.instagram.com/p/DZJQm-MgeFL/" },
640
+ ["1:1: centra el retrato y reparte el titular en dos l\xEDneas detr\xE1s de los hombros; conserva letras de anclaje visibles.", "9:16: apila titular y figura en profundidad, con rostro libre y m\xE1rgenes para subt\xEDtulos; no ampl\xEDes el cuerpo para llenar vac\xEDos.", "16:9: desplaza el sujeto a un tercio y prolonga el t\xEDtulo detr\xE1s con un bloque legible lateral; recomp\xF3n la m\xE1scara sin estirar el retrato."]
641
+ ),
642
+ composition(
643
+ [
644
+ "route-journal",
645
+ "Diario de ruta",
646
+ "Un lugar y un recorrido esquem\xE1tico se conectan con notas breves.",
647
+ "Orientar una historia espacial sin confundir un mapa editorial con navegaci\xF3n precisa.",
648
+ ["Confirma lugares, orden y relaci\xF3n del recorrido; distingue coordenadas verificadas de una ruta conceptual.", "Combina material autorizado del lugar con un trazado editable y puntos de parada; la referencia apoya el contexto de lugar, no demuestra un mapa animado.", "Revela el recorrido por etapas con un marcador y una nota activa, sin inventar calles, distancias, tiempos ni geolocalizaci\xF3n.", "Mant\xE9n el lugar visible al mostrar el esquema; rotula como ilustrativo cualquier trazado no geogr\xE1fico y revisa permisos de mapas utilizados."],
649
+ [{ key: "stops", value: [2, 4], unit: "paradas", adaptation: "El orden viene del relato; menos puntos favorecen orientaci\xF3n y lectura en m\xF3vil." }, { key: "active_note", value: 1, unit: "nota por estado", adaptation: "Solo incluye nombres y datos confirmados; no deduce ubicaci\xF3n a partir de una imagen sint\xE9tica." }, { key: "route_hold", value: [1, 3], unit: "s de partida", adaptation: "La duraci\xF3n permite leer, no representa tiempo de viaje ni velocidad real." }],
650
+ ["Lugares y recorrido confirmados, o identificaci\xF3n expl\xEDcita del esquema como ilustrativo.", "Material de lugar autorizado y motor disponible para trazo, marcador y etiquetas; mapa licenciado si se requiere precisi\xF3n."],
651
+ ["La escena necesita navegaci\xF3n o distancias exactas y no hay datos fiables.", "Revela ubicaciones privadas o presenta un entorno generado como registro de un viaje real."]
652
+ ],
653
+ { title: "Apoyo visual de lugar; la ruta es una adaptaci\xF3n original", url: "https://www.instagram.com/p/Dasq8-6tbt9/" },
654
+ ["1:1: reserva un panel al lugar y otro al esquema con una nota activa; evita un mapa lleno de etiquetas.", "9:16: coloca el lugar arriba y el recorrido esquem\xE1tico debajo, conectados por la misma etiqueta; redistribuye los nodos verticalmente sin alterar una geograf\xEDa real.", "16:9: combina una ventana amplia del lugar con el esquema lateral y notas cortas; conserva la escala de los mapas reales y usa r\xF3tulos externos."]
655
+ ),
656
+ composition(
657
+ [
658
+ "focus-reveal",
659
+ "Del detalle al conjunto",
660
+ "Un detalle abre el encuadre hasta mostrar su contexto.",
661
+ "Conectar una textura o funci\xF3n con el objeto al que realmente pertenece.",
662
+ ["Elige un recurso autorizado de suficiente resoluci\xF3n que contenga tanto el detalle como el objeto completo.", "Define un recorte inicial y un encuadre final del mismo recurso; los planos macro y contextuales de la referencia apoyan la escala, no corroboran un revelado continuo del mismo objeto.", "Ampl\xEDa la ventana de encuadre o reduce el acercamiento sin generar partes ocultas; si son tomas distintas, usa un corte expl\xEDcito y verifica identidad y continuidad.", "Sost\xE9n el conjunto con una etiqueta editable que explique el detalle sin a\xF1adir propiedades no demostradas."],
663
+ [{ key: "start_crop", value: [25, 45], unit: "% del ancho de la fuente", adaptation: "Limita el acercamiento por resoluci\xF3n real y por la informaci\xF3n que se debe reconocer." }, { key: "reveal_duration", value: [800, 1600], unit: "ms de partida", adaptation: "Ajusta a narraci\xF3n y movimiento ya presentes; con movimiento reducido muestra detalle y conjunto en estados." }, { key: "end_context", value: "Objeto reconocible y margen contextual", adaptation: "No exige revelar una cara posterior o entorno inexistente en el material." }],
664
+ ["Mismo objeto y detalle verificados, con resoluci\xF3n suficiente o clip continuo revisado.", "Motor disponible para recorte y escala; cualquier reconstrucci\xF3n exige autorizaci\xF3n y identificaci\xF3n aparte."],
665
+ ["El detalle y el conjunto pertenecen a objetos distintos o la ampliaci\xF3n inventa partes.", "El reencuadre pierde nitidez, contradice el material real o oculta una advertencia del producto."]
666
+ ],
667
+ { title: "Apoyo de escala macro y contextual; revelado continuo propuesto", url: "https://www.instagram.com/p/DbZDnSpyilk/" },
668
+ ["1:1: abre desde un detalle desplazado al centro hacia el objeto completo, conservando un \xE1rea breve para la etiqueta.", "9:16: usa un recorrido de encuadre vertical solo si la fuente lo permite; termina con objeto y explicaci\xF3n apilados sin recortar su funci\xF3n.", "16:9: expande el recorte hacia un plano contextual y sit\xFAa la etiqueta en el espacio libre lateral; no estires una foto vertical."]
669
+ ),
670
+ composition(
671
+ [
672
+ "transformation-wipe",
673
+ "Cambio en paralelo",
674
+ "Dos estados alineados comparten un encuadre y un barrido de comparaci\xF3n.",
675
+ "Hacer visible una diferencia verificable manteniendo constantes las referencias espaciales.",
676
+ ["Confirma qu\xE9 comparan los dos estados y alinea encuadre, escala y puntos reconocibles.", "Usa etiquetas antes/despu\xE9s solo si existe evidencia temporal; para variantes o demos utiliza propuesta A/B.", "Mueve una m\xE1scara sobre im\xE1genes alineadas o separa paneles si la alineaci\xF3n no es fiable; el barrido es una adaptaci\xF3n nueva de una comparaci\xF3n observada, no un efecto medido.", "Sost\xE9n ambos estados con etiquetas legibles y conserva diferencias aut\xE9nticas sin retocar selectivamente para exagerar un resultado."],
677
+ [{ key: "states", value: 2, unit: "estados comparables", adaptation: "La relaci\xF3n debe estar documentada o identificada como variante conceptual." }, { key: "wipe_range", value: [10, 90], unit: "% del encuadre", adaptation: "Deja contexto para identificar ambos estados; usa corte o paneles est\xE1ticos con movimiento reducido." }, { key: "comparison_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta al detalle relevante y no deduzcas eficacia, duraci\xF3n del proceso ni causalidad." }],
678
+ ["Dos recursos autorizados comparables con etiquetas y relaci\xF3n confirmadas.", "Motor disponible para m\xE1scaras y alineaci\xF3n; m\xE9todo de paneles cuando cambien punto de vista o geometr\xEDa."],
679
+ ["La comparaci\xF3n mezcla sujetos, \xE1ngulos o condiciones que enga\xF1an sobre el cambio.", "Los recursos sint\xE9ticos se presentan como prueba de mejora, tratamiento o fabricaci\xF3n real."]
680
+ ],
681
+ { title: "Referencia de comparaci\xF3n alineada; barrido propuesto", url: "https://www.instagram.com/p/Dcggcv6xgAn/" },
682
+ ["1:1: alinea ambos estados en un marco com\xFAn y ubica etiquetas en bordes opuestos sin tapar la diferencia.", "9:16: usa estados apilados o un barrido vertical cuando conserve puntos de referencia; elige paneles si el recorte oculta contexto.", "16:9: compara en un lienzo compartido con barrido horizontal o dos paneles equivalentes; no deformes una versi\xF3n para hacerla coincidir."]
683
+ ),
684
+ composition(
685
+ [
686
+ "evidence-spotlight",
687
+ "Detalle con evidencia",
688
+ "Un recorte destacado conecta una fuente con una anotaci\xF3n verificable.",
689
+ "Dirigir la atenci\xF3n hacia el detalle que respalda una afirmaci\xF3n concreta.",
690
+ ["Selecciona la fuente autorizada y confirma qu\xE9 pasaje o detalle sostiene la afirmaci\xF3n; registra procedencia sin exponer informaci\xF3n privada.", "Mant\xE9n una vista contextual y un recorte ampliado del mismo recurso, unidos por una llamada editable.", "Marca una regi\xF3n con flecha, subrayado o contorno sin alterar su contenido; los datos y etiquetas se componen en c\xF3digo y se verifican contra la fuente.", "Deja tiempo para leer y distingue evidencia documental de una muestra ficticia; la anotaci\xF3n no convierte una imagen generada en prueba."],
691
+ [{ key: "callouts", value: [1, 2], unit: "llamadas por estado", adaptation: "Cada llamada debe se\xF1alar un dato concreto; divide pasajes largos en estados sucesivos." }, { key: "detail_share", value: [35, 60], unit: "% del panel", adaptation: "La ampliaci\xF3n depende de resoluci\xF3n y legibilidad real, no de OCR supuesto." }, { key: "reading_hold", value: [2, 4], unit: "s de partida", adaptation: "Ampl\xEDa el tiempo para texto denso y mant\xE9n contexto suficiente para no tergiversar la fuente." }],
692
+ ["Fuente autorizada, procedencia y afirmaci\xF3n verificadas; datos privados eliminados antes de composici\xF3n.", "Motor disponible para recortes, llamadas y texto editable con contraste suficiente."],
693
+ ["La fuente no respalda la afirmaci\xF3n o el recorte cambia su sentido.", "La demo, el OCR no revisado o una imagen sint\xE9tica se presentar\xEDan como evidencia real."]
694
+ ],
695
+ { title: "Referencia de pasaje documental con flecha y contorno", url: "https://www.instagram.com/p/DZXtFEbKxdb/" },
696
+ ["1:1: conserva la fuente en un panel y sit\xFAa la ampliaci\xF3n junto a una sola llamada; prioriza la lectura del detalle.", "9:16: apila contexto, detalle y conclusi\xF3n breve; evita convertir una p\xE1gina completa en texto diminuto.", "16:9: coloca fuente y ampliaci\xF3n lado a lado con una conexi\xF3n corta; preserva pie de fuente y escala legible."]
697
+ ),
698
+ composition(
699
+ [
700
+ "interface-walkthrough",
701
+ "Recorrido de interfaz",
702
+ "Una interfaz recorre una acci\xF3n con cursor, foco y confirmaci\xF3n.",
703
+ "Explicar una tarea digital mediante estados claros y un resultado comprensible.",
704
+ ["Define una sola tarea y estados reales verificados o una interfaz ficticia claramente identificada; nunca presenta la demo como integraci\xF3n disponible.", "Comp\xF3n paneles, textos y cursor en capas editables; elimina datos privados si el recurso es una captura real.", "Gu\xEDa hacia un control, ampl\xEDa la regi\xF3n \xFAtil y muestra el siguiente estado; el cursor y la magnificaci\xF3n son adaptaci\xF3n propuesta, no l\xF3gica de aplicaci\xF3n demostrada por los frames.", "Cierra con una confirmaci\xF3n solo cuando corresponda al comportamiento verificado o a la demo rotulada; comprueba texto, destino y l\xEDmites de la acci\xF3n."],
705
+ [{ key: "steps", value: [2, 4], unit: "estados por tarea", adaptation: "Divide flujos largos en escenas; no simules automatizaci\xF3n no disponible." }, { key: "focus_regions", value: 1, unit: "regi\xF3n activa", adaptation: "El resto conserva contexto estable; el cursor no debe competir con la informaci\xF3n que se explica." }, { key: "step_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta a lectura y explicaci\xF3n; estados est\xE1ticos y foco por corte funcionan con movimiento reducido." }],
706
+ ["Flujo real revisado o demo ficticia rotulada, sin credenciales ni datos de clientes.", "Motor disponible para estados de interfaz, cursor y magnificaci\xF3n; acceso y permisos confirmados por separado."],
707
+ ["La secuencia inventa capacidades de un producto o promete una integraci\xF3n habilitada.", "Una ampliaci\xF3n vuelve ilegible el control, oculta un error real o filtra datos privados."]
708
+ ],
709
+ { title: "Referencia de estados de editor, men\xFA y confirmaci\xF3n", url: "https://www.instagram.com/p/DdocHKLq7i0/" },
710
+ ["1:1: muestra un panel de tarea y ampl\xEDa la regi\xF3n activa dentro del mismo marco sin perder el estado anterior.", "9:16: recomp\xF3n la interfaz como paneles legibles o usa un recorte guiado con contexto; no reduzcas un escritorio completo a m\xF3vil.", "16:9: conserva la interfaz amplia y usa una lente o panel lateral de detalle; el cursor mantiene una ruta corta entre controles."]
711
+ ),
712
+ composition(
713
+ [
714
+ "quote-portrait",
715
+ "Retrato y frase",
716
+ "Un retrato acompa\xF1a una frase breve con jerarqu\xEDa editorial.",
717
+ "Relacionar una idea con una presencia humana sin inventar su autor\xEDa.",
718
+ ["Confirma derechos del retrato, texto exacto y atribuci\xF3n; si son recursos sint\xE9ticos o texto de muestra, identif\xEDcalos como ilustrativos.", "Comp\xF3n retrato y frase como elementos independientes; el referente aporta persona y palabras de anclaje, no una cita atribuida equivalente.", "Revela la frase en bloques legibles y conserva rostro y gesto libres; no generes movimiento labial ni voz para atribuirle palabras al sujeto.", "A\xF1ade nombre o fuente solo cuando est\xE9 verificado; sin atribuci\xF3n usa una idea editorial identificada, evitando comillas que simulen un testimonio."],
719
+ [{ key: "quote_words", value: [6, 18], unit: "palabras orientativas", adaptation: "Respeta el texto exacto autorizado; divide una cita larga sin cambiar su significado." }, { key: "text_blocks", value: [1, 3], unit: "bloques", adaptation: "La jerarqu\xEDa usa las fuentes de marca y no exige cambiar de familia o inventar una firma." }, { key: "reading_hold", value: [2, 5], unit: "s de partida", adaptation: "Ajusta al n\xFAmero de palabras y al p\xFAblico; permite un estado completo legible." }],
720
+ ["Retrato autorizado y texto con atribuci\xF3n confirmada o identificaci\xF3n expl\xEDcita como idea editorial de muestra.", "Motor disponible para tipograf\xEDa editable y composici\xF3n; fuentes de marca instaladas."],
721
+ ["La composici\xF3n har\xEDa parecer que una persona real o sint\xE9tica pronunci\xF3 una frase no verificada.", "El texto cambia el sentido del testimonio, oculta identidad relevante o necesita una voz no autorizada."]
722
+ ],
723
+ { title: "Apoyo de retrato y palabras de anclaje; cita editorial propuesta", url: "https://www.instagram.com/p/DdKae1xzRao/" },
724
+ ["1:1: equilibra retrato y frase en dos zonas con una atribuci\xF3n secundaria legible cuando exista.", "9:16: coloca retrato arriba y frase debajo; conserva zona segura para subt\xEDtulos sin superponer dos lecturas densas.", "16:9: usa retrato en un tercio y un bloque de cita amplio en el resto; no expanda el texto sobre rasgos esenciales."]
725
+ ),
726
+ composition(
727
+ [
728
+ "type-poster",
729
+ "Cartel en movimiento",
730
+ "Palabras grandes construyen un cartel mediante cambios de estado.",
731
+ "Convertir una idea breve en una jerarqu\xEDa tipogr\xE1fica memorable y legible.",
732
+ ["Define un mensaje breve y sus palabras clave sin alterar su orden o sentido.", "Comp\xF3n el cartel con fuentes disponibles de marca; escala, peso y espacio crean contraste sin exigir la paleta del referente.", "Anima entradas, m\xE1scaras o desplazamientos entre estados; la captura de texto grande apoya la jerarqu\xEDa, no acredita una curva o cadencia espec\xEDfica.", "Sost\xE9n el mensaje completo y revisa acentos, interlineado y recortes; con movimiento reducido muestra estados estables sin parpadeos."],
733
+ [{ key: "headline_words", value: [2, 8], unit: "palabras por cartel", adaptation: "Divide mensajes extensos en carteles completos, no en fragmentos que cambien la interpretaci\xF3n." }, { key: "type_levels", value: [2, 3], unit: "niveles de jerarqu\xEDa", adaptation: "Usa peso, tama\xF1o y espaciado dentro de las fuentes de marca; no impone una tipograf\xEDa nueva." }, { key: "state_hold", value: [800, 1800], unit: "ms de partida", adaptation: "Ajusta a lectura real y evita destellos o alternancias r\xE1pidas de alto contraste." }],
734
+ ["Texto confirmado, fuentes de marca disponibles y motor de composici\xF3n tipogr\xE1fica editable.", "Revisi\xF3n de caracteres espa\xF1oles, contraste y \xE1reas seguras en cada formato."],
735
+ ["El texto es demasiado largo para leerse al ritmo propuesto.", "La animaci\xF3n recorta palabras esenciales, convierte decoraci\xF3n en un claim o requiere parpadeos intensos."]
736
+ ],
737
+ { title: "Referencia de palabra tipogr\xE1fica de gran escala", url: "https://www.instagram.com/p/DdKae1xzRao/" },
738
+ ["1:1: organiza una palabra dominante y una l\xEDnea de apoyo con m\xE1rgenes consistentes y un estado final completo.", "9:16: apila l\xEDneas cortas de arriba abajo con pausas de lectura; recomp\xF3n los saltos sin cortar palabras ni acentos.", "16:9: extiende la jerarqu\xEDa horizontalmente o contrapone dos bloques; conserva el orden de lectura y evita ampliar una palabra hasta perder sus extremos."]
739
+ )
740
+ ]
741
+ };
742
+ function getMediaStyle(id, version) {
743
+ return MEDIA_STYLE_CATALOG.styles.find((style) => style.id === id && (version === void 0 || style.version === version));
744
+ }
745
+ var record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
746
+ var only = (value, fields) => Object.keys(value).every((key) => fields.includes(key));
747
+ function visualTasteIssues(value) {
748
+ if (!record(value) || !only(value, ["density", "energy", "contrast"])) return ["visualTaste supports density, energy and contrast only."];
749
+ const options = { density: ["minimal", "layered"], energy: ["calm", "lively"], contrast: ["soft", "strong"] };
750
+ return Object.entries(options).flatMap(([key, allowed]) => value[key] === void 0 || typeof value[key] === "string" && allowed.includes(value[key]) ? [] : [`Invalid visualTaste ${key}. Omit undecided choices.`]);
751
+ }
752
+ function motionSelectionIssues(value) {
753
+ return value === void 0 || value === "adaptive" || value === "guided" ? [] : ["motionSelection must be adaptive or guided. Omit undecided choices."];
754
+ }
755
+ function motionProductionIssues(value) {
756
+ return value === void 0 || value === "auto" || value === "code" || value === "ai" ? [] : ["motionProduction must be auto, code or ai. Omit undecided choices."];
757
+ }
758
+ function resolveMotionSelection(selections = [], motionSelection) {
759
+ return motionSelection ?? (selections.some((selection) => selection.state === "default" && getMediaStyle(selection.styleId, selection.version)?.category === "motion") ? "guided" : "adaptive");
760
+ }
761
+ function mediaStyleSelectionIssues(value, category) {
762
+ if (!Array.isArray(value) || value.length > 24) return ["mediaStyles must be an array of at most 24 selections."];
763
+ const issues = [], ids = /* @__PURE__ */ new Set(), defaults = /* @__PURE__ */ new Set();
764
+ for (const selection of value) {
765
+ if (!record(selection) || !only(selection, ["styleId", "version", "state"]) || typeof selection.styleId !== "string" || typeof selection.version !== "string" || !["favorite", "default", "avoid"].includes(String(selection.state))) {
766
+ issues.push("Media style selections require styleId, version and favorite/default/avoid state.");
767
+ continue;
768
+ }
769
+ const style = getMediaStyle(selection.styleId, selection.version);
770
+ if (!style) {
771
+ issues.push(`Unknown media style or unsupported version: ${selection.styleId}@${selection.version}.`);
772
+ continue;
773
+ }
774
+ if (category && style.category !== category) issues.push(`Media style ${style.id} does not belong to ${category}.`);
775
+ if (ids.has(style.id)) issues.push(`Duplicate media style selection: ${style.id}.`);
776
+ ids.add(style.id);
777
+ if (selection.state === "default") {
778
+ if (defaults.has(style.category)) issues.push(`Only one default media style is allowed for ${style.category}.`);
779
+ defaults.add(style.category);
780
+ }
781
+ }
782
+ return issues;
783
+ }
784
+ function mediaStyleOverrideIssues(value) {
785
+ if (!record(value) || !only(value, ["image", "motion"])) return ["mediaStyleOverrides supports image and motion categories only."];
786
+ const issues = [];
787
+ for (const category of ["image", "motion"]) {
788
+ const override = value[category];
789
+ if (override === void 0) continue;
790
+ if (!record(override) || !["inherit", "replace", "disabled"].includes(String(override.mode))) {
791
+ issues.push(`Invalid ${category} media style override.`);
792
+ continue;
793
+ }
794
+ if (override.mode === "replace") {
795
+ if (!only(override, ["mode", "selections"])) issues.push(`Unknown ${category} media style override fields.`);
796
+ if (!Array.isArray(override.selections) || !override.selections.length) issues.push(`The ${category} replace mode requires selections; use disabled to clear the category.`);
797
+ issues.push(...mediaStyleSelectionIssues(override.selections, category));
798
+ } else if (!only(override, ["mode"])) issues.push(`The ${category} ${override.mode} mode cannot contain selections.`);
799
+ }
800
+ return issues;
801
+ }
802
+ function resolveMediaPreferences(selections = [], overrides = {}, taste = {}, motionSelection, motionProduction, editorialDelivery, captionsEnabled) {
803
+ const issues = [...mediaStyleSelectionIssues(selections), ...mediaStyleOverrideIssues(overrides), ...visualTasteIssues(taste), ...motionSelectionIssues(motionSelection), ...motionProductionIssues(motionProduction), ...editorialDeliveryIssues(editorialDelivery)];
804
+ if (issues.length) throw new Error(issues.join("\n"));
805
+ const category = (kind) => {
806
+ const override = overrides[kind], mode = override?.mode ?? "inherit";
807
+ const selected = mode === "disabled" ? [] : override?.mode === "replace" ? override.selections : selections.filter((selection) => getMediaStyle(selection.styleId, selection.version).category === kind);
808
+ return { mode, source: mode === "inherit" ? selected.length ? "brand" : "unspecified" : "project", selections: selected.map((selection) => ({ ...selection, name: getMediaStyle(selection.styleId, selection.version).name })) };
809
+ };
810
+ const motion2 = category("motion");
811
+ const effectiveMotionSelection = resolveMotionSelection(motion2.selections, motionSelection);
812
+ const motionSelectionSource = motionSelection !== void 0 ? "brand" : effectiveMotionSelection === "guided" && motion2.source !== "unspecified" ? motion2.source : "implicit";
813
+ if (effectiveMotionSelection === "adaptive") motion2.selections = motion2.selections.map((selection) => selection.state === "default" ? { ...selection, state: "favorite" } : selection);
814
+ const delivery = resolveEditorialDelivery(editorialDelivery, captionsEnabled);
815
+ return { schemaVersion: 1, catalog: { id: MEDIA_STYLE_CATALOG.id, version: MEDIA_STYLE_CATALOG.version }, image: category("image"), motion: motion2, motionSelection: effectiveMotionSelection, motionSelectionSource, motionProduction: motionProduction ?? "auto", motionProductionSource: motionProduction === void 0 ? "implicit" : "brand", ...Object.keys(delivery).length ? { editorialDelivery: delivery } : {}, taste: { ...taste }, policy: "Current explicit instructions govern. Project overrides define effective choices; disabled categories stay disabled. Required brand tokens govern, never preview palettes/fonts. Favorites are candidates; effective avoid entries always exclude. Adaptive motion chooses per scene or none; saved defaults become candidate favorites. Guided motion starts from saved choices and checks fit. Production auto chooses by scene and verified tools; code prefers complete editable JavaScript scenes, ai prefers generated clips. Keep exact text/data in controlled composition. Production preferences prove neither personal integration access nor model availability, rights or permission to spend. Check the current member\u2019s integrations and session tools; files access alone cannot generate. Taste guides intensity, not tokens or a forced recipe. Soft contrast must retain readable text and visible essential information." + (Object.keys(delivery).length ? " Editorial delivery contains explicit brand preferences only; omitted fields are for the agent to decide. Existing avoid and source restrictions constrain delivery; no-SFX overrides a general preference for effects. Silent music/effects does not silence speech, and no narration does not mute ambience. Text density excludes subtitles; B-roll format does not authorize a source. Disabled captions omit their language, treatment and position from the effective view without erasing saved choices; a chosen appearance cannot enable captions. Respect framing and safe areas; karaoke needs verified word timing. Explicit project output settings prevail; otherwise source uses verified speech language and brand uses language.json. CTA goal none excludes every CTA and omits its treatment. Other CTA goals guide each project; confirm final copy, keyword and destination in its brief or direction, never invent them. Spoken CTA grants no new speech or rewriting permission and cannot override no narration. Voiceover authorizes neither cloning nor providers or spending." : "") };
816
+ }
817
+
818
+ // packages/core/src/editorial-profile.ts
819
+ var profileOptions = {
820
+ experience: ["Estoy empezando", "Ya creo videos", "Edito profesionalmente"],
821
+ role: ["Marca personal / creador", "Negocio / equipo de marketing", "Agencia / editor para clientes"],
822
+ platforms: ["Instagram / Reels", "TikTok", "YouTube Shorts", "YouTube", "LinkedIn"],
823
+ formats: ["A c\xE1mara", "Tutoriales", "Entrevistas", "Vlogs", "Producto / UGC", "Sin aparecer en c\xE1mara"],
824
+ goal: ["Ense\xF1ar", "Generar confianza", "Vender", "Entretener", "Crear comunidad"],
825
+ pace: ["Pausado", "\xC1gil", "Muy din\xE1mico"],
826
+ captions: ["Subt\xEDtulos discretos", "Palabras destacadas", "Sin subt\xEDtulos"],
827
+ sound: ["Voz protagonista", "M\xFAsica discreta", "M\xFAsica y efectos presentes", "Sin m\xFAsica ni efectos"],
828
+ freedom: ["Mantener un estilo consistente", "Explorar seg\xFAn el proyecto"],
829
+ assets: ["Solo mi material", "Se puede proponer material de stock", "Se puede proponer material generado"],
830
+ avoid: ["Sin emojis", "Sin transiciones llamativas", "Sin efectos de sonido", "No reescribir mis palabras", "No usar im\xE1genes generadas"],
831
+ aspects: ["Tipograf\xEDa", "Composici\xF3n", "Colores", "Im\xE1genes", "Ritmo", "Animaci\xF3n", "Sonido"]
832
+ };
833
+ var emptyProfile = () => ({ schemaVersion: 1, experience: "", role: "", niche: "", platforms: [], formats: [], goal: "", pace: "", captions: "", sound: "", freedom: "", assets: "", avoid: [], notes: "", references: [] });
834
+ var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
835
+ var string = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max;
836
+ var choices = (value, options) => Array.isArray(value) && value.length <= options.length && value.every((item) => typeof item === "string" && options.includes(item));
837
+ function editorialProfileIssues(value) {
838
+ if (!record2(value)) return ["El perfil editorial debe ser un objeto."];
839
+ const issues = [];
840
+ if (value.schemaVersion !== 1) issues.push("schemaVersion del perfil editorial debe ser 1.");
841
+ const fields = [...Object.keys(emptyProfile()), "mediaStyles", "visualTaste", "motionSelection", "motionProduction", "editorialDelivery"];
842
+ if (Object.keys(value).some((key) => !fields.includes(key))) issues.push("El perfil editorial contiene campos desconocidos.");
843
+ for (const key of ["experience", "role", "goal", "pace", "captions", "sound", "freedom", "assets"]) {
844
+ const selected = value[key];
845
+ if ((key === "experience" || key === "role") && selected === void 0) continue;
846
+ if (typeof selected !== "string" || selected !== "" && !profileOptions[key].includes(selected)) issues.push(`Preferencia editorial inv\xE1lida: ${key}.`);
847
+ }
848
+ for (const key of ["platforms", "formats", "avoid"]) if (!choices(value[key], profileOptions[key])) issues.push(`Opciones editoriales inv\xE1lidas: ${key}.`);
849
+ if (!string(value.niche, 0, 500)) issues.push("niche debe ser texto de hasta 500 caracteres.");
850
+ if (!string(value.notes, 0, 1500)) issues.push("notes debe ser texto de hasta 1500 caracteres.");
851
+ if (!Array.isArray(value.references) || value.references.length > 12) issues.push("references admite hasta 12 referencias.");
852
+ else for (const ref of value.references) {
853
+ if (!record2(ref) || !string(ref.videoId, 1, 150) || !string(ref.exampleId, 1, 150) || typeof ref.timestampMs !== "number" || !Number.isFinite(ref.timestampMs) || ref.timestampMs < 0 || !["like", "avoid"].includes(String(ref.sentiment)) || !choices(ref.aspects, profileOptions.aspects)) issues.push("Referencia editorial inv\xE1lida.");
854
+ }
855
+ if (value.mediaStyles !== void 0) issues.push(...mediaStyleSelectionIssues(value.mediaStyles));
856
+ if (value.visualTaste !== void 0) issues.push(...visualTasteIssues(value.visualTaste));
857
+ issues.push(...motionSelectionIssues(value.motionSelection));
858
+ issues.push(...motionProductionIssues(value.motionProduction));
859
+ issues.push(...editorialDeliveryIssues(value.editorialDelivery));
860
+ return issues;
861
+ }
862
+ function parseEditorialProfile(value) {
863
+ const issues = editorialProfileIssues(value);
864
+ if (issues.length) throw new Error(issues.join("\n"));
865
+ const input = value;
866
+ return { ...input, experience: input.experience ?? "", role: input.role ?? "", platforms: [...input.platforms], formats: [...input.formats], avoid: [...input.avoid], references: input.references.map((ref) => ({ videoId: ref.videoId, exampleId: ref.exampleId, timestampMs: ref.timestampMs, sentiment: ref.sentiment, aspects: [...ref.aspects] })), ...input.mediaStyles !== void 0 ? { mediaStyles: input.mediaStyles.map((selection) => ({ ...selection })) } : {}, ...input.visualTaste !== void 0 ? { visualTaste: { ...input.visualTaste } } : {}, ...input.editorialDelivery !== void 0 ? { editorialDelivery: { ...input.editorialDelivery } } : {} };
867
+ }
868
+ var start = "<!-- editorbrain:editorial-profile:start -->";
869
+ var end = "<!-- editorbrain:editorial-profile:end -->";
870
+ function bounds(content) {
871
+ const a = content.indexOf(start), b = content.indexOf(end);
872
+ if (a === -1 && b === -1) return null;
873
+ if (a === -1 || b < a || content.indexOf(start, a + start.length) !== -1 || content.indexOf(end, b + end.length) !== -1) throw new Error("El perfil fue editado fuera del formulario. Revisa su bloque en Gu\xEDa visual antes de guardar.");
874
+ return [a, b + end.length];
875
+ }
876
+ function readProfile(content) {
877
+ const span = bounds(content);
878
+ if (!span) return emptyProfile();
879
+ const match = content.slice(...span).match(/```json\r?\n([\s\S]*?)\r?\n```/);
880
+ if (!match) throw new Error("No pudimos leer tu perfil. Revisa el bloque en Gu\xEDa visual; conservamos el archivo original.");
881
+ return parseEditorialProfile(JSON.parse(match[1]));
882
+ }
883
+
203
884
  // packages/core/src/production-plan.ts
204
- var record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
885
+ var record3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
205
886
  var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
206
887
  var nonnegative = (v) => v === 0 || positive(v);
207
888
  var hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
208
889
  var text = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
209
- var range = (v) => record(v) && nonnegative(v.startSeconds) && positive(v.endSeconds) && v.endSeconds > v.startSeconds;
890
+ var range = (v) => record3(v) && nonnegative(v.startSeconds) && positive(v.endSeconds) && v.endSeconds > v.startSeconds;
210
891
  var MAX_ENTRIES = 2e3;
211
892
  function validateProductionPlan(value) {
212
893
  const issues = [];
213
- if (!record(value) || value.schemaVersion !== 1 || value.kind !== "editorbrain.production-plan") return { valid: false, issues: ["Expected schemaVersion 1 and kind editorbrain.production-plan."] };
894
+ if (!record3(value) || value.schemaVersion !== 1 || value.kind !== "editorbrain.production-plan") return { valid: false, issues: ["Expected schemaVersion 1 and kind editorbrain.production-plan."] };
214
895
  if (Object.keys(value).some((key) => !["schemaVersion", "kind", "inspections", "uses"].includes(key))) issues.push("Unknown plan fields; keep editorial notes in direction.md and use the documented plan schema.");
215
896
  if (!Array.isArray(value.inspections) || value.inspections.length > MAX_ENTRIES) issues.push("inspections must be an array of at most 2000 observations.");
216
897
  else value.inspections.forEach((item, index) => {
217
898
  const label = `inspections[${index}]`;
218
- if (!record(item) || !hash(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
899
+ if (!record3(item) || !hash(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
219
900
  issues.push(`${label}: invalid source hash, modality or method.`);
220
901
  return;
221
902
  }
@@ -233,7 +914,7 @@ function validateProductionPlan(value) {
233
914
  const ids = /* @__PURE__ */ new Set();
234
915
  value.uses.forEach((item, index) => {
235
916
  const label = `uses[${index}]`;
236
- if (!record(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
917
+ if (!record3(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
237
918
  issues.push(`${label}: requires ID, source hash, source and timeline intervals.`);
238
919
  return;
239
920
  }
@@ -259,7 +940,7 @@ function checkProductionPlan(inventory, value) {
259
940
  const findings = checked.issues.map((message) => ({ code: "INVALID_PLAN", severity: "error", message }));
260
941
  const sources = /* @__PURE__ */ new Map();
261
942
  let inventoryComplete = true;
262
- if (!record(inventory) || inventory.schemaVersion !== 1 || inventory.kind !== "editorbrain.media-inventory" || inventory.summary === true || typeof inventory.complete !== "boolean" || !Array.isArray(inventory.assets) || inventory.assets.length > 1e4) {
943
+ if (!record3(inventory) || inventory.schemaVersion !== 1 || inventory.kind !== "editorbrain.media-inventory" || inventory.summary === true || typeof inventory.complete !== "boolean" || !Array.isArray(inventory.assets) || inventory.assets.length > 1e4) {
263
944
  inventoryComplete = false;
264
945
  findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Use the full schemaVersion 1 media inventory saved with --out, not compact stdout. It must declare complete." });
265
946
  } else {
@@ -268,7 +949,7 @@ function checkProductionPlan(inventory, value) {
268
949
  findings.push({ code: "INVENTORY_INCOMPLETE", severity: "warning", message: "Inventory traversal or probing was incomplete. This plan check covers only recorded assets." });
269
950
  }
270
951
  for (const asset of inventory.assets) {
271
- if (!record(asset) || !hash(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record(s) || typeof s.type !== "string"))) {
952
+ if (!record3(asset) || !hash(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record3(s) || typeof s.type !== "string"))) {
272
953
  findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Inventory assets need a source hash, nonnegative duration or null, and valid optional streams." });
273
954
  continue;
274
955
  }
@@ -284,12 +965,12 @@ function checkProductionPlan(inventory, value) {
284
965
  if (!source) findings.push({ code: "UNKNOWN_SOURCE", severity: "error", message: "A source hash is absent from the selected inventory. Refresh the inventory after changing media.", sourceSha256 });
285
966
  return source;
286
967
  };
287
- const inBounds = (end, source) => source.durationSeconds === null || end <= source.durationSeconds + 1e-3;
968
+ const inBounds = (end2, source) => source.durationSeconds === null || end2 <= source.durationSeconds + 1e-3;
288
969
  for (const inspection of plan.inspections) {
289
970
  const source = sourceFor(inspection.sourceSha256);
290
971
  if (!source) continue;
291
972
  if (source.streams && !source.streams.some((s) => s.type === (inspection.modality === "audio" ? "audio" : "video") && (inspection.modality !== "motion" || !s.attachedPicture))) findings.push({ code: "MODALITY_ABSENT", severity: "error", message: `Declared ${inspection.modality} inspection has no matching stream.`, sourceSha256: source.sha256 });
292
- if ([...inspection.sampledSeconds ?? [], ...(inspection.intervals ?? []).map((r) => r.endSeconds)].some((end) => !inBounds(end, source))) findings.push({ code: "INSPECTION_OUT_OF_BOUNDS", severity: "error", message: "Inspection extends past the source duration.", sourceSha256: source.sha256 });
973
+ if ([...inspection.sampledSeconds ?? [], ...(inspection.intervals ?? []).map((r) => r.endSeconds)].some((end2) => !inBounds(end2, source))) findings.push({ code: "INSPECTION_OUT_OF_BOUNDS", severity: "error", message: "Inspection extends past the source duration.", sourceSha256: source.sha256 });
293
974
  }
294
975
  const validUses = [];
295
976
  for (const use of plan.uses) {
@@ -331,6 +1012,53 @@ function checkProductionPlan(inventory, value) {
331
1012
  return { schemaVersion: 1, kind: "editorbrain.production-plan-check", valid: !findings.some((f) => f.severity === "error"), inventoryComplete, findings, coverage, repeatedPairs, sourceFilesReverified: false, quality: "not_assessed", observations: "agent_declared" };
332
1013
  }
333
1014
 
1015
+ // packages/core/src/integrations.ts
1016
+ var integrationCatalog = [
1017
+ { id: "higgsfield", name: "Higgsfield", description: "Im\xE1genes y clips generados para apoyar tu edici\xF3n.", capabilities: ["image", "video"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://open.higgsfield.ai/quick-start" },
1018
+ { id: "elevenlabs", name: "ElevenLabs", description: "Voces, efectos de sonido y m\xFAsica generada.", capabilities: ["voiceover", "sfx", "music"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://elevenlabs.io/docs/overview" },
1019
+ { id: "suno", name: "Suno", description: "M\xFAsica generada con Suno; el acceso API depende de tu cuenta.", capabilities: ["music"], accessMethods: ["browser", "api", "files"], docsUrl: "https://platform.suno.com/" },
1020
+ { id: "openai-images", name: "OpenAI Images", description: "Creaci\xF3n y edici\xF3n de im\xE1genes para tu contenido.", capabilities: ["image"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://platform.openai.com/docs/guides/image-generation" },
1021
+ { id: "pexels", name: "Pexels", description: "Fotos y videos de stock; revisa la licencia y el contexto de uso.", capabilities: ["stock-image", "stock-video"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://www.pexels.com/api/documentation/" },
1022
+ { id: "assemblyai", name: "AssemblyAI", description: "Transcripci\xF3n con marcas de tiempo para planificar cortes y subt\xEDtulos.", capabilities: ["transcription"], accessMethods: ["agent", "api", "files"], docsUrl: "https://www.assemblyai.com/docs" }
1023
+ ];
1024
+ function object2(value) {
1025
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1026
+ }
1027
+ function knownFields(value, allowed) {
1028
+ if (Object.keys(value).some((key) => !allowed.includes(key))) throw new Error("Unknown integration field. Credentials and user/workspace IDs are not accepted.");
1029
+ }
1030
+ function containsCredential(value) {
1031
+ return /-----BEGIN [A-Z ]*PRIVATE KEY-----|\bBearer\s+\S{12,}|\b(?:sk-(?:proj-)?|AIza|cfat_|apikey_|tlk_)[A-Za-z0-9_-]{15,}|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|(?:api[_ -]?key|access[_ -]?token|secret|password)\s*[:=]\s*["']?\S{8,}/i.test(value);
1032
+ }
1033
+ function validateIntegrationUpdate(providerId, input) {
1034
+ const provider = integrationCatalog.find((entry) => entry.id === providerId);
1035
+ if (!provider) throw new Error("Unknown integration provider.");
1036
+ if (!object2(input)) throw new Error("Integration settings must be an object.");
1037
+ knownFields(input, ["enabled", "accessMethod", "preferences", "expectedRevision"]);
1038
+ if (typeof input.enabled !== "boolean") throw new Error("enabled must be a boolean.");
1039
+ if (!provider.accessMethods.includes(input.accessMethod)) throw new Error("Unsupported access method for this provider.");
1040
+ if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) throw new Error("The current expectedRevision is required.");
1041
+ if (!object2(input.preferences)) throw new Error("preferences must be an object.");
1042
+ knownFields(input.preferences, ["instructions", "model", "voiceId", "language", "musicMode", "spending"]);
1043
+ const preferences = {};
1044
+ for (const [field, max] of [["instructions", 1e3], ["model", 120], ["voiceId", 120], ["language", 60]]) {
1045
+ const value = input.preferences[field];
1046
+ if (value === void 0) continue;
1047
+ if (typeof value !== "string" || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value)) throw new Error(`${field} must be text of at most ${max} characters.`);
1048
+ if (containsCredential(value)) throw new Error("Credentials must stay in your provider connection, never in integration preferences.");
1049
+ if (value.trim()) preferences[field] = value.trim();
1050
+ }
1051
+ const musicMode = input.preferences.musicMode, spending = input.preferences.spending;
1052
+ if (musicMode !== void 0) {
1053
+ if (!provider.capabilities.includes("music") || !["instrumental", "vocals", "either"].includes(musicMode)) throw new Error("musicMode is only available for music integrations.");
1054
+ preferences.musicMode = musicMode;
1055
+ }
1056
+ if (preferences.voiceId && !provider.capabilities.includes("voiceover")) throw new Error("voiceId is only available for voice integrations.");
1057
+ if (spending !== void 0 && !["ask", "use-approved-budget"].includes(spending)) throw new Error("Invalid spending preference.");
1058
+ preferences.spending = spending ?? "ask";
1059
+ return { enabled: input.enabled, accessMethod: input.accessMethod, preferences, expectedRevision: input.expectedRevision };
1060
+ }
1061
+
334
1062
  // packages/core/src/index.ts
335
1063
  var SCHEMA_VERSION = 1;
336
1064
  var MAX_DOCUMENT_BYTES = 128 * 1024;
@@ -345,7 +1073,7 @@ var ValidationError = class extends Error {
345
1073
  var identifier2 = /^[a-z0-9][a-z0-9_-]{0,79}$/;
346
1074
  var tokenName = /^[a-z][a-z0-9_.-]{0,79}$/;
347
1075
  var own2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
348
- var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1076
+ var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
349
1077
  var nonempty2 = (value) => typeof value === "string" && value.trim().length > 0;
350
1078
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
351
1079
  var tokenValue = (value) => typeof value === "string" || typeof value === "boolean" || finite(value) || Array.isArray(value) && value.every((v) => typeof v === "string");
@@ -362,13 +1090,13 @@ function safeDocumentPath(path) {
362
1090
  return path;
363
1091
  }
364
1092
  function checkRules(value, add) {
365
- if (!object2(value)) {
1093
+ if (!object3(value)) {
366
1094
  add("tokens must be an object");
367
1095
  return;
368
1096
  }
369
1097
  for (const [key, rule] of Object.entries(value)) {
370
1098
  if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid semantic token ${key}`);
371
- if (!object2(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
1099
+ if (!object3(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
372
1100
  }
373
1101
  }
374
1102
  function checkEvidence(value, add) {
@@ -377,7 +1105,7 @@ function checkEvidence(value, add) {
377
1105
  return;
378
1106
  }
379
1107
  for (const ref of value) {
380
- if (!object2(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
1108
+ if (!object3(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
381
1109
  else {
382
1110
  if (ref.timestampSeconds !== void 0 && (!finite(ref.timestampSeconds) || ref.timestampSeconds < 0)) add("Evidence timestamp must be nonnegative seconds");
383
1111
  if (ref.reviewStatus !== void 0 && !["unreviewed", "agent_reviewed", "human_verified"].includes(String(ref.reviewStatus))) add("Invalid evidence reviewStatus");
@@ -398,7 +1126,16 @@ function validateDocument(path, content) {
398
1126
  return { valid: false, issues };
399
1127
  }
400
1128
  if (content.includes("\0")) add("NUL bytes are not allowed");
401
- if (path.endsWith(".md")) return { valid: issues.length === 0, issues };
1129
+ if (path.endsWith(".md")) {
1130
+ if (/^\.editorbrain\/brands\/[^/]+\/design\.md$/.test(canonicalDocumentPath(path))) {
1131
+ try {
1132
+ readProfile(content);
1133
+ } catch (error) {
1134
+ add(error.message);
1135
+ }
1136
+ }
1137
+ return { valid: issues.length === 0, issues };
1138
+ }
402
1139
  let doc;
403
1140
  try {
404
1141
  doc = JSON.parse(content);
@@ -406,7 +1143,7 @@ function validateDocument(path, content) {
406
1143
  add("Invalid JSON");
407
1144
  return { valid: false, issues };
408
1145
  }
409
- if (!object2(doc)) {
1146
+ if (!object3(doc)) {
410
1147
  add("JSON document must be an object");
411
1148
  return { valid: false, issues };
412
1149
  }
@@ -430,19 +1167,20 @@ function validateDocument(path, content) {
430
1167
  if (isStyle) {
431
1168
  if (!nonempty2(doc.name)) add("Style name is required");
432
1169
  if (doc.references !== void 0) checkEvidence(doc.references, add);
433
- if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object2(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
1170
+ if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object3(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
434
1171
  }
435
1172
  }
436
1173
  if (isProject) {
437
1174
  for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier2.test(doc[key])) add(`${key} must be a valid identifier`);
438
- if (!object2(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
1175
+ if (!object3(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
439
1176
  if (doc.reviewMode !== void 0 && !["direct", "directions", "sample"].includes(doc.reviewMode)) add("reviewMode must be direct, directions or sample");
1177
+ if (doc.mediaStyleOverrides !== void 0) for (const issue of mediaStyleOverrideIssues(doc.mediaStyleOverrides)) add(issue);
440
1178
  if (doc.script !== void 0 && typeof doc.script !== "string") add("script must be text");
441
1179
  if (doc.overrides !== void 0) {
442
- if (!object2(doc.overrides)) add("overrides must be an object");
1180
+ if (!object3(doc.overrides)) add("overrides must be an object");
443
1181
  else for (const [key, rule] of Object.entries(doc.overrides)) {
444
1182
  if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid override token ${key}`);
445
- if (!object2(rule) || !tokenValue(rule.value) || !nonempty2(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
1183
+ if (!object3(rule) || !tokenValue(rule.value) || !nonempty2(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
446
1184
  }
447
1185
  }
448
1186
  }
@@ -452,7 +1190,7 @@ function validateDocument(path, content) {
452
1190
  else {
453
1191
  const ids = /* @__PURE__ */ new Set();
454
1192
  for (const asset of doc.assets) {
455
- if (!object2(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty2(asset.name) || typeof asset.mimeType !== "string" || !/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(asset.mimeType) || !Number.isSafeInteger(asset.bytes) || Number(asset.bytes) < 1) {
1193
+ if (!object3(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty2(asset.name) || typeof asset.mimeType !== "string" || !/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(asset.mimeType) || !Number.isSafeInteger(asset.bytes) || Number(asset.bytes) < 1) {
456
1194
  add("Production assets require a SHA-256 id, name, media mimeType and positive integer bytes");
457
1195
  continue;
458
1196
  }
@@ -466,7 +1204,7 @@ function validateDocument(path, content) {
466
1204
  for (const key of ["width", "height"]) if (!Number.isInteger(doc[key]) || Number(doc[key]) < 1 || Number(doc[key]) > 16384) add(`${key} must be an integer between 1 and 16384`);
467
1205
  if (!finite(doc.fps) || doc.fps <= 0 || doc.fps > 240) add("fps must be between 0 and 240");
468
1206
  if (doc.safeAreas !== void 0) {
469
- if (!object2(doc.safeAreas) || ["top", "right", "bottom", "left"].some((k) => !finite(doc.safeAreas[k]) || Number(doc.safeAreas[k]) < 0 || Number(doc.safeAreas[k]) >= 0.5)) add("safeAreas must have normalized fractions from 0 up to (not including) 0.5 for all four sides");
1207
+ if (!object3(doc.safeAreas) || ["top", "right", "bottom", "left"].some((k) => !finite(doc.safeAreas[k]) || Number(doc.safeAreas[k]) < 0 || Number(doc.safeAreas[k]) >= 0.5)) add("safeAreas must have normalized fractions from 0 up to (not including) 0.5 for all four sides");
470
1208
  }
471
1209
  }
472
1210
  return { valid: issues.length === 0, issues };
@@ -501,7 +1239,7 @@ function validateWorkspace(files) {
501
1239
  return { valid: issues.length === 0, issues };
502
1240
  }
503
1241
  function stableJson(value) {
504
- const sort = (item) => Array.isArray(item) ? item.map(sort) : object2(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
1242
+ const sort = (item) => Array.isArray(item) ? item.map(sort) : object3(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
505
1243
  return JSON.stringify(sort(value), null, 2) + "\n";
506
1244
  }
507
1245
  async function sha256(content) {
@@ -554,6 +1292,7 @@ async function resolveProject(files, projectId) {
554
1292
  if (!files[directionPath].trim() || files[directionPath].includes("DIRECTION_PENDING")) warnings.push("Creative direction is pending: the host agent or editor must write a coherent plan for the actual material.");
555
1293
  for (const component of style.components ?? []) if (component.status !== "transfer_tested") warnings.push(`Component ${component.id} is ${component.status}; transfer fidelity is not verified.`);
556
1294
  const sourceHashes = Object.fromEntries(await Promise.all(sourcePaths.map(async (source) => [source, await sha256(files[source])])));
1295
+ const editorialProfile = readProfile(files[optionalPaths[0]] ?? "");
557
1296
  return {
558
1297
  schemaVersion: 1,
559
1298
  project,
@@ -568,6 +1307,7 @@ async function resolveProject(files, projectId) {
568
1307
  ...own2(files, optionalPaths[3]) ? { media: JSON.parse(files[optionalPaths[3]]) } : {},
569
1308
  ...own2(files, optionalPaths[7]) ? { styleResources: JSON.parse(files[optionalPaths[7]]) } : {},
570
1309
  memory: resolveMemory(files, project),
1310
+ mediaPreferences: resolveMediaPreferences(editorialProfile.mediaStyles, project.mediaStyleOverrides, editorialProfile.visualTaste, editorialProfile.motionSelection, editorialProfile.motionProduction, editorialProfile.editorialDelivery, editorialProfile.captions === "Sin subt\xEDtulos" || output2.captions?.enabled === false ? false : void 0),
571
1311
  evidence: style.references ?? [],
572
1312
  warnings,
573
1313
  lock: { schemaVersion: 1, projectId, sourceHashes, overrides: project.overrides ?? {}, adapters: Object.fromEntries(["neutral", "hyperframes", "remotion"].map((key) => [key, { version: "0.2.1", kind: "editorial_handoff", rendered: false }])) }
@@ -589,6 +1329,10 @@ ${stableJson(resolved.media)}\`\`\``) : "";
589
1329
 
590
1330
  \`\`\`json
591
1331
  ${stableJson(resolved.styleResources)}\`\`\``) : "";
1332
+ const mediaStyleSection = resolved.mediaPreferences.editorialDelivery || resolved.mediaPreferences.motionSelectionSource !== "implicit" || resolved.mediaPreferences.motionProductionSource !== "implicit" || Object.keys(resolved.mediaPreferences.taste).length || resolved.mediaPreferences.image.selections.length || resolved.mediaPreferences.motion.selections.length || resolved.mediaPreferences.image.mode !== "inherit" || resolved.mediaPreferences.motion.mode !== "inherit" ? section("Media style preferences", `Adaptive motion: choose per scene, using favorites as guidance and excluding avoided styles; no animation is also valid. Guided motion: start from saved choices, checking scene fit. Production auto/code/ai is separate: code can build complete JavaScript scenes; ai prefers generated clips only with verified personal tool access and existing authorization. Disabled categories remain disabled. Retrieve only relevant recipes by ID and version. Required brand tokens govern.
1333
+
1334
+ \`\`\`json
1335
+ ${stableJson(resolved.mediaPreferences)}\`\`\``) : "";
592
1336
  if (adapter === "neutral") return { ...common, "brief.md": `# ${resolved.project.id}
593
1337
 
594
1338
  ${disclaimer}
@@ -605,7 +1349,7 @@ ${resolved.editorial.brand}
605
1349
 
606
1350
  ## Style
607
1351
 
608
- ${resolved.editorial.style}${section("Design", resolved.editorial.design)}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}
1352
+ ${resolved.editorial.style}${section("Design", resolved.editorial.design)}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}${mediaStyleSection}
609
1353
 
610
1354
  Exact tokens, output settings and evidence are in resolved.json.
611
1355
  ` };
@@ -633,13 +1377,13 @@ ${resolved.editorial.direction}
633
1377
 
634
1378
  ## Style intent
635
1379
 
636
- ${resolved.editorial.style}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}
1380
+ ${resolved.editorial.style}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}${mediaStyleSection}
637
1381
 
638
1382
  ## Delivery
639
1383
 
640
1384
  ${resolved.output.width} \xD7 ${resolved.output.height}, ${resolved.output.fps} fps. Adapt composition to the actual material and safe areas in resolved.json. Implement and preview the composition in Hyperframes before claiming completion.
641
1385
  ` };
642
- if (adapter === "remotion") return { ...common, "remotion-plan.json": stableJson({ schemaVersion: 1, kind: "editorial_handoff", renderer: "remotion", executable: false, note: disclaimer, composition: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, durationInFrames: null }, tokens: resolved.tokens, project: resolved.project, direction: resolved.editorial.direction, editorial: resolved.editorial, ...resolved.media ? { media: resolved.media } : {}, ...resolved.styleResources ? { styleResources: resolved.styleResources } : {}, evidence: resolved.evidence, implementationTasks: ["Determine duration from the script/audio and actual cuts.", "Implement React composition and assets.", "Preview and verify typography, safe areas, motion and audio."] }) };
1386
+ if (adapter === "remotion") return { ...common, "remotion-plan.json": stableJson({ schemaVersion: 1, kind: "editorial_handoff", renderer: "remotion", executable: false, note: disclaimer, composition: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, durationInFrames: null }, tokens: resolved.tokens, project: resolved.project, direction: resolved.editorial.direction, editorial: resolved.editorial, mediaPreferences: resolved.mediaPreferences, ...resolved.media ? { media: resolved.media } : {}, ...resolved.styleResources ? { styleResources: resolved.styleResources } : {}, evidence: resolved.evidence, implementationTasks: ["Determine duration from the script/audio and actual cuts.", "Implement React composition and assets.", "Preview and verify typography, safe areas, motion and audio."] }) };
643
1387
  throw new Error(`Unsupported adapter: ${adapter}`);
644
1388
  }
645
1389
  function createWorkspace(options = {}) {
@@ -1116,7 +1860,7 @@ import { createHash } from "node:crypto";
1116
1860
  import { constants as constants2 } from "node:fs";
1117
1861
  import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
1118
1862
  import { fileURLToPath } from "node:url";
1119
- var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3", "dacce1c4127f4281df6f7e1681e243444bb6f8a8e74b829e52ee0108c6672348", "09740ab4b275eada65b5040ae6000104ff39094f534a2784e67feb2afc2a59b3", "f18269ad5504fff8a4fc522d463483261f5041e56cb7f1081f9614bc67e0d96f"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640", "bacedb7bdedab5abe75c9601717cfdb05a96208fa2e2a42af878e2dfd2ae4251", "ea3b473179857ccc07e9e4f21a6ede9e0906a09e495f4a051069d15aee1556cd", "faa2bba99ae3023be7f588d02177a4abf583d3a8a65f617b8cff7a3ceed09dfc"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008", "8b2e01bfd937237919f5ade794eed01dc6e32b556ede9de3a0ccc28cbdd988d1", "ca88b2816a795a5ebe834074b178469367634b80ad837e3697182ea585af1c05", "490daf154b428b536d0a6f590030c854b2fc8ca6adb995c6ecd5a162f85e471e"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5", "70fc660e7ed3d70c16c33c62c8e0399b99c3251808f093ef3d4ea5e8d39e4c8a", "28c62c1e28842142607f5168ee79b18fd797900dd66dd9ac5eb3cfcf8a09f781", "b86eae85cf86dbf4409b10f4d8df1f1649fc408ce1a240f7cf26d5bb69aabfd1"], "references/first-edit.md": ["cb1e9d3272b2b22ae095aed02e33067c646da48e7b64cf71ff81197f6e67b673"], "references/memory-and-review.md": ["51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289"], "references/search-brief.md": ["137203383a7873593136428520f5f9cd6ce1ab6cfeab365c277175786eba3ab3"], "references/style-creator.md": ["1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2"] };
1863
+ var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3", "dacce1c4127f4281df6f7e1681e243444bb6f8a8e74b829e52ee0108c6672348", "09740ab4b275eada65b5040ae6000104ff39094f534a2784e67feb2afc2a59b3", "f18269ad5504fff8a4fc522d463483261f5041e56cb7f1081f9614bc67e0d96f", "5779a28879313070b0d819771ac967d4a133cff1c3dbda2ec44b4dd2abcb17d1", "30f1e64622e70552a1a6129cacc646f56b09b3d7a1fd3f5bf22299bc6bfd0d1e", "6975d917226f75ad55c67543bf7f1c5ef18f4cfe43426c69400c56c1ed497db3"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640", "bacedb7bdedab5abe75c9601717cfdb05a96208fa2e2a42af878e2dfd2ae4251", "ea3b473179857ccc07e9e4f21a6ede9e0906a09e495f4a051069d15aee1556cd", "faa2bba99ae3023be7f588d02177a4abf583d3a8a65f617b8cff7a3ceed09dfc", "83c75394bc7eaab6662a76d89339361e836cc1a988237720eeeacdd6b62e726f", "b685cb7ff21d5bfe19d96ce0139c17b7eb014b7ecda8ec5321558b0004bfdc71"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008", "8b2e01bfd937237919f5ade794eed01dc6e32b556ede9de3a0ccc28cbdd988d1", "ca88b2816a795a5ebe834074b178469367634b80ad837e3697182ea585af1c05", "490daf154b428b536d0a6f590030c854b2fc8ca6adb995c6ecd5a162f85e471e", "9f98affecd69752c17f52fb51ae74b26a1cadea5919b8b74b7865a20661c4418", "19fc85e92cef203f4162e2bfb2dd2df8ef90472bc8f81dfb568b65a714335ff9"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3", "4a4035b87a6c024b419847ab8592bddfeb9b2d9c184f65880c47d661f0bb250e"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5", "70fc660e7ed3d70c16c33c62c8e0399b99c3251808f093ef3d4ea5e8d39e4c8a", "28c62c1e28842142607f5168ee79b18fd797900dd66dd9ac5eb3cfcf8a09f781", "b86eae85cf86dbf4409b10f4d8df1f1649fc408ce1a240f7cf26d5bb69aabfd1", "fc65f343e50b91327653bdc5d94f35af9c86c097bb46810b4b2f7c9a1c200166"], "references/first-edit.md": ["cb1e9d3272b2b22ae095aed02e33067c646da48e7b64cf71ff81197f6e67b673", "5a02253d9907f5e6705304565da7b2d1787e84ae17616414b295a8526ec3e4a5"], "references/memory-and-review.md": ["51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289"], "references/search-brief.md": ["137203383a7873593136428520f5f9cd6ce1ab6cfeab365c277175786eba3ab3", "df93154562bb89a9f22d31260c17406565f5f308c407643c85d15e528ef2c086"], "references/style-creator.md": ["1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2"], "references/local-production.md": ["0a13c65cd60e40ba703095f5e33a1ea66433323b38345c2034fb73c602c438ee"], "references/integrations.md": ["a3500dd79c71ab3ee80a865bb5bf8c94287efb933e07d577738891e276a819a0"], "references/media-preferences.md": ["970b97fbd55b8e9e05fd7e51b4a953cd1b96b9bee7830382aa7514f1b240b3aa", "481c643a5e6516413a6cae970b5170573c981d0effd7d4e146c5cd07c0ee3b1f", "8f11e1956ed06ab73ba58d8c880f0d2292eb76829b07d2de2eaaed1965885158", "e94bc13c05ffd57eceecc4ceaa42df2b83904c12f8c552e161c98f13e6427e5b"] };
1120
1864
  var destinations = {
1121
1865
  codex: { directory: ".agents/skills/editorbrain", entryPoint: "AGENTS.md" },
1122
1866
  claude: { directory: ".claude/skills/editorbrain", entryPoint: "CLAUDE.md" }
@@ -1478,8 +2222,8 @@ async function setup(options, runner = runHost) {
1478
2222
  }
1479
2223
 
1480
2224
  // packages/cli/src/index.ts
1481
- import { chmod, lstat as lstat7, mkdir as mkdir5, open as open7 } from "node:fs/promises";
1482
- import { constants as constants7 } from "node:fs";
2225
+ import { chmod, lstat as lstat7, mkdir as mkdir5, open as open8 } from "node:fs/promises";
2226
+ import { constants as constants8 } from "node:fs";
1483
2227
  import { homedir } from "node:os";
1484
2228
  import { basename as basename2, dirname as dirname6, extname as extname3, join as join3, resolve as resolve6 } from "node:path";
1485
2229
 
@@ -1520,7 +2264,7 @@ function expired() {
1520
2264
  function invalid() {
1521
2265
  return new DeviceLoginError("invalid_response", "The authorization server returned an invalid response. No credential was saved.");
1522
2266
  }
1523
- function record2(value) {
2267
+ function record4(value) {
1524
2268
  return !!value && typeof value === "object" && !Array.isArray(value);
1525
2269
  }
1526
2270
  function abortCheck(signal) {
@@ -1568,11 +2312,11 @@ async function responseJson(response) {
1568
2312
  }
1569
2313
  }
1570
2314
  function sessionFrom(value) {
1571
- if (!record2(value) || typeof value.deviceCode !== "string" || !/^[a-zA-Z0-9._~-]{16,1024}$/.test(value.deviceCode) || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || typeof value.verificationUri !== "string" || !Number.isInteger(value.expiresIn) || Number(value.expiresIn) < 1 || Number(value.expiresIn) > 3600 || !Number.isInteger(value.interval) || Number(value.interval) < 1 || Number(value.interval) > 60) throw invalid();
2315
+ if (!record4(value) || typeof value.deviceCode !== "string" || !/^[a-zA-Z0-9._~-]{16,1024}$/.test(value.deviceCode) || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || typeof value.verificationUri !== "string" || !Number.isInteger(value.expiresIn) || Number(value.expiresIn) < 1 || Number(value.expiresIn) > 3600 || !Number.isInteger(value.interval) || Number(value.interval) < 1 || Number(value.interval) > 60) throw invalid();
1572
2316
  return { deviceCode: value.deviceCode, userCode: value.userCode, verificationUri: verificationUrl(value.verificationUri, value.userCode, value.deviceCode), expiresIn: Number(value.expiresIn), interval: Number(value.interval) };
1573
2317
  }
1574
2318
  function authorizationFrom(value) {
1575
- if (!record2(value) || typeof value.apiKey !== "string" || !/^eh_[a-f0-9]{64}$/.test(value.apiKey) || value.role !== "editor" || !record2(value.workspace) || typeof value.workspace.id !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(value.workspace.id) || typeof value.workspace.name !== "string" || !value.workspace.name.trim() || value.workspace.name.length > 100) throw invalid();
2319
+ if (!record4(value) || typeof value.apiKey !== "string" || !/^eh_[a-f0-9]{64}$/.test(value.apiKey) || value.role !== "editor" || !record4(value.workspace) || typeof value.workspace.id !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(value.workspace.id) || typeof value.workspace.name !== "string" || !value.workspace.name.trim() || value.workspace.name.length > 100) throw invalid();
1576
2320
  return { apiKey: value.apiKey, workspace: { id: value.workspace.id, name: value.workspace.name }, role: "editor" };
1577
2321
  }
1578
2322
  async function loginWithDevice(options, overrides = {}) {
@@ -2249,21 +2993,21 @@ import { resolve as resolve5 } from "node:path";
2249
2993
  function invalidProfile(message) {
2250
2994
  throw new CliError("RENDER_PROFILE_INVALID", message, "Use a schemaVersion: 1 render profile with explicit technical requirements.");
2251
2995
  }
2252
- function object3(value, path, keys) {
2996
+ function object4(value, path, keys) {
2253
2997
  if (!value || typeof value !== "object" || Array.isArray(value)) invalidProfile(`${path} must be an object.`);
2254
2998
  const result = value;
2255
2999
  for (const key of Object.keys(result)) if (!keys.includes(key)) invalidProfile(`${path} contains unsupported field ${key}.`);
2256
3000
  return result;
2257
3001
  }
2258
3002
  function validateRenderCheckProfile(value) {
2259
- const root = object3(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
3003
+ const root = object4(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
2260
3004
  if (root.schemaVersion !== 1) invalidProfile("profile.schemaVersion must be 1.");
2261
- const expected = root.expected === void 0 ? {} : object3(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
2262
- const tolerance = root.tolerance === void 0 ? {} : object3(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
2263
- const silence = root.silence === void 0 ? {} : object3(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
2264
- const black = root.black === void 0 ? {} : object3(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
2265
- const peak = root.peak === void 0 ? {} : object3(root.peak, "peak", ["enabled", "thresholdDbFS"]);
2266
- const limits = root.limits === void 0 ? {} : object3(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
3005
+ const expected = root.expected === void 0 ? {} : object4(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
3006
+ const tolerance = root.tolerance === void 0 ? {} : object4(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
3007
+ const silence = root.silence === void 0 ? {} : object4(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
3008
+ const black = root.black === void 0 ? {} : object4(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
3009
+ const peak = root.peak === void 0 ? {} : object4(root.peak, "peak", ["enabled", "thresholdDbFS"]);
3010
+ const limits = root.limits === void 0 ? {} : object4(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
2267
3011
  const number = (v, name, min, max, fallback, integer = false) => {
2268
3012
  if (v === void 0 && fallback !== void 0) return fallback;
2269
3013
  if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max || integer && !Number.isInteger(v)) invalidProfile(`${name} must be ${integer ? "an integer" : "a finite number"} between ${min} and ${max}.`);
@@ -2355,11 +3099,11 @@ function readDetectorOutput(report, stderr, audioStreams) {
2355
3099
  const match = /black_start:([\d.e+-]+)\s+black_end:([\d.e+-]+)\s+black_duration:([\d.e+-]+)/i.exec(line);
2356
3100
  if (match && match.slice(1).every((value) => Number.isFinite(Number(value)))) addFinding(report, { kind: "black_candidate", streamIndex, startSeconds: Number(match[1]), endSeconds: Number(match[2]), durationSeconds: Number(match[3]), requiresReview: true, message: "A mostly black interval met the configured detector threshold. It may be intentional; review in context." });
2357
3101
  } else if (tag[1] === "silencedetect@a") {
2358
- const start = /silence_start:\s*([\d.e+-]+)/i.exec(line);
2359
- if (start && Number.isFinite(Number(start[1]))) pendingSilence.set(streamIndex, Number(start[1]));
2360
- const end = /silence_end:\s*([\d.e+-]+)\s*\|\s*silence_duration:\s*([\d.e+-]+)/i.exec(line);
2361
- if (end && end.slice(1).every((value) => Number.isFinite(Number(value)))) {
2362
- addFinding(report, { kind: "silence_candidate", streamIndex, startSeconds: pendingSilence.get(streamIndex) ?? Number(end[1]) - Number(end[2]), endSeconds: Number(end[1]), durationSeconds: Number(end[2]), requiresReview: true, message: "A quiet interval met the configured detector threshold. Pauses and silent edits may be intentional." });
3102
+ const start2 = /silence_start:\s*([\d.e+-]+)/i.exec(line);
3103
+ if (start2 && Number.isFinite(Number(start2[1]))) pendingSilence.set(streamIndex, Number(start2[1]));
3104
+ const end2 = /silence_end:\s*([\d.e+-]+)\s*\|\s*silence_duration:\s*([\d.e+-]+)/i.exec(line);
3105
+ if (end2 && end2.slice(1).every((value) => Number.isFinite(Number(value)))) {
3106
+ addFinding(report, { kind: "silence_candidate", streamIndex, startSeconds: pendingSilence.get(streamIndex) ?? Number(end2[1]) - Number(end2[2]), endSeconds: Number(end2[1]), durationSeconds: Number(end2[2]), requiresReview: true, message: "A quiet interval met the configured detector threshold. Pauses and silent edits may be intentional." });
2363
3107
  pendingSilence.delete(streamIndex);
2364
3108
  }
2365
3109
  } else {
@@ -2579,6 +3323,97 @@ function summarizeProductionReport(value) {
2579
3323
  throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
2580
3324
  }
2581
3325
 
3326
+ // packages/cli/src/integrations.ts
3327
+ import { constants as constants7 } from "node:fs";
3328
+ import { open as open7 } from "node:fs/promises";
3329
+ function object5(value) {
3330
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3331
+ }
3332
+ function invalidState(write) {
3333
+ return new CliError(
3334
+ "INVALID_RESPONSE",
3335
+ write ? "The integration write could not be confirmed from the API response." : "The API returned invalid integration state.",
3336
+ write ? "The write may have committed. Run editorbrain integrations list for this endpoint and reconcile remote state before another write." : "Run editorbrain integrations list again for this endpoint; check the service if the response remains invalid.",
3337
+ false,
3338
+ { outcome: write ? "unknown" : "read_failed" }
3339
+ );
3340
+ }
3341
+ function integrationState(value, write = false) {
3342
+ if (!object5(value) || !["member", "connection"].includes(value.scope) || !Number.isSafeInteger(value.revision) || value.revision < 0 || !(value.updatedAt === null || typeof value.updatedAt === "string" && value.updatedAt.length <= 64 && Number.isFinite(Date.parse(value.updatedAt))) || !Array.isArray(value.entries) || !Array.isArray(value.catalog)) throw invalidState(write);
3343
+ const providers = /* @__PURE__ */ new Set();
3344
+ for (const entry of value.entries) {
3345
+ if (!object5(entry) || typeof entry.providerId !== "string" || !entry.providerId || entry.providerId.length > 120 || providers.has(entry.providerId) || typeof entry.enabled !== "boolean" || typeof entry.accessMethod !== "string" || !entry.accessMethod || entry.accessMethod.length > 64 || !object5(entry.preferences)) throw invalidState(write);
3346
+ providers.add(entry.providerId);
3347
+ }
3348
+ if (value.catalog.some((provider) => !object5(provider) || typeof provider.id !== "string" || !provider.id)) throw invalidState(write);
3349
+ return {
3350
+ scope: value.scope,
3351
+ revision: value.revision,
3352
+ updatedAt: value.updatedAt,
3353
+ entries: value.entries,
3354
+ catalog: value.catalog
3355
+ };
3356
+ }
3357
+ async function readIntegrationSettings(file) {
3358
+ const handle = await open7(file, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
3359
+ try {
3360
+ const stat = await handle.stat();
3361
+ if (!stat.isFile() || stat.size > 16 * 1024) throw new CliError("INVALID_ARGUMENT", "Choose a regular integration settings JSON file under 16 KiB.", "Store provider credentials in the provider connection, not this file.");
3362
+ try {
3363
+ return JSON.parse(await handle.readFile("utf8"));
3364
+ } catch {
3365
+ throw new CliError("INVALID_ARGUMENT", "Integration settings must be valid JSON.", "The file should contain enabled, accessMethod and preferences only.");
3366
+ }
3367
+ } finally {
3368
+ await handle.close();
3369
+ }
3370
+ }
3371
+ function integrationRevision(value) {
3372
+ if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value) || !Number.isSafeInteger(Number(value))) {
3373
+ throw new CliError("INVALID_ARGUMENT", "--expected-revision must be the revision returned by integrations list.", "Read current settings before changing them.");
3374
+ }
3375
+ return Number(value);
3376
+ }
3377
+ async function listIntegrations(client, details = false) {
3378
+ const state = integrationState(await client.request("/v1/integrations"));
3379
+ if (details) return state;
3380
+ return {
3381
+ scope: state.scope,
3382
+ revision: state.revision,
3383
+ updatedAt: state.updatedAt,
3384
+ entries: state.entries,
3385
+ access: "user_declared",
3386
+ notice: "Verify provider access in this agent session before use. Settings do not connect tools or authorize charges."
3387
+ };
3388
+ }
3389
+ async function setIntegration(client, providerId, settings, expectedRevision) {
3390
+ let update;
3391
+ try {
3392
+ if (!settings || typeof settings !== "object" || Array.isArray(settings) || "expectedRevision" in settings) throw new Error();
3393
+ update = validateIntegrationUpdate(providerId, { ...settings, expectedRevision });
3394
+ } catch {
3395
+ throw new CliError("INVALID_ARGUMENT", "Invalid integration preferences, access method or provider.", "Use integrations catalog for supported providers. The JSON accepts enabled, accessMethod, preferences; never credentials.");
3396
+ }
3397
+ let response;
3398
+ try {
3399
+ response = await client.request(`/v1/integrations/${encodeURIComponent(providerId)}`, { method: "PUT", body: update });
3400
+ } catch (error) {
3401
+ if (error instanceof ApiError && error.status === 409) throw new CliError(
3402
+ "INTEGRATION_CONFLICT",
3403
+ "Integration preferences changed before this save.",
3404
+ "Run editorbrain integrations list for this endpoint, reconcile the preferences and use the returned revision. Do not retry the old write blindly.",
3405
+ false,
3406
+ { status: 409 }
3407
+ );
3408
+ if (error instanceof CliError && ["INVALID_RESPONSE", "RESPONSE_TOO_LARGE"].includes(error.code)) throw invalidState(true);
3409
+ throw error;
3410
+ }
3411
+ const state = integrationState(response, true);
3412
+ const entry = state.entries.find((value) => value.providerId === providerId);
3413
+ if (state.revision !== expectedRevision + 1 || !entry || entry.enabled !== update.enabled || entry.accessMethod !== update.accessMethod || stableJson(entry.preferences) !== stableJson(update.preferences)) throw invalidState(true);
3414
+ return { scope: state.scope, revision: state.revision, updatedAt: state.updatedAt, entry, access: "user_declared" };
3415
+ }
3416
+
2582
3417
  // packages/cli/src/index.ts
2583
3418
  var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
2584
3419
 
@@ -2610,7 +3445,10 @@ editorbrain improvements accept RUN --index N --expected-revision N
2610
3445
  editorbrain review list --project ID
2611
3446
  editorbrain review create --project ID --asset SHA --title TEXT --kind sample|render --days 7 --share-authorized
2612
3447
  editorbrain review revoke ID
2613
- editorbrain workflow check [--project id] [--previous-version hash]
3448
+ editorbrain workflow check [--project id] [--previous-version hash] [--previous-context-version hash]
3449
+ editorbrain integrations catalog
3450
+ editorbrain integrations list [--details]
3451
+ editorbrain integrations set <provider-id> --file settings.json --expected-revision N
2614
3452
  editorbrain search brief --file <editorial-brief.json> [--project id] [--mode styles|events|both] [--review direct|directions|sample]
2615
3453
  editorbrain search edit [--script file | --video file | --asset sha256 | --project id] [--requirements text] [--assets tag,tag] [--mode styles|events|both] [--review direct|directions|sample]
2616
3454
  editorbrain library search [query]
@@ -2621,6 +3459,8 @@ editorbrain narrative get <video-id> [--sha256 document-hash] [--event-offset 0]
2621
3459
  editorbrain styles list
2622
3460
  editorbrain styles create <style-id> [--name name] [--dir folder]
2623
3461
  editorbrain styles check <style-id> [--dir folder]
3462
+ editorbrain media styles [--category image|motion]
3463
+ editorbrain media style <style-id> [--style-version 1.0.0]
2624
3464
  editorbrain media inventory --input file-or-folder [--max-files 200] [--dir folder] [--out .editorbrain-local/report.json] [--details]
2625
3465
  editorbrain media sample --file source --at 0,5,12 --out .editorbrain-local/frames [--dir folder] [--details]
2626
3466
  editorbrain production check --file render [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/report.json] [--details]
@@ -2641,6 +3481,7 @@ Push/pull preserve conflicting changes; media uploads use the separate assets co
2641
3481
  Skill install works offline, preserves existing instructions, and defaults to both agents.
2642
3482
  Styles create/check work locally without login or media upload; checks do not certify render quality.
2643
3483
  Media/production commands run locally with ffprobe/ffmpeg, without automatic installation or media upload.
3484
+ Integrations store personal preferences, not credentials. Declared access is checked by your agent before use.
2644
3485
  Production output is compact by default; --details expands it. --out saves full private reports without overwriting.
2645
3486
  Keys create stores the returned secret in the private user config; stdout has no key.
2646
3487
  Output is JSON except help; login progress/code goes to stderr. --url overrides the configured endpoint.
@@ -2649,7 +3490,7 @@ function parseArgs(argv) {
2649
3490
  const positional = [];
2650
3491
  const flags = {};
2651
3492
  const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details"]);
2652
- const known = /* @__PURE__ */ new Set(["input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
3493
+ const known = /* @__PURE__ */ new Set(["input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "previous-context-version", "category", "style-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
2653
3494
  for (let i = 0; i < argv.length; i++) {
2654
3495
  const arg = argv[i];
2655
3496
  if (!arg.startsWith("--")) {
@@ -2807,7 +3648,39 @@ async function main() {
2807
3648
  process.stdout.write(HELP);
2808
3649
  return;
2809
3650
  }
2810
- if (args.flags.details && !["media", "production"].includes(command)) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media and production.", "Read editorbrain help.");
3651
+ if (args.flags.details && !["media", "production", "integrations"].includes(command)) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media, production and integrations list.", "Read editorbrain help.");
3652
+ if (command === "integrations") {
3653
+ const allowed = new Set(subcommand === "catalog" ? [] : subcommand === "list" ? ["url", "details"] : subcommand === "set" ? ["url", "file", "expected-revision", "dir"] : []);
3654
+ if (!["catalog", "list", "set"].includes(subcommand) || args.positional.length > (subcommand === "set" ? 3 : 2)) throw new CliError("INVALID_ARGUMENT", "Use integrations catalog, list or set <provider-id>.", "Read editorbrain help.");
3655
+ for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for this integrations command.`, "Read editorbrain help.");
3656
+ if (subcommand === "catalog") {
3657
+ output({ catalog: integrationCatalog, credentialsRequired: false });
3658
+ return;
3659
+ }
3660
+ if (subcommand === "set") {
3661
+ const file = str(args, "file");
3662
+ if (!argument || !file) throw new CliError("INVALID_ARGUMENT", "integrations set requires a provider ID and --file.", "Use integrations list first, then supply --expected-revision.");
3663
+ const revision = integrationRevision(str(args, "expected-revision"));
3664
+ const settings = await readIntegrationSettings(resolve6(str(args, "dir") ?? process.cwd(), file));
3665
+ output(await setIntegration(new Client(await config(args)), argument, settings, revision));
3666
+ } else output(await listIntegrations(new Client(await config(args)), !!args.flags.details));
3667
+ return;
3668
+ }
3669
+ if (command === "media" && (subcommand === "styles" || subcommand === "style")) {
3670
+ const allowed = new Set(subcommand === "styles" ? ["category"] : ["style-version"]);
3671
+ for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported.`, "Read editorbrain help.");
3672
+ if (subcommand === "styles") {
3673
+ const category = str(args, "category");
3674
+ if (argument || category && !["image", "motion"].includes(category)) throw new CliError("INVALID_ARGUMENT", "Use --category image or motion.", "Read editorbrain media styles.");
3675
+ output({ id: MEDIA_STYLE_CATALOG.id, version: MEDIA_STYLE_CATALOG.version, styles: MEDIA_STYLE_CATALOG.styles.filter((s) => !category || s.category === category).map(({ id, version, category: category2, name, description, status }) => ({ id, version, category: category2, name, description, status })), next: "media style <id> --style-version <version>" });
3676
+ } else {
3677
+ if (!argument) throw new CliError("INVALID_ARGUMENT", "A media style id is required.", "Use editorbrain media styles.");
3678
+ const style = getMediaStyle(argument, str(args, "style-version"));
3679
+ if (!style) throw new CliError("NOT_FOUND", "This media style or version is not bundled with this CLI.", "Check the id/version or update EditorBrain; do not substitute another version silently.");
3680
+ output(style);
3681
+ }
3682
+ return;
3683
+ }
2811
3684
  if (command === "media" || command === "production") {
2812
3685
  const required = (name) => {
2813
3686
  const value = str(args, name);
@@ -3049,7 +3922,7 @@ async function main() {
3049
3922
  }
3050
3923
  if (subcommand === "upload" && argument) {
3051
3924
  const target = resolve6(str(args, "dir") ?? process.cwd(), argument);
3052
- const handle = await open7(target, constants7.O_RDONLY | constants7.O_NOFOLLOW);
3925
+ const handle = await open8(target, constants8.O_RDONLY | constants8.O_NOFOLLOW);
3053
3926
  let bytes;
3054
3927
  try {
3055
3928
  const info = await handle.stat();
@@ -3100,13 +3973,13 @@ async function main() {
3100
3973
  }
3101
3974
  if (command === "workflow" && subcommand === "check") {
3102
3975
  const client = new Client(await config(args));
3103
- output(await client.request("/v1/workflow/check", { method: "POST", body: { projectId: str(args, "project"), previousVersion: str(args, "previous-version") } }));
3976
+ output(await client.request("/v1/workflow/check", { method: "POST", body: { projectId: str(args, "project"), previousVersion: str(args, "previous-version"), previousContextVersion: str(args, "previous-context-version") } }));
3104
3977
  return;
3105
3978
  }
3106
3979
  if (command === "search" && subcommand === "brief") {
3107
3980
  const path = str(args, "file");
3108
3981
  if (!path) throw new Error("Provide --file with the editorial brief JSON after inspecting the actual material.");
3109
- const handle = await open7(resolve6(path), constants7.O_RDONLY | constants7.O_NOFOLLOW);
3982
+ const handle = await open8(resolve6(path), constants8.O_RDONLY | constants8.O_NOFOLLOW);
3110
3983
  let brief;
3111
3984
  try {
3112
3985
  const stat = await handle.stat();
@@ -3128,7 +4001,7 @@ async function main() {
3128
4001
  const scriptPath = str(args, "script"), videoPath = str(args, "video");
3129
4002
  let script, assetId = str(args, "asset");
3130
4003
  const readInput = async (path, max) => {
3131
- const handle = await open7(resolve6(path), constants7.O_RDONLY | constants7.O_NOFOLLOW);
4004
+ const handle = await open8(resolve6(path), constants8.O_RDONLY | constants8.O_NOFOLLOW);
3132
4005
  try {
3133
4006
  const stat = await handle.stat();
3134
4007
  if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);