@nodaro/shared 3.8.0 → 3.10.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.
Files changed (53) hide show
  1. package/dist/index.cjs +325 -63
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +806 -41
  4. package/dist/index.d.ts +806 -41
  5. package/dist/index.js +295 -64
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/credit-identifiers.test.ts +51 -1
  9. package/src/__tests__/gvp-supported-providers.test.ts +20 -3
  10. package/src/__tests__/model-catalog-sections.test.ts +59 -0
  11. package/src/__tests__/model-tree.test.ts +5 -2
  12. package/src/__tests__/parameter-node-value.test.ts +35 -0
  13. package/src/__tests__/pricing-default-duration.test.ts +45 -0
  14. package/src/__tests__/prompt-length-limits.test.ts +4 -0
  15. package/src/__tests__/scene3d-delivery-review.test.ts +512 -0
  16. package/src/__tests__/suno-credit-type.test.ts +46 -0
  17. package/src/__tests__/template-categories.test.ts +45 -0
  18. package/src/__tests__/video-ref-limits.test.ts +11 -2
  19. package/src/credit-identifiers.ts +64 -14
  20. package/src/i18n/camera-motions.ar.ts +5 -5
  21. package/src/i18n/camera-motions.de.ts +5 -5
  22. package/src/i18n/camera-motions.es.ts +5 -5
  23. package/src/i18n/camera-motions.fr.ts +5 -5
  24. package/src/i18n/camera-motions.he.ts +5 -5
  25. package/src/i18n/camera-motions.hi.ts +5 -5
  26. package/src/i18n/camera-motions.ja.ts +5 -5
  27. package/src/i18n/camera-motions.ko.ts +5 -5
  28. package/src/i18n/camera-motions.pt-BR.ts +5 -5
  29. package/src/i18n/camera-motions.ru.ts +5 -5
  30. package/src/i18n/camera-motions.zh-CN.ts +5 -5
  31. package/src/i18n/character-motion.ar.ts +1031 -0
  32. package/src/i18n/character-motion.de.ts +1031 -0
  33. package/src/i18n/character-motion.es.ts +1031 -0
  34. package/src/i18n/character-motion.fr.ts +1031 -0
  35. package/src/i18n/character-motion.he.ts +1031 -0
  36. package/src/i18n/character-motion.hi.ts +1031 -0
  37. package/src/i18n/character-motion.ja.ts +1031 -0
  38. package/src/i18n/character-motion.ko.ts +1031 -0
  39. package/src/i18n/character-motion.pt-BR.ts +1031 -0
  40. package/src/i18n/character-motion.ru.ts +1031 -0
  41. package/src/i18n/character-motion.zh-CN.ts +1031 -0
  42. package/src/i18n/types.ts +1 -0
  43. package/src/index.ts +28 -0
  44. package/src/model-catalog.ts +88 -35
  45. package/src/model-constants.ts +118 -24
  46. package/src/node-execution-state.ts +95 -0
  47. package/src/parameter-node-value.ts +33 -0
  48. package/src/presentation-utils.ts +1 -0
  49. package/src/pro-3d-render.ts +159 -0
  50. package/src/scene3d-delivery-notes.ts +490 -0
  51. package/src/scene3d-v2-plan.ts +8 -2
  52. package/src/smart-cut-windows.ts +15 -10
  53. package/src/template-categories.ts +67 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * What a Scene3D authoring run says about its OWN answer, in the published result.
3
+ *
4
+ * An advanced authoring engine knows things about the scene it just made that nothing else can
5
+ * reconstruct afterwards: which parts of the brief it had to ASSUME, what it thinks it authored,
6
+ * what it spent getting there, and — when the scene was delivered without the visual reviewer's
7
+ * approval — what that reviewer wanted changed, or that it gave no usable verdict at all. All of it
8
+ * rides the completed job's `output_data`, in slots the delivery contract already had:
9
+ *
10
+ * | What | Where | Shape |
11
+ * |---|---|---|
12
+ * | the planner's assumptions, each with the engine's own normalizations prefixed | `validation.warnings[]` | one entry per assumption, `code` = {@link SCENE3D_AUTHORING_ASSUMPTION_CODE} |
13
+ * | the planner's one-or-two-sentence description of what it authored (on a repaired run, of the REPAIR) | `metadata.summary` | string |
14
+ * | repair passes actually RUN — never the authoring-pass count | `repairPasses` | top-level number |
15
+ * | pre-build planner retries that did NOT spend a repair pass | `admissionRetries` | top-level number |
16
+ * | repairs the engine applied from the compiler's OWN remedy, on their own quoted allowance | `mechanicalPasses` | top-level number, counted APART from `repairPasses` |
17
+ * | the per-remedy account of what one of those applied | `validation.warnings[]` | one entry per remedy, `code` = {@link SCENE3D_REMEDY_AUTO_APPLIED_CODE} |
18
+ * | mandatory assertions the engine put BACK after a planner answer re-shaped them | `restoredAssertions` | array of {@link Scene3DRestoredAssertion} |
19
+ * | the per-assertion account of one of those restores | `validation.warnings[]` | one entry per restore, `code` = {@link SCENE3D_ASSERTION_RESTORED_CODE} |
20
+ * | the visual reviewer's refusal of a scene that was delivered anyway | `metadata.review` + `validation.warnings[]` | {@link Scene3DReviewVerdict} with `verdict: "refused"`, plus one entry per objection coded {@link SCENE3D_REVIEW_REFUSED_CODE} |
21
+ * | that the review produced no usable verdict at all — its provider was never reached, or answered unusably — and the scene was delivered unreviewed | `metadata.review` + `validation.warnings[]` | {@link Scene3DReviewVerdict} with `verdict: "unavailable"` and a `reason` from {@link SCENE3D_REVIEW_UNAVAILABLE_REASONS}, plus one LEADING entry coded {@link SCENE3D_REVIEW_UNAVAILABLE_CODE} |
22
+ *
23
+ * Every one of them is OPTIONAL, and absent is a first-class answer:
24
+ *
25
+ * - the deterministic Basic lane authors nothing with a model, so it carries none of them;
26
+ * - a render-only export authored nothing either — it reports no `summary` and omits
27
+ * `repairPasses` entirely rather than claiming `0` about a run that never happened;
28
+ * - a run whose recipe was refused on every pass has no composition to describe, so it carries
29
+ * the assumptions and `repairPasses` but NO `summary` and no `metadata` block to hold one;
30
+ * what it DOES carry is {@link Scene3DAuthoringValidation.sourceRetained}, saying whether the
31
+ * recipe it was refused for is retrievable from its delivery;
32
+ * - `repairPasses` is `0`, not absent, on a run that was accepted first time;
33
+ * - `admissionRetries` is absent on a run that never had one, and counts only the pre-build
34
+ * planner retries — a slip the compiler would not admit, re-asked without spending a repair;
35
+ * - `mechanicalPasses` is counted APART from `repairPasses`, the same way `admissionRetries` is,
36
+ * because it buys a different thing and is BOUGHT differently: a mandatory finding that carries
37
+ * the compiler's own structured remedy is answered by applying that remedy and rebuilding, with
38
+ * no planner call, and those passes have their own quoted allowance — a `mechanical` line on the
39
+ * quote, released when unspent — rather than spending one of the caller's repairs. So they are
40
+ * never folded into `repairPasses`, and the pass identity the pricing keeps is
41
+ * `buildPasses === authoringPasses + repairPasses + mechanicalPasses`. Absent on a run that took
42
+ * none and on an engine that does not report it, so absent is never evidence that the planner
43
+ * authored every repair;
44
+ * - the ONE exception is a run quoted BEFORE that allowance existed. Its quote carries no
45
+ * `mechanical` line, and there the older accounting still holds: the pass charged a repair, so
46
+ * the count is a SUBSET of `repairPasses` rather than a sibling of it. The discriminant is the
47
+ * quote, not the result — the result reports the same field either way — so a reader that must
48
+ * know which accounting applies reads the quote it was given rather than inferring one;
49
+ * - `review` is present ONLY on a delivery the reviewer did not APPROVE (below) — one it refused,
50
+ * or one it never got to judge because its provider could not be reached. A scene the reviewer
51
+ * accepted carries no `review` at all — absent means "nothing to report", never "passed
52
+ * silently", and never "nobody looked".
53
+ *
54
+ * ## The advisory delivery
55
+ *
56
+ * The visual reviewer's refusal drives a repair for as long as the repair budget lasts. Once the
57
+ * budget is spent, a scene whose every MANDATORY assertion passed is delivered rather than
58
+ * withheld: the job completes, the video is real, and the refusal rides along as
59
+ * {@link Scene3DAuthoringDeliveryMetadata.review} plus one `SCENE_REVIEW_REFUSED` warning per
60
+ * objection. `validation.status` stays `"passed"` on such a result — the assertions DID pass —
61
+ * so the presence of `review` is the only thing that tells an advisory delivery apart from a
62
+ * clean one. A caller that wants the stricter reading tests for it explicitly.
63
+ *
64
+ * A refusal that raised NO objection is still a verdict, and still arrives: `objections: []` says
65
+ * the reviewer refused without naming anything actionable, which an absent field would hide.
66
+ *
67
+ * ## The UNREVIEWED delivery
68
+ *
69
+ * The second way a scene is delivered without approval: the review produced no usable verdict.
70
+ * That is the advisory situation with a different cause, and it differs in exactly one way — a
71
+ * repair answers an objection, and a missing opinion raises none, so waiting for the repair budget
72
+ * to run out buys nothing. The review is asked once more — after a bounded pause when its provider
73
+ * was unreachable, at once when it answered unusably, and not at all when a provider broke after it
74
+ * had already reported usage — and if there is still no usable answer, the assertion-passing scene
75
+ * is delivered immediately with `{ verdict: "unavailable", reason, attempts }` on {@link
76
+ * Scene3DAuthoringDeliveryMetadata.review}, and `validation.warnings[]` LEADS with one
77
+ * `SCENE_REVIEW_UNAVAILABLE` entry. `reason` is one of {@link SCENE3D_REVIEW_UNAVAILABLE_REASONS}:
78
+ * `"provider"` when no asking reached the provider, `"unusable"` when one did and answered with
79
+ * nothing usable. `attempts` is how many times the review was asked, so one unlucky call is
80
+ * distinguishable from a provider that was down for the whole minute.
81
+ *
82
+ * `objections` may still be non-empty on that arm. A review is BATCHED, and batches that answered
83
+ * before the provider went away are evidence a caller is entitled to; each arrives as a
84
+ * `SCENE_REVIEW_REFUSED` warning UNDER the leading one, which qualifies every line below it. A
85
+ * caller reading only `objections: []` would otherwise take the silence for approval.
86
+ *
87
+ * With the advisory policy off, that scene is RETAINED as a draft on a `failed` job instead of
88
+ * delivered. There is no `metadata.review` on that lane — a retained failure publishes no
89
+ * metadata block — and the `SCENE_REVIEW_UNAVAILABLE` warning is what says nobody judged it. The
90
+ * pinned validation report says `review: "unavailable"` on both lanes.
91
+ *
92
+ * Nothing here is new wire surface: the schemas that read these results are `.passthrough()`
93
+ * and the fields were already arriving. Declaring them is what makes them visible to SDK
94
+ * users, typed for a TypeScript caller, and documentable — instead of reachable only by
95
+ * casting the result to `Record<string, unknown>`.
96
+ */
97
+
98
+ /**
99
+ * The `code` on a `validation.warnings[]` entry that is an authoring ASSUMPTION rather than a
100
+ * reviewer finding — "the brief did not say, so the run decided". Consumers tell the two apart
101
+ * by this code alone, without knowing anything about the engine that produced them.
102
+ */
103
+ export const SCENE3D_AUTHORING_ASSUMPTION_CODE = "SCENE_AUTHORING_ASSUMPTION"
104
+
105
+ /** One advisory about a delivered scene. Same shape as `Pro3DRenderValidationWarning`. */
106
+ export interface Scene3DDeliveryWarning {
107
+ /**
108
+ * `SCENE_AUTHORING_ASSUMPTION` for an authoring caveat; `SCENE_REVIEW_REFUSED` for one
109
+ * objection the reviewer raised against a scene that was DELIVERED anyway;
110
+ * `SCENE_REVIEW_UNAVAILABLE` for a scene delivered or retained with NO reviewer verdict at all,
111
+ * because the review's provider never answered or answered unusably — it leads the array when it
112
+ * is there;
113
+ * `REMEDY_AUTO_APPLIED` for one remedy the engine applied itself on a mechanical repair;
114
+ * `ASSERTION_RESTORED` for one mandatory assertion it put back after a planner answer
115
+ * re-shaped it; a `SCENE_QUALITY_*` code for a reviewer finding on a job that FAILED.
116
+ * Open-ended on purpose — an unknown code is shown, never refused.
117
+ */
118
+ code: string
119
+ message: string
120
+ /** The shot the advisory is about, when it is about exactly one. */
121
+ shotId?: string
122
+ }
123
+
124
+ /**
125
+ * The validation block an authoring lane publishes.
126
+ *
127
+ * Indexed on purpose: the block carries more than `warnings` (status, the pinned report, the
128
+ * scope, and on a retained failure the pass count and phase), and a reader of THIS version
129
+ * must keep reading those as it always did rather than have them typed away.
130
+ */
131
+ export interface Scene3DAuthoringValidation {
132
+ /** Advisories about the delivered scene, including the authoring assumptions. */
133
+ warnings?: Scene3DDeliveryWarning[]
134
+ /**
135
+ * Whether a run that never COMPILED kept its last admitted recipe.
136
+ *
137
+ * Present only on the refused-authoring shape — a job that failed with no `scenePlan` and no
138
+ * `sceneRevisionId`, because the compiler refused the recipe on every pass. `true` means the
139
+ * recipe is retrievable: `GET /v1/3d-scene/deliveries/{deliveryId}` lists a `source-json`
140
+ * descriptor beside the refusal report, and its bytes come back from the delivery assets
141
+ * route, to a caller with `edit` on the job's workflow. `false` means no pass ever cleared
142
+ * admission, so there is no recipe to fetch — only the report.
143
+ *
144
+ * A row-level flag rather than only a descriptor, because it answers "is there anything to
145
+ * fetch" without a round trip, and answers it honestly when the caller's access would hide
146
+ * the descriptor anyway.
147
+ */
148
+ sourceRetained?: boolean
149
+ [key: string]: unknown
150
+ }
151
+
152
+ /** The composition metadata block, plus the planner's own account of what it authored. */
153
+ export interface Scene3DAuthoringDeliveryMetadata {
154
+ /**
155
+ * The planner's one-or-two-sentence description of the scene it authored — on a repaired
156
+ * run, of the repair it made. Routinely absent: a model that returns no summary is not an
157
+ * error, and a lane that compiled nothing has nothing to describe.
158
+ */
159
+ summary?: string
160
+ /**
161
+ * The visual reviewer's whole verdict, present ONLY on a delivery it did not approve — a scene
162
+ * whose mandatory assertions all passed, delivered either once the repair budget was spent and
163
+ * the reviewer still objected (`verdict: "refused"`), or without any verdict at all because the
164
+ * review's provider could not be reached (`verdict: "unavailable"`). Absent on every other
165
+ * result, including a clean one. Read it with {@link scene3DReviewVerdictOf} rather than by
166
+ * hand, and render it with {@link scene3DReviewNote}.
167
+ */
168
+ review?: Scene3DReviewVerdict
169
+ [key: string]: unknown
170
+ }
171
+
172
+ /**
173
+ * The additive fields every Scene3D AUTHORING delivery may carry. Mixed into the job output
174
+ * types rather than repeated, so a lane that grows one grows all of them.
175
+ */
176
+ export interface Scene3DAuthoringDelivery {
177
+ /** Advisories about this result. Absent on the deterministic Basic lane. */
178
+ validation?: Scene3DAuthoringValidation
179
+ /** Composition metadata, including the planner's `summary`. */
180
+ metadata?: Scene3DAuthoringDeliveryMetadata
181
+ /**
182
+ * Repair passes actually RUN, never the number of authoring passes: `0` on a scene accepted
183
+ * first time, absent on a lane that authored nothing.
184
+ */
185
+ repairPasses?: number
186
+ /**
187
+ * Pre-build planner retries, counted separately from {@link repairPasses} because they cost a
188
+ * different thing: a recipe the compiler would not ADMIT is re-asked of the planner without a
189
+ * build, so no repair pass was spent on it. Absent on a run that needed none and on a lane
190
+ * that authored nothing.
191
+ */
192
+ admissionRetries?: number
193
+ /**
194
+ * Repairs the engine authored ITSELF, from the compiler's own structured remedy, without asking
195
+ * the planner.
196
+ *
197
+ * Counted APART from {@link repairPasses} and never folded into it — the same way
198
+ * {@link admissionRetries} is — because these passes have their own quoted allowance: a
199
+ * `mechanical` line on the quote, bounded and released when unspent, rather than one of the
200
+ * caller's repairs. The pass identity the pricing keeps is
201
+ * `buildPasses === authoringPasses + repairPasses + mechanicalPasses`.
202
+ *
203
+ * ONE exception, and the discriminant is the QUOTE rather than this result: a run quoted before
204
+ * that allowance existed carries no `mechanical` quote line, and there the older accounting
205
+ * still holds — the pass charged a repair, so the count is a subset of {@link repairPasses}.
206
+ * The result reports the same field either way, so a reader that must know which accounting
207
+ * applies reads the quote it was given rather than inferring one from the counts.
208
+ *
209
+ * Absent on a run that took none, on a lane that authored nothing, and on an engine that does
210
+ * not report it — so absent means "not reported", never "the planner authored every repair".
211
+ */
212
+ mechanicalPasses?: number
213
+ /**
214
+ * Mandatory assertions the engine put BACK, each because a planner answer re-shaped one the
215
+ * feedback had not named.
216
+ *
217
+ * A repair is invited to change what the feedback names and nothing else. When an answer
218
+ * re-shapes a mandatory assertion outside that invitation, the engine restores it to the last
219
+ * admitted recipe's exact form and carries on, instead of refusing the answer and spending a
220
+ * retry to be told to put back a value it already held. An assertion the feedback DOES name is
221
+ * left alone — re-shaping one you were invited to re-shape is a disagreement, not a slip.
222
+ *
223
+ * Each entry is also one `ASSERTION_RESTORED` warning. Absent on a run that restored nothing
224
+ * and on an engine that does not report it; `[]` is possible and means the same thing.
225
+ */
226
+ restoredAssertions?: Scene3DRestoredAssertion[]
227
+ }
228
+
229
+ /**
230
+ * The `code` on a `validation.warnings[]` entry that is one objection the visual reviewer raised
231
+ * against a scene that was DELIVERED anyway.
232
+ *
233
+ * Distinct from the `SCENE_QUALITY_*` codes on purpose: those appear on a job that FAILED, where
234
+ * the finding is the reason there is no video. This one appears on a job that COMPLETED, where
235
+ * the video is real and the finding is advice about it. One entry per objection, carrying the
236
+ * `shotId` when every frame the objection cites falls inside one shot.
237
+ */
238
+ export const SCENE3D_REVIEW_REFUSED_CODE = "SCENE_REVIEW_REFUSED"
239
+
240
+ /**
241
+ * The `code` on the `validation.warnings[]` entry that says NOBODY reviewed this scene, because
242
+ * the review produced no usable verdict: its provider never answered, or answered unusably (the
243
+ * message says which, in the words {@link scene3DReviewNote} uses).
244
+ *
245
+ * Its own code rather than a {@link SCENE3D_REVIEW_REFUSED_CODE} with an apologetic message:
246
+ * "the reviewer objected to X" and "there is no reviewer verdict at all" are different facts
247
+ * about the scene a caller is holding, and only the second one is answered by running the job
248
+ * again later.
249
+ *
250
+ * It LEADS the warning array wherever it appears, because it qualifies every line under it — an
251
+ * objection below it came from a review that never finished, and a reader shown only the
252
+ * objection would take the silence on everything else for approval. It appears on the DELIVERED
253
+ * lane beside `metadata.review`, and on the RETAINED failure lane, which publishes no metadata
254
+ * block and where this warning is therefore the only thing that says nobody judged the draft.
255
+ */
256
+ export const SCENE3D_REVIEW_UNAVAILABLE_CODE = "SCENE_REVIEW_UNAVAILABLE"
257
+
258
+ /**
259
+ * The `code` on a `validation.warnings[]` entry that records one remedy the engine applied ITSELF
260
+ * — a mechanical repair, counted by {@link Scene3DAuthoringDelivery.mechanicalPasses}.
261
+ *
262
+ * Written down here for the same reason the other two codes are: so a consumer that wants to tell
263
+ * "the platform fixed this from the compiler's own instruction" apart from "the planner was asked"
264
+ * has one place to read the string from, rather than spelling it at each call site. The entry
265
+ * names the assertion that refused the build, the ops applied, and the measurement before them.
266
+ */
267
+ export const SCENE3D_REMEDY_AUTO_APPLIED_CODE = "REMEDY_AUTO_APPLIED"
268
+
269
+ /**
270
+ * The `code` on a `validation.warnings[]` entry that records one mandatory assertion the engine
271
+ * RESTORED after a planner answer re-shaped it without being asked to.
272
+ *
273
+ * Distinct from {@link SCENE3D_REMEDY_AUTO_APPLIED_CODE}: that one says the engine changed the
274
+ * SCENE to satisfy an assertion, this one says it changed the ANSWER back to leave an assertion
275
+ * as it was. Both appear on runs that completed normally — neither is a failure.
276
+ */
277
+ export const SCENE3D_ASSERTION_RESTORED_CODE = "ASSERTION_RESTORED"
278
+
279
+ /**
280
+ * One mandatory assertion put back to its last admitted form, with the edit that did it.
281
+ *
282
+ * Read tolerantly and declared loosely on purpose: `op` and `path` describe an edit in the
283
+ * engine's own vocabulary rather than a format this package pins, and `value` is whatever the
284
+ * restored assertion holds — a number, a string, an object — so it is `unknown` rather than
285
+ * narrowed to whatever today's assertions happen to use.
286
+ */
287
+ export interface Scene3DRestoredAssertion {
288
+ /** The edit applied to put it back, in the engine's vocabulary (e.g. `"replace"`). */
289
+ op: string
290
+ /** Where in the recipe it was put back. */
291
+ path: string
292
+ /** The restored value. Absent for an edit that carries none, such as a removal. */
293
+ value?: unknown
294
+ /** The assertion's own id, the same one the refusal would have named. */
295
+ assertionId: string
296
+ /** Why it was restored, in the engine's words. Open-ended; shown, never matched on. */
297
+ reason: string
298
+ }
299
+
300
+ /**
301
+ * One thing the visual reviewer wanted changed, with the frames it was looking at.
302
+ *
303
+ * There is no `severity` here because severity is the FILTER, not a field: only the reviewer's
304
+ * blocking findings become objections, so every entry in this list is blocking by construction.
305
+ */
306
+ export interface Scene3DReviewObjection {
307
+ /** The reviewer's own category for the finding, e.g. `"evidence"`. Open-ended. */
308
+ category: string
309
+ /** What is wrong — the finding's own description, never the reviewer's summary. */
310
+ what: string
311
+ /** The recipe-level change the reviewer asked for. Absent when it named none. */
312
+ correction?: string
313
+ /** The frames it cited, in the order the report gave them. Empty when it cited none. */
314
+ frames: number[]
315
+ }
316
+
317
+ /**
318
+ * What every verdict carries, whichever way the review ended.
319
+ *
320
+ * Shared by both arms of {@link Scene3DReviewVerdict} so a consumer that only wants the findings
321
+ * can take this type and stay indifferent to why the review did not approve the scene.
322
+ */
323
+ export interface Scene3DReviewFindings {
324
+ /**
325
+ * Every blocking finding across the run's reviews, de-duplicated. May be EMPTY: a refusal that
326
+ * named nothing actionable is still a refusal, and `[]` reports it honestly. On the
327
+ * `"unavailable"` arm it holds whichever review BATCHES answered usably before the one that did
328
+ * not, and is `[]` in the common case where the first batch is the one that failed.
329
+ */
330
+ objections: Scene3DReviewObjection[]
331
+ /** The reviewer's own account of what it found CORRECT. Never a substitute for an objection. */
332
+ observed?: string
333
+ }
334
+
335
+ /** The reviewer answered, and objected. {@link Scene3DReviewFindings.objections} is what it wants changed. */
336
+ export type Scene3DReviewRefused = { verdict: "refused" } & Scene3DReviewFindings
337
+
338
+ /**
339
+ * The review never produced an answer, so the scene was delivered with nobody's opinion on it.
340
+ *
341
+ * `objections` may still be non-empty here — see {@link Scene3DReviewFindings.objections} — which
342
+ * is exactly why this is a distinct verdict rather than a refusal with a flag: the findings that
343
+ * DID arrive are real, and the fact that they are not the whole verdict is the thing a caller
344
+ * must not lose.
345
+ */
346
+ export type Scene3DReviewUnavailable = {
347
+ verdict: "unavailable"
348
+ /**
349
+ * Why the verdict is missing — one of {@link SCENE3D_REVIEW_UNAVAILABLE_REASONS}. `"provider"`:
350
+ * the review never reached its provider. `"unusable"`: it was reached, and answered with nothing
351
+ * usable. The delivery is the same unreviewed scene either way; the reason changes the sentence
352
+ * ({@link scene3DReviewNote}), because "did not reach its provider" is untrue of a provider that
353
+ * answered.
354
+ */
355
+ reason: Scene3DReviewUnavailableReason
356
+ /**
357
+ * How many times the review was ASKED — the call plus its retry, so `2` on the common outage and
358
+ * on an unusable answer. `1` when a provider broke after it had already reported usage: that
359
+ * asking was paid, so it is not asked a second time. Always a positive integer.
360
+ */
361
+ attempts: number
362
+ } & Scene3DReviewFindings
363
+
364
+ /**
365
+ * Every cause a {@link Scene3DReviewUnavailable} verdict can name, written down ONCE.
366
+ *
367
+ * The reader schema (`pro3DRenderReviewVerdictSchema`), {@link scene3DReviewVerdictOf} and
368
+ * {@link scene3DReviewNote} all read this list rather than spelling their own, so a cause the
369
+ * engine adds is taught here and nowhere else. A wire value NOT in it is read as `"provider"` by
370
+ * both tolerant readers — the only cause an older engine emits — instead of discarding a verdict
371
+ * on a scene that was delivered and paid for.
372
+ *
373
+ * - `"provider"` — the review never reached its provider: an outage, or a call that broke before it
374
+ * produced an answer.
375
+ * - `"unusable"` — at least one asking REACHED the provider, and no asking got a usable answer
376
+ * back: an answer that failed the review contract, one that cited a frame the batch never
377
+ * supplied, or a host refusal that is not a transport fault.
378
+ */
379
+ export const SCENE3D_REVIEW_UNAVAILABLE_REASONS = ["provider", "unusable"] as const
380
+
381
+ /** One of {@link SCENE3D_REVIEW_UNAVAILABLE_REASONS}. */
382
+ export type Scene3DReviewUnavailableReason = (typeof SCENE3D_REVIEW_UNAVAILABLE_REASONS)[number]
383
+
384
+ /** Whether a wire value is a reason this package knows. The one guard both tolerant readers share. */
385
+ export function isScene3DReviewUnavailableReason(value: unknown): value is Scene3DReviewUnavailableReason {
386
+ return (SCENE3D_REVIEW_UNAVAILABLE_REASONS as readonly unknown[]).includes(value)
387
+ }
388
+
389
+ /**
390
+ * The visual reviewer's verdict on a scene that was delivered WITHOUT its approval.
391
+ *
392
+ * `verdict` is the discriminant, and it is the only one: an ACCEPTED review produces no verdict
393
+ * at all, because a result that carries this field is by definition one the reviewer did not
394
+ * approve. The two arms say WHY it did not — it objected, or it gave no usable answer — and a consumer
395
+ * that switches on `verdict` gets a compile-time answer for both.
396
+ *
397
+ * Deliberately NOT a second `status` field beside `verdict`: a caller written against the
398
+ * refused-only shape needs one new arm and no re-typing of the arm it already has.
399
+ */
400
+ export type Scene3DReviewVerdict = Scene3DReviewRefused | Scene3DReviewUnavailable
401
+
402
+ /**
403
+ * Whether a delivered result was published WITHOUT the visual reviewer's approval — accepted by
404
+ * every mandatory assertion, and then either refused by that reviewer or never judged by it.
405
+ *
406
+ * The one reader every surface should use, because the discriminant is easy to get wrong:
407
+ * `validation.status` is `"passed"` on such a result (the assertions did pass), and the warning
408
+ * array may legitimately hold zero `SCENE_REVIEW_REFUSED` entries for a refusal that raised no
409
+ * objection. The presence of the verdict is the only reliable test.
410
+ *
411
+ * Reading the `"unavailable"` arm is equally the point: a surface that checked
412
+ * `verdict === "refused"` and stopped would show NOTHING for an unreviewed delivery, which is
413
+ * indistinguishable from a clean one — the silence this whole module exists to prevent.
414
+ *
415
+ * Tolerant like the schemas that parse these results: an `attempts` that is not a positive
416
+ * integer is clamped to `1` rather than discarding a verdict that is otherwise well-formed. A
417
+ * verdict this cannot recognise at all returns `undefined`, which the caller reads as "no
418
+ * verdict" — the same answer an older engine gives.
419
+ */
420
+ export function scene3DReviewVerdictOf(output: unknown): Scene3DReviewVerdict | undefined {
421
+ const review = (output as { metadata?: { review?: unknown } } | null | undefined)?.metadata?.review
422
+ if (typeof review !== "object" || review === null) return undefined
423
+ const candidate = review as {
424
+ verdict?: unknown; objections?: unknown; observed?: unknown; reason?: unknown; attempts?: unknown
425
+ }
426
+ if (candidate.verdict !== "refused" && candidate.verdict !== "unavailable") return undefined
427
+ const objections = (Array.isArray(candidate.objections) ? candidate.objections : [])
428
+ .flatMap((entry): Scene3DReviewObjection[] => {
429
+ if (typeof entry !== "object" || entry === null) return []
430
+ const objection = entry as Record<string, unknown>
431
+ if (typeof objection.what !== "string" || !objection.what) return []
432
+ const frames = (Array.isArray(objection.frames) ? objection.frames : [])
433
+ .filter((frame): frame is number => Number.isSafeInteger(frame) && frame >= 0)
434
+ return [{
435
+ category: typeof objection.category === "string" && objection.category ? objection.category : "unsupported",
436
+ what: objection.what,
437
+ ...(typeof objection.correction === "string" && objection.correction ? { correction: objection.correction } : {}),
438
+ frames,
439
+ }]
440
+ })
441
+ const findings: Scene3DReviewFindings = {
442
+ objections,
443
+ ...(typeof candidate.observed === "string" && candidate.observed ? { observed: candidate.observed } : {}),
444
+ }
445
+ if (candidate.verdict === "refused") return { verdict: "refused", ...findings }
446
+ const attempts = typeof candidate.attempts === "number" && Number.isSafeInteger(candidate.attempts)
447
+ && candidate.attempts > 0 ? candidate.attempts : 1
448
+ // Kept when it is a cause this package knows, and read as `"provider"` otherwise — the same
449
+ // fallback the reader schema applies, so the two readers never disagree about one row.
450
+ const reason = isScene3DReviewUnavailableReason(candidate.reason) ? candidate.reason : "provider"
451
+ return { verdict: "unavailable", reason, attempts, ...findings }
452
+ }
453
+
454
+ /**
455
+ * One user-safe sentence for a verdict, so the same run is never described two different ways.
456
+ *
457
+ * Written down once here for the reason the codes above are: a surface that spells this itself
458
+ * gets it wrong on the arm it was not thinking about — a banner that reads "the reviewer refused
459
+ * this scene" for a scene NO reviewer ever saw is worse than no banner, because it invents an
460
+ * opinion. Callers that want their own wording still have the structured verdict; this is the
461
+ * default they do not have to write.
462
+ *
463
+ * Phrased for the DELIVERED lane, which is the only lane `metadata.review` appears on: the
464
+ * retained-draft lane publishes no metadata block, and says the same thing through its leading
465
+ * {@link SCENE3D_REVIEW_UNAVAILABLE_CODE} warning instead.
466
+ */
467
+ export function scene3DReviewNote(verdict: Scene3DReviewVerdict): string {
468
+ if (verdict.verdict === "unavailable") {
469
+ const asked = verdict.attempts === 1 ? "one attempt" : `${verdict.attempts} attempts`
470
+ const partial = verdict.objections.length
471
+ ? ` Part of the review did answer first, and its ${verdict.objections.length === 1
472
+ ? "one finding is" : `${verdict.objections.length} findings are`} listed — but they are not the whole verdict.`
473
+ : ""
474
+ // The reason picks the clause, and nothing else: "did not reach its provider" is untrue of a
475
+ // provider that answered, and the engine's own `SCENE_REVIEW_UNAVAILABLE` warning says the
476
+ // same run in these same words, so a banner and the warning under it never disagree.
477
+ const missing = verdict.reason === "unusable"
478
+ ? `returned no usable verdict in ${asked}`
479
+ : `did not reach its provider in ${asked}`
480
+ return `The scene built and every mandatory assertion passed, but the visual review ${missing}; `
481
+ + `it was delivered unreviewed.${partial}`
482
+ }
483
+ const count = verdict.objections.length
484
+ if (!count) {
485
+ return "The visual review refused this scene without naming anything to change; "
486
+ + "it was delivered anyway because every mandatory assertion passed."
487
+ }
488
+ return `The visual review refused this scene on ${count === 1 ? "one finding" : `${count} findings`}; `
489
+ + "it was delivered anyway because every mandatory assertion passed."
490
+ }
@@ -55,18 +55,24 @@ import {
55
55
  type Scene3DPlanV1,
56
56
  type Scene3DSemanticIssue,
57
57
  } from "./scene3d.js"
58
+ import type { Scene3DAuthoringDelivery } from "./scene3d-delivery-notes.js"
58
59
 
59
60
  /** THE plan type. Narrow with `isScene3DPlanV1` / `isScene3DPlanV2` before
60
61
  * reading version-specific fields. */
61
62
  export type Scene3DPlan = Scene3DPlanV1 | Scene3DPlanV2
62
63
 
63
- export interface Scene3DJobOutputV2 {
64
+ /**
65
+ * An advanced (engine-authored) scene job's output. The three
66
+ * {@link Scene3DAuthoringDelivery} fields are additive and optional — a result from a
67
+ * deployment without an advanced engine, or from one that predates them, simply has none.
68
+ */
69
+ export interface Scene3DJobOutputV2 extends Scene3DAuthoringDelivery {
64
70
  scenePlan: Scene3DPlanV2
65
71
  changeSummary?: string
66
72
  }
67
73
 
68
74
  /** Job output when either version may come back. */
69
- export interface Scene3DJobOutputAny {
75
+ export interface Scene3DJobOutputAny extends Scene3DAuthoringDelivery {
70
76
  scenePlan: Scene3DPlan
71
77
  changeSummary?: string
72
78
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Smart-cut SEARCH WINDOWS — the shared bound + clamp for
3
- * generate-video-pro's `smartCutFramesPrev` / `smartCutFramesNext`.
2
+ * Smart-cut SEARCH WINDOWS — the shared bound + clamp for the
3
+ * `smartCutFramesPrev` / `smartCutFramesNext` of BOTH smart-cut nodes:
4
+ * generate-video-pro (segment boundaries) and combine-videos (clip joins).
4
5
  *
5
6
  * They bound how much of each side of a boundary the engine considers when
6
7
  * it places the cut: N frames from the end of a segment and M from the start
@@ -8,16 +9,20 @@
8
9
  * behavior before they were exposed. A boundary the engine cannot resolve
9
10
  * inside the window falls back to the fixed freeze-trims; recast pins 24/24.
10
11
  *
11
- * Why a shared clamp: the canvas node (single-node Run) and the orchestrator
12
- * (workflow Run) are two independent send paths into the same engine route,
13
- * whose Zod schema rejects out-of-range values. Deriving both from this one
14
- * function means a stale or hand-edited node value degrades to a legal
15
- * request instead of 400-ing an entire multi-segment run at finalize time —
16
- * and the two paths cannot drift apart.
12
+ * Why a shared clamp: the config panel, the canvas node (single-node Run) and
13
+ * the orchestrator (workflow Run) are three independent writers/send paths into
14
+ * the same route, whose Zod schema REJECTS out-of-range values (its `.min()` /
15
+ * `.max()` are these very constants). Deriving all of them from this one
16
+ * function means a stale, imported or hand-edited node value degrades to a
17
+ * legal request instead of 400-ing an entire run — and the paths cannot drift
18
+ * apart. An `<input type="number" max={…}>` is not a substitute: the browser
19
+ * does not stop a typed or pasted value from reaching React's onChange.
17
20
  */
18
21
 
19
- /** Widest window the UI offers (the engine route itself accepts up to 48).
20
- * Past this, added frames only cost search time and invite a false match. */
22
+ /** Widest window we accept anywhere — the UI's `max`, the clamp's ceiling and
23
+ * `/v1/combine-videos`' own Zod `.max()` (the generate-video-pro engine route
24
+ * itself accepts up to 48). Past this, added frames only cost search time and
25
+ * invite a false match. */
21
26
  export const SMART_CUT_WINDOW_MAX = 24
22
27
  /** Narrowest meaningful window — one frame each side. */
23
28
  export const SMART_CUT_WINDOW_MIN = 1
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The use cases a marketplace template is filed under — exactly one each.
3
+ * Shared by the publish/browse routes (the `category` enum), the Templates
4
+ * page (tiles, URL filter, labels) and the publish dialog, so the list can
5
+ * only change in one place. Labels live with the frontend's i18n
6
+ * (`lib/template-categories.ts`), keyed by these values.
7
+ */
8
+ export const TEMPLATE_CATEGORIES = [
9
+ "product-imagery",
10
+ "static-ads",
11
+ "user-generated-content",
12
+ "video-ads",
13
+ "brand-assets",
14
+ "automations",
15
+ "campaign-concepts",
16
+ "social-creatives",
17
+ ] as const
18
+
19
+ export type TemplateCategory = (typeof TEMPLATE_CATEGORIES)[number]
20
+
21
+ /** Where a template with no usable category is filed: the plain "it is a workflow" bucket. */
22
+ export const DEFAULT_TEMPLATE_CATEGORY: TemplateCategory = "automations"
23
+
24
+ /**
25
+ * The categories templates were filed under before September 2026, each
26
+ * read as its new home. Migration 423 rewrites the rows with the same map;
27
+ * this keeps an old link (`?category=video-production`), a cached card and a
28
+ * row the migration has not reached yet resolving in the meantime.
29
+ */
30
+ export const LEGACY_TEMPLATE_CATEGORIES: Readonly<Record<string, TemplateCategory>> = {
31
+ "image-generation": "product-imagery",
32
+ "video-production": "video-ads",
33
+ "audio-music": "social-creatives",
34
+ "content-writing": "campaign-concepts",
35
+ "social-media": "social-creatives",
36
+ "data-processing": "automations",
37
+ "multi-step": "automations",
38
+ other: "automations",
39
+ }
40
+
41
+ const CURRENT = new Set<string>(TEMPLATE_CATEGORIES)
42
+
43
+ export function isTemplateCategory(value: unknown): value is TemplateCategory {
44
+ return typeof value === "string" && CURRENT.has(value)
45
+ }
46
+
47
+ /** A current category as is, a legacy one as its new home, anything else as undefined. */
48
+ export function resolveTemplateCategory(value: string | null | undefined): TemplateCategory | undefined {
49
+ if (value == null) return undefined
50
+ if (isTemplateCategory(value)) return value
51
+ return LEGACY_TEMPLATE_CATEGORIES[value]
52
+ }
53
+
54
+ /** The category a stored row reads as: resolved, else the default bucket. */
55
+ export function normalizeTemplateCategory(value: string | null | undefined): TemplateCategory {
56
+ return resolveTemplateCategory(value) ?? DEFAULT_TEMPLATE_CATEGORY
57
+ }
58
+
59
+ /**
60
+ * Every stored value that reads as `category` — the value itself and the
61
+ * legacy ones that map to it — so a database filter matches rows the
62
+ * migration has not rewritten yet.
63
+ */
64
+ export function templateCategoryStoredValues(category: TemplateCategory): string[] {
65
+ const legacy = Object.keys(LEGACY_TEMPLATE_CATEGORIES).filter((value) => LEGACY_TEMPLATE_CATEGORIES[value] === category)
66
+ return [category, ...legacy]
67
+ }