@nodaro/shared 3.1.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.1.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,36 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { scene3DPlanV2Schema } from "../scene3d-v2-plan.js"
3
+ import { applyScene3DV2EditOperations } from "../scene3d-v2-edit.js"
4
+ import { computeScene3DPlanV2ContentHash, verifyScene3DPlanV2ContentHash } from "../scene3d-v2-resources.js"
5
+ import { planV2 } from "./scene3d-v2-fixtures.js"
6
+
7
+ describe("baked entity visibility", () => {
8
+ it("round-trips false without adding a default to existing revisions", async () => {
9
+ const original = planV2()
10
+ const hidden = { ...original, objects: original.objects.map(e => ({ ...e, visible: false })) }
11
+ expect(scene3DPlanV2Schema.parse(hidden).objects.every(e => e.visible === false)).toBe(true)
12
+ expect(scene3DPlanV2Schema.parse(original).objects.every(e => !("visible" in e))).toBe(true)
13
+ expect(await computeScene3DPlanV2ContentHash(hidden)).not.toBe(await computeScene3DPlanV2ContentHash(original))
14
+ expect(scene3DPlanV2Schema.safeParse({ ...hidden, objects: hidden.objects.map(e => ({ ...e, visible: "false" })) }).success).toBe(false)
15
+ })
16
+
17
+ it("keeps baked visibility when a temporary show override is added and removed", async () => {
18
+ const original = planV2()
19
+ const hidden = { ...original, overrides: [], objects: original.objects.map(e => ({ ...e, visible: e.id !== "e2" })) }
20
+ hidden.provenance = { ...hidden.provenance, contentHash: await computeScene3DPlanV2ContentHash(hidden) }
21
+ const shown = await applyScene3DV2EditOperations(hidden, [{ op: "set-override", override: {
22
+ kind: "entity-visibility", entityId: "e2", visible: true,
23
+ } }], { expectedRevisionId: hidden.revisionId })
24
+ expect(shown.ok).toBe(true)
25
+ if (!shown.ok) return
26
+ const override = shown.plan.overrides!.find(o => o.kind === "entity-visibility")!
27
+ const reset = await applyScene3DV2EditOperations(shown.plan, [{ op: "remove-override", overrideId: override.id }], {
28
+ expectedRevisionId: shown.plan.revisionId,
29
+ })
30
+ expect(reset.ok).toBe(true)
31
+ if (!reset.ok) return
32
+ expect(reset.plan.objects.find(e => e.id === "e2")?.visible).toBe(false)
33
+ expect(reset.plan.overrides).toEqual([])
34
+ expect(await verifyScene3DPlanV2ContentHash(reset.plan)).toBe(true)
35
+ })
36
+ })
@@ -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
+ }
package/src/scene3d-v2.ts CHANGED
@@ -314,6 +314,8 @@ export interface Scene3DEntityV2 {
314
314
  position?: Vec3
315
315
  rotation?: Vec3
316
316
  scale?: Vec3
317
+ /** Baked visibility before manual overlays. Absent means visible. */
318
+ visible?: boolean
317
319
  /** The selection/identity chip colour. Opaque hex, sRGB. */
318
320
  identityColor?: string
319
321
  anchors?: Scene3DAnchor[]
@@ -541,6 +543,7 @@ export const scene3DEntityV2Schema = z
541
543
  position: vec3Schema.optional(),
542
544
  rotation: rotationVec3Schema.optional(),
543
545
  scale: scaleVec3Schema.optional(),
546
+ visible: z.boolean().optional(),
544
547
  identityColor: scene3DColorSchema.optional(),
545
548
  anchors: z.array(scene3DAnchorSchema).max(SCENE3D_V2_LIMITS.maxAnchorsPerEntity).optional(),
546
549
  capabilities: z.array(scene3DEntityCapabilitySchema).max(SCENE3D_ENTITY_CAPABILITIES.length).optional(),
@@ -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
+ }