@nodaro/shared 2.8.0 → 2.11.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 (56) hide show
  1. package/dist/index.cjs +329 -53
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +631 -94
  4. package/dist/index.d.ts +631 -94
  5. package/dist/index.js +297 -53
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/catalog-projection.test.ts +14 -0
  9. package/src/__tests__/default-video-provider.test.ts +1 -1
  10. package/src/__tests__/llm-models.test.ts +3 -2
  11. package/src/__tests__/organizations-types.test.ts +61 -0
  12. package/src/__tests__/pack-sidecar-localization.test.ts +21 -0
  13. package/src/__tests__/parameter-node-value.test.ts +18 -1
  14. package/src/__tests__/producer-types.test.ts +11 -0
  15. package/src/__tests__/prompt-length-limits.test.ts +7 -3
  16. package/src/__tests__/resolve-pipeline-model.test.ts +4 -4
  17. package/src/__tests__/seedance2-continuation-ref.test.ts +2 -3
  18. package/src/__tests__/video-analysis-catalog-sync.test.ts +1 -1
  19. package/src/__tests__/video-analysis-pricing.test.ts +2 -2
  20. package/src/__tests__/video-mode-for-inputs.test.ts +74 -0
  21. package/src/animals.ts +10 -0
  22. package/src/catalog-projection.ts +48 -0
  23. package/src/combine-transitions.ts +38 -0
  24. package/src/credit-estimators/video-utils.ts +1 -1
  25. package/src/entity-node-fields.ts +147 -0
  26. package/src/featured-entities.ts +1 -1
  27. package/src/furniture.ts +10 -0
  28. package/src/i18n/index.ts +33 -3
  29. package/src/i18n/transitions.ar.ts +6 -0
  30. package/src/i18n/transitions.de.ts +6 -0
  31. package/src/i18n/transitions.es.ts +6 -0
  32. package/src/i18n/transitions.fr.ts +6 -0
  33. package/src/i18n/transitions.he.ts +6 -0
  34. package/src/i18n/transitions.hi.ts +6 -0
  35. package/src/i18n/transitions.ja.ts +6 -0
  36. package/src/i18n/transitions.ko.ts +6 -0
  37. package/src/i18n/transitions.pt-BR.ts +6 -0
  38. package/src/i18n/transitions.ru.ts +6 -0
  39. package/src/i18n/transitions.zh-CN.ts +6 -0
  40. package/src/i18n/types.ts +12 -14
  41. package/src/index.ts +30 -1
  42. package/src/llm-models.ts +4 -0
  43. package/src/model-catalog.ts +19 -0
  44. package/src/model-constants.ts +52 -7
  45. package/src/organizations/index.ts +2 -0
  46. package/src/organizations/types.ts +152 -0
  47. package/src/organizations/views.ts +220 -0
  48. package/src/parameter-node-value.ts +23 -3
  49. package/src/producer-types.ts +10 -0
  50. package/src/smart-cut-windows.ts +8 -17
  51. package/src/suno-track-sources.ts +23 -0
  52. package/src/surround.ts +10 -90
  53. package/src/vehicles.ts +11 -1
  54. package/src/video-analysis-pricing.ts +25 -36
  55. package/src/weapons.ts +11 -1
  56. package/src/workflow-export.ts +41 -0
package/src/surround.ts CHANGED
@@ -1,48 +1,25 @@
1
1
  /**
2
- * Surround continuation — shared single source of truth.
2
+ * Surround continuation — the shared WIRE CONTRACT.
3
3
  *
4
4
  * The Location 360° "look-around" builds each ring view (45°, 90°, …) as an
5
- * image-to-image continuation of the previous view. The platform forces
6
- * geometric continuity by handing the model a half-done frame: one edge holds
7
- * the previous view's carried pixels, the rest is flat gray, and the model is
8
- * asked to paint the gray region.
5
+ * image-to-image continuation of the previous one.
9
6
  *
10
- * This module owns the bits that are pure and reused across the route Zod
11
- * schema, the SDK input type, and the worker: the direction enum, the carried
12
- * fraction defaults, and the fill prompt. The geometry math + sharp compositing
13
- * + color harmonization live backend-side (they need `sharp`).
7
+ * This module owns only what the route Zod schema, the SDK input type, and the
8
+ * worker all need to agree on: the direction enum and the carried-fraction
9
+ * defaults. The fill prompt lives in `@nodaro/prompts` (never published) and
10
+ * the compositing/harmonization engine is private.
14
11
  */
15
12
 
16
13
  /**
17
- * The carry/paint axis for a continuation.
18
- *
19
- * PAN (horizontal — half-carry continuation):
20
- * - `right` — turning right: the new frame's LEFT edge continues the previous
21
- * view's RIGHT edge, so the carried band sits on the LEFT, painted on the RIGHT.
22
- * - `left` — turning left: the new frame's RIGHT edge continues the previous
23
- * view's LEFT edge, so the carried band sits on the RIGHT, painted on the LEFT.
24
- * (Mirror of `right`. Lets studio chain BOTH ways from a keyframe, capping
25
- * chain depth so quality doesn't compound down a long one-way chain.)
26
- *
27
- * TILT (vertical — thin-strip, subject-driven re-render):
28
- * - `up` — tilting straight up: render the open SKY overhead. A thin strip of
29
- * the establishing shot's TOP edge is carried into the new frame's BOTTOM for
30
- * a soft horizon transition; the rest is painted as sky (NOT a mirrored
31
- * landscape).
32
- * - `down` — tilting straight down: render the GROUND below. A thin strip of the
33
- * BOTTOM edge is carried into the new frame's TOP.
14
+ * The camera move a continuation represents: `right` / `left` pan the view
15
+ * horizontally, `up` / `down` tilt it vertically.
34
16
  */
35
17
  export const SURROUND_DIRECTIONS = ["right", "left", "up", "down"] as const
36
18
  export type SurroundDirection = (typeof SURROUND_DIRECTIONS)[number]
37
19
 
38
- /** Half the frame is carried for a horizontal pan (matches studio's composite). */
20
+ /** Default carried fraction for a horizontal pan. */
39
21
  export const DEFAULT_CARRIED_FRACTION = 0.5
40
- /**
41
- * Tilts carry only a thin horizon strip. Carrying half of a horizontal frame is
42
- * exactly what makes the model echo/mirror the landscape vertically instead of
43
- * rendering what's actually overhead/underfoot — so tilts keep the carry small
44
- * and let the tilt prompt drive the subject.
45
- */
22
+ /** Default carried fraction for a vertical tilt. */
46
23
  export const TILT_CARRIED_FRACTION = 0.12
47
24
 
48
25
  /** True for the vertical tilt directions (up/down), false for the pans. */
@@ -54,60 +31,3 @@ export function isTiltDirection(direction: SurroundDirection): boolean {
54
31
  export function defaultCarriedFraction(direction: SurroundDirection): number {
55
32
  return isTiltDirection(direction) ? TILT_CARRIED_FRACTION : DEFAULT_CARRIED_FRACTION
56
33
  }
57
-
58
- /** Which edge of the NEW frame holds the carried pixels vs the painted region. */
59
- const EDGE: Record<SurroundDirection, { carried: string; painted: string }> = {
60
- right: { carried: "left", painted: "right" },
61
- left: { carried: "right", painted: "left" },
62
- up: { carried: "bottom", painted: "top" },
63
- down: { carried: "top", painted: "bottom" },
64
- }
65
-
66
- /** What a tilt must actually render (NOT a continuation of the landscape). */
67
- const TILT_SUBJECT: Record<"up" | "down", { word: string; subject: string; where: string }> = {
68
- up: {
69
- word: "up",
70
- subject: "the open sky directly overhead — sky, clouds, or (for an interior) the canopy or ceiling",
71
- where: "overhead",
72
- },
73
- down: {
74
- word: "down",
75
- subject: "the ground directly below — terrain, floor, or water surface",
76
- where: "below",
77
- },
78
- }
79
-
80
- /**
81
- * Build the fill prompt the model receives alongside the half-carry composite.
82
- *
83
- * `userPrompt` (an optional scene hint from the caller) is woven in front. PAN
84
- * directions get the seamless-continuation prompt (with the anti-golden-hour
85
- * negative that fights the documented warm-regrade drift). TILT directions get a
86
- * subject-forcing prompt — render the sky / ground overhead / below, explicitly
87
- * NOT a mirrored landscape — which is what stops the vertical echo.
88
- */
89
- export function buildSurroundFillPrompt(direction: SurroundDirection, userPrompt?: string): string {
90
- const scene = userPrompt && userPrompt.trim() ? `${userPrompt.trim()}. ` : ""
91
- const { carried, painted } = EDGE[direction]
92
-
93
- if (direction === "up" || direction === "down") {
94
- const t = TILT_SUBJECT[direction]
95
- return (
96
- `${scene}` +
97
- `This is a camera tilted straight ${t.word} from the same scene. The ${carried} strip holds real, finished pixels from the edge of the horizon view; the ${painted} region is flat gray and MUST be painted as ${t.subject}. ` +
98
- `Render what is genuinely ${t.where} — do NOT repeat, mirror, or continue the landscape, and do NOT draw a horizon line or distant scenery in the painted region. ` +
99
- `CRITICAL: keep the ${carried} strip unchanged and match the scene's EXACT lighting, time of day, white balance, and color grade — the same light as the ${carried} strip; no golden hour, no sunset, no warm relight, no cinematic regrade. ` +
100
- `Blend smoothly into the ${carried} strip with no visible seam. No people, no text, no labels, no watermarks.`
101
- )
102
- }
103
-
104
- // pan (right / left)
105
- return (
106
- `${scene}` +
107
- `This is a partial frame: the ${carried} portion contains real, finished pixels and the ${painted} portion is flat gray that MUST be painted in. ` +
108
- `Paint ONLY the ${painted} gray region as a natural, seamless continuation of the ${carried} portion — same scene, same perspective, continuing the horizon, geometry, and content across the boundary with no break. ` +
109
- `Keep the ${carried} portion completely unchanged. ` +
110
- `CRITICAL: do NOT change the lighting, exposure, white balance, or time of day. Match the ${carried} portion's EXACT light, color temperature, and contrast across the whole frame — if it is flat overcast daylight, keep flat overcast daylight. No golden hour, no sunset, no warm relight, no cinematic regrade. ` +
111
- `The seam between the ${carried} and ${painted} portions must be invisible. No people, no text, no labels, no watermarks.`
112
- )
113
- }
package/src/vehicles.ts CHANGED
@@ -31,6 +31,16 @@ export interface Vehicle {
31
31
  readonly label: string
32
32
  readonly subcategory: VehicleSubcategory
33
33
  readonly description: string
34
+ /**
35
+ * Optional authored compact term — the short phrase a professional would
36
+ * write in a prompt, as opposed to the user-facing `label` (see the `term`
37
+ * convention in `@nodaro/prompts`'s `term.ts`). Authored ONLY where the
38
+ * lowercased label is not that phrase — a UI compound naming two things at
39
+ * once ("Airship / Dirigible" -> "airship", "Plasma Sword / Lightsaber" ->
40
+ * "plasma sword"). Everywhere else the lowercased label IS the term for a
41
+ * concrete object ("golden retriever", "katana"), so nothing is authored.
42
+ */
43
+ readonly term?: string
34
44
  }
35
45
 
36
46
  export const VEHICLES: ReadonlyArray<Vehicle> = [
@@ -117,7 +127,7 @@ export const VEHICLES: ReadonlyArray<Vehicle> = [
117
127
  { id: "seaplane", label: "Seaplane", subcategory: "aircraft", description: "Seaplane with twin pontoon floats instead of wheels, high wings and a propeller, resting on calm water" },
118
128
  { id: "hot-air-balloon", label: "Hot Air Balloon", subcategory: "aircraft", description: "Giant hot-air balloon with a colorful rainbow-striped fabric envelope, a roaring flame burner flaring upward into the mouth and a wicker passenger basket suspended below by braided cables" },
119
129
  { id: "blimp", label: "Blimp", subcategory: "aircraft", description: "Sausage-shaped blimp airship with a sleek silver envelope, small rear fins and a slung gondola beneath" },
120
- { id: "airship", label: "Airship / Dirigible", subcategory: "aircraft", description: "Massive rigid lighter-than-air dirigible with a long cigar-shaped fabric-covered metal frame, large rear stabilizer fins, multiple slung engine gondolas and a long passenger cabin running underneath the hull" },
130
+ { id: "airship", label: "Airship / Dirigible", subcategory: "aircraft", description: "Massive rigid lighter-than-air dirigible with a long cigar-shaped fabric-covered metal frame, large rear stabilizer fins, multiple slung engine gondolas and a long passenger cabin running underneath the hull", term: "airship" },
121
131
  { id: "glider", label: "Glider", subcategory: "aircraft", description: "Elegant sailplane glider with ultra-long narrow wings, no engine and a teardrop cockpit pod" },
122
132
  { id: "paraglider", label: "Paraglider", subcategory: "aircraft", description: "Foot-launched paraglider with a wide elliptical soft-fabric ram-air wing arched overhead, dozens of slender suspension lines converging into a tandem harness with a seated pilot" },
123
133
  { id: "microlight", label: "Microlight Aircraft", subcategory: "aircraft", description: "Lightweight single-pilot microlight aircraft with an exposed tubular metal frame, fabric-covered high wings, a small pusher propeller engine behind the open seat and tricycle landing gear" },
@@ -9,14 +9,12 @@
9
9
  * `buildVideoAnalysisCreditId` + `/v1/credits/model-cost`) all derive from
10
10
  * these.
11
11
  *
12
- * The measured-rate constants and the $-derived `videoAnalysisBucketCredits`
13
- * formula that GENERATE these numbers live PRIVATELY in the
14
- * `@nodaroai/cloud-plugins` package (`src/plugins/video-analysis/cost.ts`)
15
- * never in this public repo. They were first moved out of this package
16
- * (published Apache-2.0 on npm) per the 2026-07-06 public-flip IP audit S5,
17
- * then out of the app repo entirely alongside the rest of the video-analysis
18
- * node. A cross-check test in that private package guards this table so the
19
- * public numbers can't silently drift from the formula.
12
+ * The rate constants and the formula that GENERATE these numbers live
13
+ * PRIVATELY in the `@nodaroai/cloud-plugins` package never in this public
14
+ * repo. They were first moved out of this package (published Apache-2.0 on
15
+ * npm), then out of the app repo entirely alongside the rest of the
16
+ * video-analysis node. A cross-check test in that private package guards
17
+ * this table so the public numbers can't silently drift from the formula.
20
18
  *
21
19
  * `VIDEO_ANALYSIS_BUCKET_CREDITS` below is the precomputed OUTPUT of that
22
20
  * private formula for every (model × bucket) combination — a plain credit
@@ -24,9 +22,8 @@
24
22
  * `VIDEO_CLIP_CREDITS` uses in `film-pricing.ts`. It is what the frontend's
25
23
  * client-side cost preview (`estimateNodeCredits` in
26
24
  * workflow-editor/types.ts) reads instead of calling the formula directly.
27
- * The formula's own test in `@nodaroai/cloud-plugins`
28
- * (`src/plugins/video-analysis/__tests__/cost.test.ts`) cross-checks this table
29
- * against it and fails on drift. There is deliberately NO app-side formula to
25
+ * The formula's own test in `@nodaroai/cloud-plugins` cross-checks this
26
+ * table against it and fails on drift. There is deliberately NO app-side formula to
30
27
  * check against — it was moved private in 2026-07 and the old backend test
31
28
  * went with it.
32
29
  *
@@ -65,27 +62,20 @@ export const VIDEO_ANALYSIS_WINDOW = { LEN: WINDOW_LEN, STRIDE: WINDOW_STRIDE, O
65
62
  // not 110). The plugin's cost test now covers sentinels as well, so this class
66
63
  // of drift fails CI instead of shipping.
67
64
  // REGENERATED 2026-07-31 with the smart-tier re-base (formula inputs moved in
68
- // the plugin: sampling 24→6 fps after a 43-run measurement found 6 equal on
69
- // content and strictly better on cast stability; the system-prompt and
70
- // per-window output-token constants trued-up to directly measured values).
71
- // Net effect: smart drops 27–47% per bucket — the 24 fps token spend was also
72
- // partly paying for media tokens the provider clamped and never counted — and
73
- // the economy rows tick up 3–6% from the prompt-token true-up.
65
+ // the plugin: sampling 24→6 fps, which proved equal on content and better on
66
+ // cast stability; system-prompt and per-window output-token constants updated).
67
+ // Net effect: smart drops 27–47% per bucket, and the economy rows tick up 3–6%.
74
68
  //
75
- // REGENERATED 2026-08-03 — V1 hybrid-smart reprice (task A3), from the
76
- // plugin's own generator (`scripts/gen-va-buckets.mjs`) at
77
- // nodaroai/nodaro-cloud-plugins commit eef077d (branch fix/va-cost-trueup).
78
- // This is the V1 true-up of task P6's provisional judge/refine/frame-judge
79
- // constants, re-derived from a 2026-08-03 staging measurement and approved by
80
- // Tal (the constants themselves, like the rest of the $-derived formula, stay
81
- // private in the plugin repo never in this public package). `smart` is now
82
- // a HYBRID plan — one native 6fps skeleton pass plus 2 fast + 2 pro donor
83
- // rolls, always refined (`selectionMode` does not apply to `smart`; it always
84
- // refines) and every multi-roll tier now carries its own explicit
85
- // judge/refine terms instead of an implicit share of a single-pass budget.
86
- // This is the full, honest reprice Tal approved, including the economy tiers
87
- // (fast 33->185 @180s ends a below-cost combine exposure that existed at the
88
- // old price). Net effect, per bucket (every row rises):
69
+ // REGENERATED 2026-08-03 — V1 hybrid-smart reprice, from the plugin's own
70
+ // generator. This is the V1 true-up of the earlier provisional
71
+ // judge/refine/frame-judge constants (the constants themselves, like the rest
72
+ // of the formula, stay private in the plugin repo never in this public
73
+ // package). `smart` is now a multi-roll plan that always refines its merged
74
+ // result (`selectionMode` does not apply to it), and every multi-roll tier
75
+ // now carries its own explicit judge/refine terms instead of an implicit
76
+ // share of a single-pass budget.
77
+ // The economy tiers rise too (fast 33->185 @180s).
78
+ // Net effect, per bucket (every row rises):
89
79
  //
90
80
  // gemini-3-flash 60s 24->180 180s 33->185 360s 86-> 514 600s 143-> 846
91
81
  // gemini-3.6-flash 60s 65->203 180s 92->218 360s 237-> 598 600s 395-> 986
@@ -119,9 +109,8 @@ export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
119
109
  "video-analysis:mixed:180s": 289,
120
110
  "video-analysis:mixed:360s": 724,
121
111
  "video-analysis:mixed:600s": 1169,
122
- // SMART — the accuracy tier, and since the 2026-08-03 hybrid re-plan a
123
- // multi-roll plan like the others: one native 6fps skeleton pass plus 2
124
- // fast + 2 pro donor rolls, always refined (`selectionMode` does not apply
112
+ // SMART — the accuracy tier, and since the 2026-08-03 re-plan a multi-roll
113
+ // plan like the others, always refined (`selectionMode` does not apply
125
114
  // here — smart always refines; it never offers a cheaper "choose" path).
126
115
  // Priced above the economy tiers because it genuinely costs more to run;
127
116
  // the only tier whose accuracy is validated against a hand-counted edit
@@ -168,7 +157,7 @@ export function videoAnalysisNumWindows(bucketSec: number): number {
168
157
  * Precomputed credit cost for the `video-audit` node ("AI Audit") — the same
169
158
  * pattern as `VIDEO_ANALYSIS_BUCKET_CREDITS` above: the OUTPUT of the private
170
159
  * `videoAuditBucketCredits` formula in `@nodaroai/cloud-plugins`
171
- * (`src/plugins/video-analysis/cost.ts`), a plain lookup table never a
160
+ * (in the plugin repo), a plain lookup table never a
172
161
  * formula, cross-checked against that package's own cost test. Shares the
173
162
  * SAME duration-bucket ladder as video-analysis (`VIDEO_ANALYSIS_DURATION_BUCKETS`
174
163
  * / `pickVideoAnalysisBucket`) — the audit re-watches the same clip, so it
@@ -195,7 +184,7 @@ export function videoAnalysisNumWindows(bucketSec: number): number {
195
184
  * `buildVideoAnalysisCreditId`.
196
185
  *
197
186
  * Values pasted verbatim from the plugin generator's output
198
- * (`scripts/gen-va-buckets.mjs`) at `@nodaroai/cloud-plugins` v0.102.0
187
+ * (the plugin repo's bucket generator)
199
188
  * never hand computed. The plugin's cost test cross-checks every row.
200
189
  */
201
190
  export const VIDEO_AUDIT_BUCKET_CREDITS: Record<string, number> = {
package/src/weapons.ts CHANGED
@@ -30,6 +30,16 @@ export interface Weapon {
30
30
  readonly label: string
31
31
  readonly subcategory: WeaponSubcategory
32
32
  readonly description: string
33
+ /**
34
+ * Optional authored compact term — the short phrase a professional would
35
+ * write in a prompt, as opposed to the user-facing `label` (see the `term`
36
+ * convention in `@nodaro/prompts`'s `term.ts`). Authored ONLY where the
37
+ * lowercased label is not that phrase — a UI compound naming two things at
38
+ * once ("Airship / Dirigible" -> "airship", "Plasma Sword / Lightsaber" ->
39
+ * "plasma sword"). Everywhere else the lowercased label IS the term for a
40
+ * concrete object ("golden retriever", "katana"), so nothing is authored.
41
+ */
42
+ readonly term?: string
33
43
  }
34
44
 
35
45
  export const WEAPONS: ReadonlyArray<Weapon> = [
@@ -131,7 +141,7 @@ export const WEAPONS: ReadonlyArray<Weapon> = [
131
141
  { id: "phaser", label: "Phaser", subcategory: "sci-fi", description: "Sleek sci-fi phaser with a minimalist curved grip, glowing emitter tip and a smooth panel controlling intensity" },
132
142
  { id: "rail-gun", label: "Rail Gun", subcategory: "sci-fi", description: "Heavy electromagnetic rail gun with parallel metal rails, massive capacitors along the body and a glowing projectile chamber" },
133
143
  { id: "emp-grenade", label: "EMP Grenade", subcategory: "sci-fi", description: "Spherical electromagnetic pulse grenade with exposed coils, glowing blue indicator lights and a holographic arming dial" },
134
- { id: "plasma-sword", label: "Plasma Sword / Lightsaber", subcategory: "sci-fi", description: "Sci-fi energy blade with a metallic ridged hilt projecting a tall column of saturated plasma energy, surrounded by a bright halo of glowing light, casting colored reflections on the wielder and humming softly with contained power" },
144
+ { id: "plasma-sword", label: "Plasma Sword / Lightsaber", subcategory: "sci-fi", description: "Sci-fi energy blade with a metallic ridged hilt projecting a tall column of saturated plasma energy, surrounded by a bright halo of glowing light, casting colored reflections on the wielder and humming softly with contained power", term: "plasma sword" },
135
145
  { id: "gravity-gun", label: "Gravity Gun", subcategory: "sci-fi", description: "Sci-fi physics-manipulating ranged weapon with a chunky metallic body, three articulated prongs at the muzzle that crackle with blue gravitational energy, exposed power conduits along the barrel and a heavy two-handed grip with a glowing trigger assembly" },
136
146
 
137
147
  // -------------------- Fantasy / Magical --------------------
@@ -74,6 +74,45 @@ export interface WorkflowExportLocation {
74
74
  styleLock?: boolean | null
75
75
  }
76
76
 
77
+ /** A media URL referenced from a node's data, located by node + field path. */
78
+ export interface WorkflowMediaRef {
79
+ nodeId: string
80
+ nodeLabel?: string
81
+ /** Dot/bracket path inside `node.data`, e.g. `imageUrl` or `referenceImageUrls[1]`. */
82
+ field: string
83
+ url: string
84
+ }
85
+
86
+ /**
87
+ * Export-time portability analysis (#866). A bundle exported from a private
88
+ * host carries media URLs only that host can serve (`http://localhost:3000/
89
+ * storage/…`, a LAN address, a `.internal` name); imported anywhere else, the
90
+ * nodes fail at Run time with an opaque provider fetch error. The exporter
91
+ * lists those URLs here so the person exporting is told BEFORE sharing, and
92
+ * an importer can explain what will not load. Absent when every media URL is
93
+ * publicly routable.
94
+ */
95
+ export interface WorkflowPortability {
96
+ unreachableMedia: WorkflowMediaRef[]
97
+ }
98
+
99
+ /**
100
+ * What the importer did about the bundle's media (#866). Publicly reachable
101
+ * media that is not already on the importing instance's own storage is
102
+ * copied there (`rehosted`) so the workflow runs from local copies; media on
103
+ * a host the importer cannot reach is left as-is and listed (`unreachable`);
104
+ * anything declined for another reason (too large, not a media type, over
105
+ * the per-import cap, upload failed) is listed with the reason (`skipped`).
106
+ */
107
+ export interface WorkflowImportReport {
108
+ rehosted: number
109
+ unreachable: WorkflowMediaRef[]
110
+ skipped: Array<WorkflowMediaRef & { reason: string }>
111
+ /** Anything else the importer should know, e.g. copies were made but the
112
+ * workflow could not be updated to use them. */
113
+ notes?: string[]
114
+ }
115
+
77
116
  export interface WorkflowExport {
78
117
  version: 1
79
118
  exportedAt: string
@@ -87,6 +126,8 @@ export interface WorkflowExport {
87
126
  creatures?: WorkflowExportCreature[]
88
127
  locations: WorkflowExportLocation[]
89
128
  }
129
+ /** Present only when the bundle references media another instance cannot fetch. */
130
+ portability?: WorkflowPortability
90
131
  }
91
132
 
92
133
  /**