@nodaro/shared 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { scene3DInputAssetsForEngine, scene3DInputAssetsSchema } from "../scene3d-input-assets.js"
3
+ import { buildPro3DRenderSource } from "../pro-3d-render.js"
4
+
5
+ const asset = { id: "vehicle", revisionId: "00000000-0000-4000-8000-000000000010",
6
+ assetId: "00000000-0000-4000-8000-000000000011", label: "Vehicle" }
7
+
8
+ describe("immutable scene input selectors", () => {
9
+ it("requires unique bounded selectors and excludes receipts and URLs", () => {
10
+ expect(scene3DInputAssetsSchema.parse([asset])).toEqual([asset])
11
+ for (const value of [[{ ...asset, url: "https://example.com/model.glb" }], [{ ...asset, sha256: "a".repeat(64) }],
12
+ [{ ...asset, revisionId: "../other" }], [{ ...asset, label: "bad\nlabel" }],
13
+ [asset, { ...asset, id: "another" }], [asset, { ...asset, assetId: asset.revisionId }], Array(9).fill(asset)]) {
14
+ expect(scene3DInputAssetsSchema.safeParse(value).success).toBe(false)
15
+ }
16
+ })
17
+ it("refuses Basic imports instead of silently ignoring geometry", () => {
18
+ for (const engine of [undefined, "basic", "unknown"]) {
19
+ expect(() => scene3DInputAssetsForEngine([asset], engine)).toThrow("advanced scene engine")
20
+ }
21
+ expect(scene3DInputAssetsForEngine(undefined, undefined)).toEqual([])
22
+ expect(scene3DInputAssetsForEngine([asset], "blender-cloud")).toEqual([asset])
23
+ })
24
+ it("carries inputs on new Pro scenes while export keeps its existing revision", () => {
25
+ expect(buildPro3DRenderSource({ prompt: "Drive past the camera", inputAssets: [asset] }))
26
+ .toEqual({ ok: true, source: { kind: "prompt", prompt: "Drive past the camera", inputAssets: [asset] } })
27
+ expect(buildPro3DRenderSource({ sourceMode: "scene", revisionId: asset.revisionId, inputAssets: [asset] }))
28
+ .toEqual({ ok: true, source: { kind: "scene", revisionId: asset.revisionId } })
29
+ expect(buildPro3DRenderSource({ prompt: "Drive", inputAssets: [{ ...asset, url: "https://example.com" } as never] }).ok).toBe(false)
30
+ })
31
+ })
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { assertCanvasExecutionAllowed, requiresSequenceExecution, STUDIO_DEPENDENT_FRAMES_CAPABILITY } from "../sequence-execution"
3
+
4
+ describe("dependency execution boundary", () => {
5
+ it.each([
6
+ { requiredCapabilities: [STUDIO_DEPENDENT_FRAMES_CAPABILITY] },
7
+ { keyframeId: "A" }, { sequenceBinding: { startKeyframeId: "A", endKeyframeId: "B" } },
8
+ { keyframeId: null }, { sequenceBinding: null },
9
+ ])("refuses declared or recognizable dependency nodes: %j", (data) => {
10
+ expect(() => assertCanvasExecutionAllowed([{ id: "linked", data }])).toThrow(expect.objectContaining({
11
+ code: "sequence_execution_required", nodeIds: ["linked"],
12
+ }))
13
+ })
14
+ it("keeps ordinary nodes runnable and reports only the blocked nodes", () => {
15
+ expect(requiresSequenceExecution({ data: { provider: "wan-3", imageUrl: "start", endFrameUrl: "end" } })).toBe(false)
16
+ expect(() => assertCanvasExecutionAllowed([{ id: "ordinary", data: {} }])).not.toThrow()
17
+ expect(() => assertCanvasExecutionAllowed([{ id: "ordinary", data: {} }, { id: "linked", data: { keyframeId: "A" } }]))
18
+ .toThrow(expect.objectContaining({ nodeIds: ["linked"] }))
19
+ })
20
+ })
package/src/index.ts CHANGED
@@ -1072,4 +1072,6 @@ export {
1072
1072
  } from "./studio-transient.js"
1073
1073
 
1074
1074
  export * from "./scene3d-v2-edit.js"
1075
+ export { STUDIO_DEPENDENT_FRAMES_CAPABILITY, SequenceExecutionRequiredError, requiresSequenceExecution, assertCanvasExecutionAllowed } from "./sequence-execution"
1075
1076
  export * from "./scene3d-authoring-engine.js"
1077
+ export * from "./scene3d-input-assets.js"
@@ -34,6 +34,7 @@
34
34
  import { z } from "zod"
35
35
  import { SCENE3D_LIMITS, type Scene3DReference } from "./scene3d.js"
36
36
  import { scene3DAnyPlanSchema, type Scene3DPlan } from "./scene3d-v2-plan.js"
37
+ import { scene3DInputAssetsSchema, type Scene3DInputAsset } from "./scene3d-input-assets.js"
37
38
 
38
39
  /** Canvas/API/MCP node type. */
39
40
  export const PRO3D_RENDER_NODE_TYPE = "pro-3d-render"
@@ -140,6 +141,7 @@ export interface Pro3DRenderPromptSource {
140
141
  kind: "prompt"
141
142
  prompt: string
142
143
  references?: readonly Scene3DReference[]
144
+ inputAssets?: readonly Scene3DInputAsset[]
143
145
  }
144
146
 
145
147
  /**
@@ -384,6 +386,7 @@ export const pro3DRenderCoreOutputSchema = z
384
386
 
385
387
  /** What a canvas node / DAG builder holds before it can name a source. */
386
388
  export interface Pro3DRenderSourceInput {
389
+ inputAssets?: readonly Scene3DInputAsset[]
387
390
  /** `"scene"` selects the existing-revision path; anything else is a brief. */
388
391
  sourceMode?: string
389
392
  /** The brief, already resolved and affix-applied by the caller. */
@@ -431,9 +434,12 @@ export function buildPro3DRenderSource(input: Pro3DRenderSourceInput): Pro3DRend
431
434
  return { ok: false, message: "no brief — describe the scene, or wire a prompt in." }
432
435
  }
433
436
  const references = input.references ?? []
437
+ const parsedAssets = scene3DInputAssetsSchema.safeParse(input.inputAssets ?? [])
438
+ if (!parsedAssets.success) return { ok: false, message: "invalid scene input assets" }
434
439
  return {
435
440
  ok: true,
436
- source: { kind: "prompt", prompt, ...(references.length > 0 ? { references } : {}) },
441
+ source: { kind: "prompt", prompt, ...(references.length > 0 ? { references } : {}),
442
+ ...(parsedAssets.data.length ? { inputAssets: parsedAssets.data } : {}) },
437
443
  }
438
444
  }
439
445
 
@@ -0,0 +1,27 @@
1
+ import { z } from "zod"
2
+
3
+ /** Immutable 3D inputs are selected by revision and artifact, never by URL or caller receipt. */
4
+ export const scene3DInputAssetSchema = z.object({
5
+ id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/),
6
+ revisionId: z.uuid(),
7
+ assetId: z.uuid(),
8
+ label: z.string().min(1).max(64).regex(/^[^\u0000-\u001f]+$/).optional(),
9
+ }).strict()
10
+
11
+ export const scene3DInputAssetsSchema = z.array(scene3DInputAssetSchema).max(8).superRefine((values, ctx) => {
12
+ if (new Set(values.map(value => value.id)).size !== values.length ||
13
+ new Set(values.map(value => value.assetId)).size !== values.length) {
14
+ ctx.addIssue({ code: "custom", message: "Scene input assets must have unique identities" })
15
+ }
16
+ })
17
+
18
+ export type Scene3DInputAsset = z.infer<typeof scene3DInputAssetSchema>
19
+
20
+ /** Both workflow engines refuse unsupported imports before starting a paid Basic run. */
21
+ export function scene3DInputAssetsForEngine(value: unknown, engine: string | undefined): Scene3DInputAsset[] {
22
+ const assets = scene3DInputAssetsSchema.parse(value ?? [])
23
+ if (assets.length && engine !== "blender-cloud" && engine !== "blender-local") {
24
+ throw new Error("Imported 3D assets require an advanced scene engine")
25
+ }
26
+ return assets
27
+ }
@@ -0,0 +1,24 @@
1
+ /** Public workflow capability marker; creative planning stays in the Studio codec. */
2
+ export const STUDIO_DEPENDENT_FRAMES_CAPABILITY = "studio-dependent-frames-v1"
3
+
4
+ export class SequenceExecutionRequiredError extends Error {
5
+ readonly code = "sequence_execution_required"
6
+ readonly statusCode = 400
7
+ constructor(readonly nodeIds: readonly string[]) {
8
+ super("Generate linked frames and clips in Studio or through its production API so the reviewed images are used.")
9
+ this.name = "SequenceExecutionRequiredError"
10
+ }
11
+ }
12
+
13
+ /** A missing declaration must not make a recognizable dependency node runnable. */
14
+ export function requiresSequenceExecution(node: { data?: unknown }): boolean {
15
+ if (!node.data || typeof node.data !== "object" || Array.isArray(node.data)) return false
16
+ const data = node.data as Record<string, unknown>
17
+ return data.keyframeId !== undefined || data.sequenceBinding !== undefined
18
+ || (Array.isArray(data.requiredCapabilities) && data.requiredCapabilities.includes(STUDIO_DEPENDENT_FRAMES_CAPABILITY))
19
+ }
20
+
21
+ export function assertCanvasExecutionAllowed(nodes: readonly { id: string; data?: unknown }[]): void {
22
+ const blocked = nodes.filter(requiresSequenceExecution).map((node) => node.id)
23
+ if (blocked.length) throw new SequenceExecutionRequiredError(blocked)
24
+ }