@nodaro/prompts 1.38.0 → 1.40.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 (130) hide show
  1. package/dist/chunk-2EW5B7ZV.cjs +86 -0
  2. package/dist/chunk-2EW5B7ZV.cjs.map +1 -0
  3. package/dist/chunk-2P26SZX5.cjs +414 -0
  4. package/dist/chunk-2P26SZX5.cjs.map +1 -0
  5. package/dist/{chunk-7XKHDXCL.cjs → chunk-3U7X6JZN.cjs} +13 -12
  6. package/dist/chunk-3U7X6JZN.cjs.map +1 -0
  7. package/dist/chunk-4GKTOVFM.cjs +8 -0
  8. package/dist/chunk-4GKTOVFM.cjs.map +1 -0
  9. package/dist/{chunk-HRBBNODV.js → chunk-6GVFJLDL.js} +4 -3
  10. package/dist/chunk-6GVFJLDL.js.map +1 -0
  11. package/dist/{chunk-LVSH7K7R.cjs → chunk-C3NHNECH.cjs} +165 -88
  12. package/dist/chunk-C3NHNECH.cjs.map +1 -0
  13. package/dist/{chunk-CEPVQQVO.js → chunk-C46256GZ.js} +43 -5
  14. package/dist/chunk-C46256GZ.js.map +1 -0
  15. package/dist/{chunk-JKCR6WRZ.cjs → chunk-DQFQJECU.cjs} +43 -5
  16. package/dist/chunk-DQFQJECU.cjs.map +1 -0
  17. package/dist/{chunk-QDEWBBUR.js → chunk-DUXX6CBX.js} +12 -143
  18. package/dist/chunk-DUXX6CBX.js.map +1 -0
  19. package/dist/chunk-EAYEZXWT.js +388 -0
  20. package/dist/chunk-EAYEZXWT.js.map +1 -0
  21. package/dist/chunk-GILUVQQJ.js +6 -0
  22. package/dist/chunk-GILUVQQJ.js.map +1 -0
  23. package/dist/{chunk-KIYNOOPJ.js → chunk-GTDKDD7Y.js} +12 -6
  24. package/dist/chunk-GTDKDD7Y.js.map +1 -0
  25. package/dist/{chunk-QBEZMR2F.js → chunk-HQTHDTHN.js} +137 -62
  26. package/dist/chunk-HQTHDTHN.js.map +1 -0
  27. package/dist/{chunk-TLVX5PXD.cjs → chunk-IUCNJAHW.cjs} +13 -7
  28. package/dist/chunk-IUCNJAHW.cjs.map +1 -0
  29. package/dist/{chunk-ZFIXQRBZ.cjs → chunk-LXPKTI2K.cjs} +12 -83
  30. package/dist/chunk-LXPKTI2K.cjs.map +1 -0
  31. package/dist/{chunk-CVPB2N3Z.cjs → chunk-LZ53F5BN.cjs} +103 -5
  32. package/dist/chunk-LZ53F5BN.cjs.map +1 -0
  33. package/dist/{chunk-4FDNVNJE.js → chunk-N6VMPG5S.js} +12 -79
  34. package/dist/chunk-N6VMPG5S.js.map +1 -0
  35. package/dist/{chunk-LK2CZK5U.cjs → chunk-PWWM7Q3T.cjs} +7 -7
  36. package/dist/{chunk-LK2CZK5U.cjs.map → chunk-PWWM7Q3T.cjs.map} +1 -1
  37. package/dist/{chunk-N7TOWXDJ.js → chunk-QDUTMOF4.js} +3 -3
  38. package/dist/{chunk-N7TOWXDJ.js.map → chunk-QDUTMOF4.js.map} +1 -1
  39. package/dist/{chunk-NO6XHX4R.js → chunk-RB2F5JQF.js} +100 -7
  40. package/dist/chunk-RB2F5JQF.js.map +1 -0
  41. package/dist/{chunk-MENBHEAY.cjs → chunk-WRM52ZBN.cjs} +19 -161
  42. package/dist/chunk-WRM52ZBN.cjs.map +1 -0
  43. package/dist/chunk-YS6HOWMW.js +80 -0
  44. package/dist/chunk-YS6HOWMW.js.map +1 -0
  45. package/dist/entries/assembly.cjs +34 -25
  46. package/dist/entries/assembly.d.cts +121 -37
  47. package/dist/entries/assembly.d.ts +121 -37
  48. package/dist/entries/assembly.js +5 -4
  49. package/dist/entries/doctrine.cjs +46 -46
  50. package/dist/entries/doctrine.d.cts +3 -3
  51. package/dist/entries/doctrine.d.ts +3 -3
  52. package/dist/entries/doctrine.js +1 -1
  53. package/dist/entries/picker-catalogs.cjs +42 -42
  54. package/dist/entries/picker-catalogs.js +3 -3
  55. package/dist/entries/presets.cjs +49 -26
  56. package/dist/entries/presets.d.cts +53 -1
  57. package/dist/entries/presets.d.ts +53 -1
  58. package/dist/entries/presets.js +4 -1
  59. package/dist/entries/prompt-builder.cjs +135 -98
  60. package/dist/entries/prompt-builder.d.cts +22 -11
  61. package/dist/entries/prompt-builder.d.ts +22 -11
  62. package/dist/entries/prompt-builder.js +4 -3
  63. package/dist/entries/prompt-fields.cjs +41 -40
  64. package/dist/entries/prompt-fields.d.cts +2 -1
  65. package/dist/entries/prompt-fields.d.ts +2 -1
  66. package/dist/entries/prompt-fields.js +2 -1
  67. package/dist/index.cjs +339 -273
  68. package/dist/index.d.cts +4 -4
  69. package/dist/index.d.ts +4 -4
  70. package/dist/index.js +11 -9
  71. package/dist/{subject-registry-BUswuEVD.d.cts → subject-registry-CQi_CZfJ.d.cts} +78 -9
  72. package/dist/{subject-registry-D7V8PEG-.d.ts → subject-registry-DFiGx49v.d.ts} +78 -9
  73. package/package.json +2 -2
  74. package/src/__tests__/assemble-image-input-style-references.test.ts +173 -0
  75. package/src/__tests__/catalog-id-guard.test.ts +30 -0
  76. package/src/__tests__/factory-preset-previews.test.ts +123 -0
  77. package/src/__tests__/factory-preset-references.test.ts +222 -0
  78. package/src/__tests__/factory-presets-prototype-keys.test.ts +43 -0
  79. package/src/__tests__/frame-reference-slots.test.ts +43 -0
  80. package/src/__tests__/gemini-omni-inputs.test.ts +12 -10
  81. package/src/__tests__/provider-prompt-doctrine.test.ts +14 -0
  82. package/src/__tests__/user-direction-refs.test.ts +180 -0
  83. package/src/__tests__/veo-i2v-inputs.test.ts +16 -11
  84. package/src/assemble-image-input.ts +114 -13
  85. package/src/catalog-id-guard.ts +18 -2
  86. package/src/color-names.ts +192 -0
  87. package/src/direction-registry.ts +66 -5
  88. package/src/entries/assembly.ts +1 -0
  89. package/src/entries/prompt-builder.ts +2 -0
  90. package/src/factory-presets/generate-image.ts +29 -0
  91. package/src/factory-presets/index.ts +6 -1
  92. package/src/factory-presets/preview-files.generated.ts +4 -0
  93. package/src/factory-presets/previews.ts +40 -0
  94. package/src/factory-presets/references.ts +81 -0
  95. package/src/factory-presets/shared-image.ts +4 -0
  96. package/src/factory-presets/types.ts +8 -0
  97. package/src/frame-reference-slots.ts +92 -0
  98. package/src/gemini-omni-inputs.ts +23 -22
  99. package/src/image-reference-doctrine.ts +3 -3
  100. package/src/image-token-pattern.ts +13 -0
  101. package/src/node-prompt-fields.ts +5 -3
  102. package/src/own-entry.ts +11 -0
  103. package/src/prompt-builder.ts +4 -5
  104. package/src/prompt-style-section.ts +3 -3
  105. package/src/prompt-wizard-categories.ts +2 -0
  106. package/src/provider-prompt-doctrine.ts +45 -3
  107. package/src/ref-binding.ts +28 -3
  108. package/src/resolve-prompt.ts +2 -1
  109. package/src/seedance-2-inputs.ts +15 -40
  110. package/src/user-direction-refs.ts +110 -0
  111. package/src/veo-i2v-inputs.ts +24 -17
  112. package/src/video-reference-resolver.ts +1 -1
  113. package/dist/chunk-4FDNVNJE.js.map +0 -1
  114. package/dist/chunk-7XKHDXCL.cjs.map +0 -1
  115. package/dist/chunk-CEPVQQVO.js.map +0 -1
  116. package/dist/chunk-CVPB2N3Z.cjs.map +0 -1
  117. package/dist/chunk-HRBBNODV.js.map +0 -1
  118. package/dist/chunk-JKCR6WRZ.cjs.map +0 -1
  119. package/dist/chunk-KIYNOOPJ.js.map +0 -1
  120. package/dist/chunk-LVSH7K7R.cjs.map +0 -1
  121. package/dist/chunk-MENBHEAY.cjs.map +0 -1
  122. package/dist/chunk-NO6XHX4R.js.map +0 -1
  123. package/dist/chunk-QBEZMR2F.js.map +0 -1
  124. package/dist/chunk-QDEWBBUR.js.map +0 -1
  125. package/dist/chunk-TLVX5PXD.cjs.map +0 -1
  126. package/dist/chunk-UJKFHPZ7.js +0 -37
  127. package/dist/chunk-UJKFHPZ7.js.map +0 -1
  128. package/dist/chunk-W5SCZIOB.cjs +0 -43
  129. package/dist/chunk-W5SCZIOB.cjs.map +0 -1
  130. package/dist/chunk-ZFIXQRBZ.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { SEEDANCE_2_REF_LIMITS } from "@nodaro/shared/models"
2
- import { REF_BINDING } from "./video-reference-resolver.js"
2
+ import { placeFramesAfterReferences } from "./frame-reference-slots.js"
3
3
 
4
4
  export type Seedance2Mode = "first-frame" | "first-last-frame" | "reference"
5
5
 
@@ -46,18 +46,7 @@ const cleanList = (xs?: readonly string[]): string[] =>
46
46
  * reference_image_urls (after the user's own images, so their @Image ordinals do
47
47
  * not shift) and named in a doctrine-compliant prompt suffix.
48
48
  */
49
- /** True when the prompt ALREADY binds an image as the first/opening frame —
50
- * e.g. the gvp overlap transport's colon-position directive ("use @image_1
51
- * as the first frame, it is the last keyframe of @video_1"). Field finding
52
- * (2026-07-20): the first-frame directive only works adjacent to the extend
53
- * colon; a duplicate sentence at the END of the prompt (this resolver's
54
- * default position) dilutes it back into coin-flip behavior — so when the
55
- * prompt carries its own binding, the suffix is suppressed (the frame image
56
- * itself still rides the reference list). */
57
- export function promptBindsFirstFrame(prompt: string | undefined): boolean {
58
- if (!prompt) return false
59
- return /@image_\d+\s+as\s+the\s+(first|opening)\s*(\(first\))?\s*frame/i.test(prompt)
60
- }
49
+ export { promptBindsFirstFrame } from "./frame-reference-slots.js"
61
50
 
62
51
  export function resolveSeedance2Inputs(args: Seedance2InputsArgs): Seedance2InputsResult {
63
52
  const limits = args.limits ?? SEEDANCE_2_REF_LIMITS
@@ -85,33 +74,19 @@ export function resolveSeedance2Inputs(args: Seedance2InputsArgs): Seedance2Inpu
85
74
  }
86
75
 
87
76
  // Reference mode: keep both frames (explicit intent), drop trailing user images
88
- // if the 9-image cap is exceeded. Frames are appended AFTER the kept user
89
- // images so existing user @Image ordinals are preserved.
90
- const frameCount = (firstFrameUrl ? 1 : 0) + (lastFrameUrl ? 1 : 0)
91
- const userImageSlots = Math.max(0, limits.images - frameCount)
92
- const keptUserImages = refImages.slice(0, userImageSlots)
93
- const droppedRefImages = refImages.length - keptUserImages.length
94
-
95
- const referenceImageUrls: string[] = [...keptUserImages]
96
- let firstOrdinal = 0
97
- let lastOrdinal = 0
98
- if (firstFrameUrl) { referenceImageUrls.push(firstFrameUrl); firstOrdinal = referenceImageUrls.length }
99
- if (lastFrameUrl) { referenceImageUrls.push(lastFrameUrl); lastOrdinal = referenceImageUrls.length }
100
-
101
- let promptSuffix = ""
102
- const suppressFrameSentence = promptBindsFirstFrame(args.prompt)
103
- if (suppressFrameSentence && firstOrdinal > 0 && lastOrdinal === 0) {
104
- // The prompt already binds the first frame at its own (working) position.
105
- } else if (firstOrdinal > 0 && lastOrdinal > 0) {
106
- // Combined sentence — REF_BINDING.frame() emits a single-frame sentence, so use
107
- // REF_BINDING.ordinal() for each ordinal inline to keep the combined wording AND
108
- // route both ordinals through the single swap-point.
109
- promptSuffix = `Use ${REF_BINDING.ordinal(firstOrdinal)} as the opening (first) frame and ${REF_BINDING.ordinal(lastOrdinal)} as the closing (last) frame of the video.`
110
- } else if (firstOrdinal > 0) {
111
- promptSuffix = REF_BINDING.frame(firstOrdinal, "opening")
112
- } else if (lastOrdinal > 0) {
113
- promptSuffix = REF_BINDING.frame(lastOrdinal, "closing")
114
- }
77
+ // if the image cap is exceeded. Frames are appended AFTER the kept user images
78
+ // so existing user @Image ordinals are preserved — the platform convention
79
+ // (placeFramesAfterReferences), shared with every frames-in-the-list model.
80
+ const placed = placeFramesAfterReferences({
81
+ prompt: args.prompt,
82
+ firstFrameUrl,
83
+ lastFrameUrl,
84
+ refImageUrls: refImages,
85
+ imageCap: limits.images,
86
+ })
87
+ const referenceImageUrls = placed.imageUrls
88
+ const promptSuffix = placed.promptSuffix
89
+ const droppedRefImages = placed.droppedRefImages
115
90
 
116
91
  return {
117
92
  mode: "reference",
@@ -0,0 +1,110 @@
1
+ /**
2
+ * USER-OWNED DIRECTION REFERENCES — a `direction` value that names one of the
3
+ * caller's own saved resources instead of a catalog id.
4
+ *
5
+ * Two rows of the direction registry take one (their `userRef` column):
6
+ * - `colorLook: "palette:<uuid>"` — a saved colour palette;
7
+ * - `style: "moodboard:<uuid>"` — a saved moodboard.
8
+ * The prefix namespaces them, so they never collide with a catalog id
9
+ * (catalog ids are slugs; none carries a colon).
10
+ *
11
+ * The wire carries the id, never the resolved text, so a re-run resolves it
12
+ * again and an edited palette reaches the next run. RESOLVING is a database
13
+ * read, so it happens in the platform before the (pure, synchronous) fold:
14
+ * the caller hands the fold a map of value → rendered clause
15
+ * (`renderDirectionHintClauses`' `refs`, `assembleImageInput`'s
16
+ * `directionRefs`), and a moodboard's images as fill-only style references
17
+ * (`assembleImageInput`'s `styleReferences`). This module holds what is pure:
18
+ * the grammar and the WORDING of those clauses and reference lines.
19
+ */
20
+ import { nearestColorName } from "./color-names.js"
21
+
22
+ /** The kinds of saved resource a direction value can name. */
23
+ export type UserDirectionRefKind = "palette" | "moodboard"
24
+
25
+ export const USER_DIRECTION_REF_KINDS: ReadonlyArray<UserDirectionRefKind> = ["palette", "moodboard"]
26
+
27
+ /** A parsed user-owned reference. `wellFormed` = its id is a uuid. */
28
+ export interface UserDirectionRef {
29
+ readonly kind: UserDirectionRefKind
30
+ readonly id: string
31
+ readonly wellFormed: boolean
32
+ }
33
+
34
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
35
+
36
+ /**
37
+ * Read one direction value. `null` when it is not a user-owned reference at
38
+ * all (a catalog id, an empty string, anything that is not a string). A value
39
+ * with a known prefix is ALWAYS a reference — `wellFormed: false` when what
40
+ * follows is not a uuid, so a caller can refuse it rather than treat it as an
41
+ * unknown catalog id and skip it in silence.
42
+ */
43
+ export function parseUserDirectionRef(value: unknown): UserDirectionRef | null {
44
+ if (typeof value !== "string") return null
45
+ for (const kind of USER_DIRECTION_REF_KINDS) {
46
+ const prefix = `${kind}:`
47
+ if (value.startsWith(prefix)) {
48
+ const id = value.slice(prefix.length)
49
+ return { kind, id, wellFormed: UUID_RE.test(id) }
50
+ }
51
+ }
52
+ return null
53
+ }
54
+
55
+ /** The wire value for a saved resource: `palette:<id>` / `moodboard:<id>`. */
56
+ export function userDirectionRefValue(kind: UserDirectionRefKind, id: string): string {
57
+ return `${kind}:${id}`
58
+ }
59
+
60
+ /** Collapse all whitespace (newlines included) to single spaces and trim. User
61
+ * text never opens a line of its own inside the assembled prompt — a newline
62
+ * there could start a fake `[style]:` section. */
63
+ function oneLine(text: string | null | undefined): string {
64
+ return (text ?? "").replace(/\s+/g, " ").trim()
65
+ }
66
+
67
+ const HEX_RE = /^#[0-9a-f]{6}$/i
68
+
69
+ /**
70
+ * A saved palette as a look clause: every colour by its everyday name and its
71
+ * exact hex, in the palette's order — `color palette: dark teal #0F4C5C,
72
+ * saffron #E3B23C`. An entry that is not a `#RRGGBB` colour is skipped; no
73
+ * valid colour renders nothing.
74
+ */
75
+ export function renderPaletteClause(colors: ReadonlyArray<string>): string {
76
+ const named = colors
77
+ .filter((c) => HEX_RE.test(c))
78
+ .map((c) => `${nearestColorName(c)} ${c.toUpperCase()}`)
79
+ return named.length > 0 ? `color palette: ${named.join(", ")}` : ""
80
+ }
81
+
82
+ /**
83
+ * A saved moodboard's written description as its look clause: the user's own
84
+ * words, on one line. No description renders nothing (the moodboard's images
85
+ * still go along as style references).
86
+ */
87
+ export function renderMoodboardClause(description: string | null | undefined): string {
88
+ return oneLine(description)
89
+ }
90
+
91
+ /** One style reference image (a moodboard's image) handed to the assembler. */
92
+ export interface StyleReferenceInput {
93
+ readonly url: string
94
+ /** What the image is for — rides its reference line. */
95
+ readonly caption?: string
96
+ }
97
+
98
+ /**
99
+ * The line a style reference carries in the assembled prompt (its
100
+ * `description` as a reference). The first one says what to take from a style
101
+ * reference and what not to; the rest stay short, so a full moodboard does not
102
+ * bury the prompt. A caption rides along on one line.
103
+ */
104
+ export function styleReferenceDescription(index: number, caption?: string): string {
105
+ const cap = oneLine(caption)
106
+ const tail = cap ? ` (${cap})` : ""
107
+ return index === 0
108
+ ? `match the colors, light, texture and mood of this style reference${tail}, not its subject or layout`
109
+ : `another style reference${tail}`
110
+ }
@@ -1,5 +1,4 @@
1
- import { promptBindsFirstFrame } from "./seedance-2-inputs.js"
2
- import { identityRefsSentence, REF_BINDING } from "./video-reference-resolver.js"
1
+ import { placeFramesAfterReferences } from "./frame-reference-slots.js"
3
2
 
4
3
  /**
5
4
  * VEO 3.x i2v input resolution — the mutually-exclusive sibling of
@@ -8,13 +7,15 @@ import { identityRefsSentence, REF_BINDING } from "./video-reference-resolver.js
8
7
  * [first(, last)] frames; REFERENCE_2_VIDEO reads every entry as a
9
8
  * reference ingredient. Frames and identities cannot ride separate
10
9
  * channels, so an anchored call that must carry identity references moves
11
- * to REFERENCE_2_VIDEO with the anchor in seat 1, bound in prose as the
12
- * opening frame (requested, not pixel-guaranteed — the accepted trade,
13
- * same as seedance-2's reference mode).
10
+ * to REFERENCE_2_VIDEO with the frames bound in prose (requested, not
11
+ * pixel-guaranteed — the accepted trade, same as seedance-2's reference mode).
14
12
  *
15
- * REFERENCES WIN THE SEATS (the 2026-08-14 standing rule: refs are a must,
16
- * frames additional): the end anchor is dropped in reference mode rather
17
- * than spending one of three seats on a closing guess. The caller logs it.
13
+ * THE PLATFORM CONVENTION (2026-10-18, `placeFramesAfterReferences`): the
14
+ * caller's references take the first seats, so their image token N is their
15
+ * Nth reference; the start frame — and the end frame, VEO takes one — follow
16
+ * them, bound as the opening / closing frame. At the 3-ingredient cap the
17
+ * frames are kept (explicit intent) and trailing references drop, reported.
18
+ * (Until then the anchor took seat 1 and an end frame was surrendered.)
18
19
  *
19
20
  * BYTE-IDENTICAL with no references: plain frame mode, frames kept, no
20
21
  * generationType, no suffix — exactly what every veo i2v call has always
@@ -30,14 +31,15 @@ export interface VeoI2vInputsArgs {
30
31
  }
31
32
 
32
33
  export interface VeoI2vInputsResult {
33
- /** The `imageUrls` payload: frames in plain mode, [anchor, ...refs] in
34
+ /** The `imageUrls` payload: frames in plain mode, [...refs, start, end?] in
34
35
  * reference mode — never more than VEO's 3-ingredient cap. */
35
36
  imageUrls: string[]
36
37
  /** Present (REFERENCE_2_VIDEO) exactly when references ride. */
37
38
  generationType?: "REFERENCE_2_VIDEO"
38
39
  promptSuffix: string
39
40
  droppedRefImages: number
40
- /** True when an end anchor was surrendered to reference mode. */
41
+ /** Always false since the frames-after-references convention: an end frame
42
+ * is kept in reference mode. Kept for callers that log it. */
41
43
  droppedEndFrame: boolean
42
44
  }
43
45
 
@@ -55,14 +57,19 @@ export function resolveVeoI2vInputs(args: VeoI2vInputsArgs): VeoI2vInputsResult
55
57
  droppedEndFrame: false,
56
58
  }
57
59
  }
58
- const kept = refs.slice(0, VEO_INGREDIENT_SLOTS - 1)
59
- const droppedRefImages = refs.length - kept.length
60
- const frameSentence = promptBindsFirstFrame(args.prompt) ? "" : REF_BINDING.frame(1, "opening")
60
+ const placed = placeFramesAfterReferences({
61
+ prompt: args.prompt,
62
+ firstFrameUrl: args.firstFrameUrl,
63
+ lastFrameUrl: args.endFrameUrl,
64
+ refImageUrls: refs,
65
+ imageCap: VEO_INGREDIENT_SLOTS,
66
+ bindIdentities: true,
67
+ })
61
68
  return {
62
- imageUrls: [args.firstFrameUrl, ...kept],
69
+ imageUrls: placed.imageUrls,
63
70
  generationType: "REFERENCE_2_VIDEO",
64
- promptSuffix: [frameSentence, identityRefsSentence(2, kept.length + 1)].filter(Boolean).join(" "),
65
- droppedRefImages,
66
- droppedEndFrame: Boolean(args.endFrameUrl),
71
+ promptSuffix: placed.promptSuffix,
72
+ droppedRefImages: placed.droppedRefImages,
73
+ droppedEndFrame: false,
67
74
  }
68
75
  }
@@ -54,7 +54,7 @@ import { tidyReferenceTokenGaps, unboundReferenceTokenText } from "./unbound-ref
54
54
  // The binding surface string and the id-addressed token resolver live in their
55
55
  // own modules (see them for the contracts); re-exported here so every existing
56
56
  // importer of this module — and the package index's `export *` — keeps working.
57
- export { REF_BINDING, identityRefsSentence, SEEDANCE_VIDEO_EDIT_PREFIX, buildSeedanceVideoEditPrompt } from "./ref-binding.js"
57
+ export { REF_BINDING, identityRefsSentence, imageBindingFor, SEEDANCE_VIDEO_EDIT_PREFIX, buildSeedanceVideoEditPrompt, type ImageBinding } from "./ref-binding.js"
58
58
  export { resolveRefIdTokens, type RefIdTokenContext } from "./ref-id-tokens.js"
59
59
 
60
60
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/resolve-prompt.ts","../src/audio-tags.ts","../src/node-prompt-fields.ts","../src/node-send-text.ts"],"names":["SOCIAL_POST_NODE_TYPES","present","resolveNodeRefs"],"mappings":"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","file":"chunk-4FDNVNJE.js","sourcesContent":["import { resolveNodeRefs } from \"@nodaro/shared/workflow\"\nimport { readPromptAffixes, type PromptAffixes } from \"@nodaro/shared/nodes\"\nimport { SOCIAL_POST_NODE_TYPES } from \"@nodaro/shared/social\"\nimport { joinSentences } from \"./hint-join.js\"\n\nexport interface ResolvePromptArgs {\n override?: string\n typed?: ReadonlyArray<string | undefined>\n wired?: string\n refMap: ReadonlyMap<string, string>\n /** Opt-in (generate-image / generate-video only): APPEND the wired\n * (connected-prompt) value to the TYPED base instead of treating wired as a\n * fallback, so a connected prompt auto-injects alongside the typed prompt. An\n * `override` (list fan-out item) still fully replaces — no wired append.\n * Off/undefined = exact legacy precedence (override > typed > wired) for every\n * other node type. */\n appendWired?: boolean\n /** Pre/post text wrapped around WHICHEVER core precedence produced (override,\n * typed, wired, or the appendWired combination). Applied last. */\n affixes?: PromptAffixes\n}\nconst present = (s?: string): s is string => typeof s === \"string\" && s.trim().length > 0\nconst rr = (s: string, m: ReadonlyMap<string, string>) => (m.size > 0 ? resolveNodeRefs(s, m) : s)\n\n// ---------------------------------------------------------------------------\n// Prompt affixes (pre/post text) — see docs/prompt-pre-post-text.md (join rule, empty-core, no-op guarantee)\n// ---------------------------------------------------------------------------\n\nconst GLUE_PUNCTUATION = new Set([\",\", \".\", \";\", \":\", \"!\", \"?\", \")\"])\n\n/** The ONE join rule between two non-blank prompt parts: a single space unless\n * the boundary already has whitespace on either side or the right part opens\n * with sentence punctuation. Shared by `joinPromptParts` and the editor's\n * Final-view segment builder so preview and run agree byte-for-byte. */\nexport function promptPartSeparator(left: string, right: string): \"\" | \" \" {\n if (left.length === 0 || right.length === 0) return \"\"\n if (/\\s$/.test(left) || /^\\s/.test(right)) return \"\"\n if (GLUE_PUNCTUATION.has(right[0])) return \"\"\n return \" \"\n}\n\n/** Join prompt parts with `promptPartSeparator`; blank parts are dropped,\n * non-blank parts are used verbatim (no trimming). */\nexport function joinPromptParts(parts: ReadonlyArray<string | undefined>): string {\n let out = \"\"\n for (const part of parts) {\n if (!present(part)) continue\n out = out.length === 0 ? part : out + promptPartSeparator(out, part) + part\n }\n return out\n}\n\n/**\n * Wrap `core` with the node's pre/post text. `{Label}` refs are resolved in the\n * AFFIXES only — the core arrives exactly as the caller's precedence produced it.\n * NO-OP GUARANTEE: with no non-blank affix the core is returned unchanged (same\n * reference) — every existing workflow stays byte-identical.\n * EMPTY CORE: a blank core with affixes yields the joined affixes alone.\n */\nexport function applyPromptAffixes(core: string, affixes: PromptAffixes | undefined, refMap: ReadonlyMap<string, string>): string\nexport function applyPromptAffixes(core: string | undefined, affixes: PromptAffixes | undefined, refMap: ReadonlyMap<string, string>): string | undefined\nexport function applyPromptAffixes(\n core: string | undefined,\n affixes: PromptAffixes | undefined,\n refMap: ReadonlyMap<string, string>,\n): string | undefined {\n const prefix = present(affixes?.prefix) ? rr(affixes!.prefix!, refMap) : undefined\n const suffix = present(affixes?.suffix) ? rr(affixes!.suffix!, refMap) : undefined\n if (prefix === undefined && suffix === undefined) return core\n return joinPromptParts([prefix, core, suffix])\n}\n\n/** SINGLE SOURCE OF TRUTH for prompt precedence across both DAG engines:\n * override (list fan-out) > first present typed candidate > wired > \"\".\n * \"present\" = non-empty after trim. {Label} refs are resolved on the chosen\n * branch via the shared resolveNodeRefs. With `appendWired`, the chosen base\n * AND the wired value are both emitted, joined as sentences (`joinSentences`:\n * \". \", or a space after one that already ends). Affixes wrap the result. */\nexport function resolvePrompt({ override, typed = [], wired, refMap, appendWired, affixes }: ResolvePromptArgs): string {\n return applyPromptAffixes(resolveCore({ override, typed, wired, refMap, appendWired }), affixes, refMap)\n}\n\nfunction resolveCore({ override, typed = [], wired, refMap, appendWired }: Omit<ResolvePromptArgs, \"affixes\">): string {\n // appendWired: a connected prompt APPENDS to the TYPED base. An `override`\n // (list fan-out item) still fully REPLACES — it never receives a wired append,\n // so per-item fan-out prompts are unchanged.\n if (appendWired && !present(override)) {\n const base = typed.find(present)\n return joinSentences([base, wired].filter(present).map((s) => rr(s, refMap)))\n }\n if (present(override)) return rr(override, refMap)\n for (const t of typed) if (present(t)) return rr(t, refMap)\n if (present(wired)) return rr(wired, refMap)\n return \"\"\n}\n\n/** Compose a final NEGATIVE prompt from a TYPED base + a WIRED (connected\n * negative-handle) value: both are emitted, joined as sentences (`joinSentences`, mirrors `appendWired`\n * for the positive prompt). Pure join — the caller resolves `{label}` refs on\n * the typed value first, and the wired value is already a resolved output that\n * the input-resolver has filtered (referenced / Inject-Negative-off dropped).\n * Generate-image / generate-video only; empty parts are dropped. */\nexport function composeNegative(typed?: string, wired?: string): string {\n return joinSentences([typed, wired])\n}\n\n/** Ordered typed-candidate fields per node type — the precedence source of\n * truth (NOT NODE_MAPPABLE_FIELDS, which is field-mapping eligibility, omits\n * video-retake, and orders llm-chat wrong). */\nexport const NODE_PROMPT_CANDIDATE_FIELDS: Readonly<Record<string, readonly string[]>> = {\n \"generate-image\": [\"prompt\"],\n // motionPrompt is here so generate-video re-typed to text-to-video keeps a\n // prompt stored in data.motionPrompt (the inline picker's legacy field). Safe\n // for a standalone text-to-video node — it never carries data.motionPrompt.\n \"text-to-video\": [\"prompt\", \"motionPrompt\"],\n \"video-to-video\": [\"prompt\"],\n \"generate-music\": [\"prompt\"],\n \"speech-to-video\": [\"prompt\"],\n \"cinematic-avatar\": [\"prompt\"],\n \"extend-video\": [\"prompt\"],\n \"video-retake\": [\"prompt\"],\n \"suno-replace-section\": [\"prompt\"],\n \"image-to-video\": [\"prompt\", \"motionPrompt\"],\n \"generate-video\": [\"prompt\", \"motionPrompt\"],\n \"text-to-audio\": [\"prompt\", \"text\"],\n // Social posts: the orchestrator + frontend executor match per-platform node\n // types (node.type is \"instagram-post\", \"telegram-post\", …), NOT a unified\n // \"social-publish\" type — so every platform needs its own [\"caption\"] entry.\n // Derived from the shared SOCIAL_POST_NODE_TYPES single source of truth so a\n // new platform can't drift out of the precedence map.\n ...Object.fromEntries(\n [...SOCIAL_POST_NODE_TYPES].map((t) => [t, [\"caption\"] as readonly string[]]),\n ),\n // Kept for any caller that passes the aggregate type instead of a platform.\n \"social-publish\": [\"caption\"],\n}\n\nexport interface ComputeNodePromptArgs {\n override?: string\n wired?: string\n refMap: ReadonlyMap<string, string>\n /** See ResolvePromptArgs.appendWired — generate-image / generate-video only. */\n appendWired?: boolean\n}\n/** Resolve a single-prompt node's final prompt (typed-primary). Both engines\n * call this so field-selection + precedence are structurally identical. */\nexport function computeNodePrompt(\n nodeType: string,\n data: Record<string, unknown>,\n { override, wired, refMap, appendWired }: ComputeNodePromptArgs,\n): string {\n let typed: ReadonlyArray<string | undefined>\n if (nodeType === \"text-to-speech\") {\n // data.text is a phantom field on TTS; only directText (gated) is real.\n //\n // The gate is a PREFERENCE, not a lock: when textSource is \"connected\" we\n // still fall back to typed text if nothing is wired. Writers flip the gate\n // (PromptFieldSpec.promptGate), but data reaches nodes from places no\n // writer touches — workflows saved before that fix, JSON imports, MCP\n // writes, templates — and there the text sat visible in the node while the\n // run failed with \"no text found\" (founder, 2026-08-14). Coerce rather\n // than reject, same principle as normalizeModelInput.\n typed =\n data.textSource === \"direct\" || !present(wired)\n ? [data.directText as string | undefined]\n : []\n } else {\n const fields = NODE_PROMPT_CANDIDATE_FIELDS[nodeType] ?? [\"prompt\"]\n typed = fields.map((f) => data[f] as string | undefined)\n }\n return resolvePrompt({ override, typed, wired, refMap, appendWired, affixes: readPromptAffixes(data) })\n}\n\nexport interface LlmChatFieldArgs {\n override?: string\n wiredUserInput?: string\n wiredSystemPrompt?: string\n refMap: ReadonlyMap<string, string>\n}\n/** llm-chat resolves TWO independent fields. override applies to userInput only. */\nexport function computeLlmChatFields(\n data: Record<string, unknown>,\n { override, wiredUserInput, wiredSystemPrompt, refMap }: LlmChatFieldArgs,\n): { userInput: string; systemPrompt: string } {\n return {\n userInput: resolvePrompt({ override, typed: [data.userInput as string | undefined], wired: wiredUserInput, refMap, affixes: readPromptAffixes(data) }),\n systemPrompt: resolvePrompt({ typed: [data.systemPrompt as string | undefined], wired: wiredSystemPrompt, refMap }),\n }\n}\n\n/** generate-script's topic. Both engines call this, so a wired Text node reads\n * the same on a single-node run and a server run.\n *\n * Precedence is override (a list fan-out item) > wired > `data.prompt`. Wired\n * comes BEFORE the typed field, the reverse of `computeNodePrompt`, because\n * `data.prompt` has no editor: the panel's text field is the style guide. A\n * value there was written by the old connect-time `{Label}` auto-fill or by an\n * agent, and it is kept only as the fallback that saved workflows rely on. */\nexport function computeScriptTopic(\n data: Record<string, unknown>,\n { override, wired, refMap }: Omit<ComputeNodePromptArgs, \"appendWired\">,\n): string {\n const core = [override, wired, data.prompt as string | undefined].find(present)\n return applyPromptAffixes(core === undefined ? \"\" : rr(core, refMap), readPromptAffixes(data), refMap)\n}\n","/**\n * Remove inline `[audio tags]` from speech text, for a model that does not\n * perform them (it would read them aloud). Pure, dependency-free: the\n * backend's credit counter (lib/speech-credits.ts), its payload builder and\n * the editor's estimate (frontend/src/lib/speech-estimate.ts) all read it, so\n * the two engines count exactly the same characters.\n *\n * A tag is `[`, one or more characters that are not `]`, then `]` — what\n * `/\\[[^\\]]+\\]/g` matches. That pattern is quadratic on an unclosed run of `[`\n * (every `[` rescans to the end of the text), and the credit counter runs this\n * on raw request text on the API and orchestrator event loops, so the scan is\n * written by hand: one `indexOf` per `[`, and when no `]` is left no later `[`\n * can match either, so the rest of the text is kept in one slice.\n */\nfunction removeBracketTags(text: string): string {\n let out = \"\"\n let from = 0\n let open = text.indexOf(\"[\")\n while (open !== -1) {\n const close = text.indexOf(\"]\", open + 1)\n if (close === -1) break\n if (close === open + 1) {\n // \"[]\" is not a tag (needs 1+ characters); the \"[\" stays and the scan moves on.\n open = text.indexOf(\"[\", open + 1)\n continue\n }\n out += text.slice(from, open)\n from = close + 1\n open = text.indexOf(\"[\", from)\n }\n return out + text.slice(from)\n}\n\nexport function stripAudioTags(text: string): string {\n return removeBracketTags(text).replace(/\\s{2,}/g, \" \").trim()\n}\n","/**\n * Single source of truth: which data field(s) hold a node's user-editable\n * prompt text. Drives the quick-edit Prompt modal so it can work generically\n * across every AI node without each call site hardcoding a field name.\n *\n * The registry LIVES HERE (`@nodaro/prompts`) so the backend orchestrator, the\n * `/v1/nodes` registry, the prompt-affix totality tests and the docs tooling\n * read the SAME list the editor does. `frontend/src/lib/prompt-fields.ts` is a\n * pure re-export of this module — frontend code keeps importing\n * `@/lib/prompt-fields` unchanged.\n *\n * Most nodes store the prompt in `data.prompt`, but several don't\n * (`text-prompt` → `text`, `image-to-text` → `customPrompt`, …) — this map is\n * where that knowledge lives, once.\n *\n * INVARIANT: every node that exposes a user-editable prompt MUST have an entry\n * here, or the quick-edit modal silently does nothing for it. A guard test\n * (`prompt-fields.test.ts`) keeps this in sync with the node set.\n *\n * This module is intentionally pure data (no React/lucide imports) so it can be\n * pulled into the app-runtime bundle and any test without dragging in the icon\n * library. The string `icon` kind is mapped to a concrete lucide component in\n * `prompt-edit-button.tsx` (`getPromptIcon`), the only place that renders it.\n */\n\nimport type { SnippetMedia } from \"./factory-snippets/types.js\"\nimport { NODE_PROMPT_CANDIDATE_FIELDS } from \"./resolve-prompt.js\"\n\n/** Which lucide glyph a node's prompt affordance uses. Kept as a string so this\n * module stays icon-library-free; mapped to a component in the strip button. */\nexport type PromptIconKind = \"pencil\" | \"paintbrush\"\n\nexport interface PromptFieldSpec {\n /** Data key holding the primary prompt (e.g. \"prompt\", \"text\", \"customPrompt\"). */\n readonly prompt: string\n /** Data key holding the negative prompt, when the node supports one. */\n readonly negative?: string\n /** Label override for the primary field (defaults to \"Prompt\"). */\n readonly promptLabel?: string\n /** Icon for the prompt affordance (strip button + modal title). Defaults to a\n * pencil; image-editing nodes use a paintbrush to read as \"edit\". */\n readonly icon?: PromptIconKind\n /** Node modality for prompt-snippet scoping — drives which snippet pool the\n * \"/\" menu and Snippets button show for this node's prompt fields. REQUIRED\n * so a new node cannot forget to declare it (compile error). */\n readonly media: SnippetMedia\n /** True when this node renders a media-result preview body and therefore\n * participates in inline-prompt mode (the `InlineNodePrompt` editor on the\n * node face, centralized in `BaseNode`). REQUIRED (like `media`) so a new\n * prompt node must consciously choose — compile error if omitted. The set of\n * `inline: true` types is guarded in `prompt-fields.test.ts`. */\n readonly inline: boolean\n /** Some nodes only READ their prompt field when a sibling discriminator\n * selects it — TTS keeps `directText` but resolves it only while\n * `textSource === \"direct\"`, defaulting to `\"connected\"`. Writing the text\n * without flipping the discriminator saved the value and then failed with\n * \"no text found\" (founder hit it live, 2026-08-14). Declare the pair here\n * and every writer flips it automatically. */\n readonly promptGate?: { readonly field: string; readonly value: string }\n /** Prompt pre/post text (`promptPrefix` / `promptSuffix`) support. Defaults to\n * ON for every prompt node; set `false` ONLY for a node whose \"prompt\" is not\n * a model prompt (the plain Text input node). Read via `nodeSupportsPromptAffixes`. */\n readonly affixes?: false\n}\n\nexport const NODE_PROMPT_FIELDS: Readonly<Record<string, PromptFieldSpec>> = {\n // ── Image ──\n // (`edit-image` / `image-to-image` are legacy types consolidated into\n // `modify-image`; they're not in the creatable node set, so a node of that\n // type never mounts and needs no entry here. The guard test enforces that.)\n \"generate-image\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"image\", inline: true },\n \"modify-image\": { prompt: \"prompt\", negative: \"negativePrompt\", icon: \"paintbrush\", media: \"image\", inline: true },\n \"generate-mask\": { prompt: \"prompt\", promptLabel: \"What to mask\", media: \"image\", inline: true },\n // ── Video ──\n \"generate-video\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"video\", inline: true },\n // Trimmed multi-segment stitch sibling of generate-video — no negativePrompt field.\n \"generate-video-pro\": { prompt: \"prompt\", media: \"video\", inline: true },\n // Span-replace sibling of generate-video-pro — no negativePrompt field.\n \"edit-video-pro\": { prompt: \"prompt\", media: \"video\", inline: true },\n \"text-to-video\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"video\", inline: true },\n \"image-to-video\": { prompt: \"prompt\", negative: \"negativePrompt\", promptLabel: \"Motion prompt\", media: \"video\", inline: true },\n \"video-to-video\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"video\", inline: true },\n \"switchx\": { prompt: \"prompt\", promptLabel: \"Look prompt\", media: \"video\", inline: true },\n \"extend-video\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"video\", inline: true },\n \"speech-to-video\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"video\", inline: true },\n \"motion-transfer\": { prompt: \"prompt\", negative: \"negativePrompt\", media: \"video\", inline: true },\n // Cinematic Avatar (HeyGen) — generative prompt (NOT a verbatim script),\n // so it participates in the quick-edit Prompt modal like other AI video nodes.\n \"cinematic-avatar\": { prompt: \"prompt\", media: \"video\", inline: true },\n \"video-sfx\": { prompt: \"prompt\", negative: \"negativePrompt\", promptLabel: \"Sound prompt\", media: \"audio\", inline: true },\n \"video-retake\": { prompt: \"prompt\", promptLabel: \"Retake prompt\", media: \"video\", inline: true },\n // Video Mask (SAM 3) — the subject to track, like Generate Mask's \"what to mask\".\n \"video-mask\": { prompt: \"prompt\", promptLabel: \"What to mask\", media: \"video\", inline: true },\n // ── Audio / music ──\n \"generate-music\": { prompt: \"prompt\", media: \"audio\", inline: true },\n \"suno-generate\": { prompt: \"prompt\", media: \"audio\", inline: true },\n \"text-to-audio\": { prompt: \"prompt\", media: \"audio\", inline: true },\n // ── Text / LLM (no media-result preview body → no inline editor) ──\n \"text-prompt\": { prompt: \"text\", promptLabel: \"Text\", media: \"text\", inline: false, affixes: false },\n \"image-to-text\": { prompt: \"customPrompt\", promptLabel: \"Question\", media: \"text\", inline: false },\n \"llm-chat\": { prompt: \"userInput\", promptLabel: \"Prompt\", media: \"text\", inline: false },\n // ── Speech / voice ──\n \"text-to-speech\": { prompt: \"directText\", promptLabel: \"Text\", media: \"audio\", inline: true, promptGate: { field: \"textSource\", value: \"direct\" } },\n \"voice-design\": { prompt: \"voiceDescription\", promptLabel: \"Voice description\", media: \"audio\", inline: true },\n \"voice-remix\": { prompt: \"voiceDescription\", promptLabel: \"Voice description\", media: \"audio\", inline: true },\n \"lip-sync\": { prompt: \"prompt\", media: \"audio\", inline: true },\n // ── Suno (music) ──\n \"suno-cover\": { prompt: \"prompt\", media: \"audio\", inline: true },\n \"suno-extend\": { prompt: \"prompt\", media: \"audio\", inline: true },\n \"suno-replace-section\": { prompt: \"prompt\", media: \"audio\", inline: true },\n \"suno-upload-extend\": { prompt: \"prompt\", media: \"audio\", inline: true },\n // NOTE: `suno-add-vocals` is intentionally absent — its node has no\n // user-editable prompt (SunoAddVocalsData declares only `model`; the config\n // panel + backend route take taskId/audioId/model, never a prompt). A stale\n // entry here rendered a phantom Prompt editor in the quick-edit modal that\n // wrote to a `data.prompt` key nothing ever reads (the dead-field class the\n // guard test now catches via defaultData ownership).\n \"suno-lyrics\": { prompt: \"prompt\", media: \"audio\", inline: false },\n \"suno-style-boost\": { prompt: \"content\", promptLabel: \"Style\", media: \"audio\", inline: false },\n // ── Composition / FX (compact, no media-result preview → no inline editor) ──\n \"image-critic\": { prompt: \"prompt\", promptLabel: \"Criteria\", media: \"image\", inline: false },\n \"motion-graphics\": { prompt: \"motionPrompt\", promptLabel: \"Motion prompt\", media: \"video\", inline: false },\n \"pro-3d-render\": { prompt: \"scenePrompt\", promptLabel: \"Scene\", media: \"text\", inline: false },\n \"generate-3d-scene\": { prompt: \"scenePrompt\", promptLabel: \"Scene\", media: \"text\", inline: false },\n \"edit-3d-scene\": { prompt: \"editPrompt\", promptLabel: \"Edit instruction\", media: \"text\", inline: false },\n \"3d-title\": { prompt: \"titlePrompt\", promptLabel: \"Title\", media: \"text\", inline: false },\n // ── Script / alignment (their primary text field) ──\n \"generate-script\": { prompt: \"styleGuide\", promptLabel: \"Style guide\", media: \"text\", inline: false },\n \"forced-alignment\": { prompt: \"transcript\", promptLabel: \"Transcript\", media: \"audio\", inline: false },\n // Video Analysis — its focus hint is the editable prompt (renders a JSON scene\n // table, not a media preview → no inline editor).\n \"video-analysis\": { prompt: \"analysisFocus\", promptLabel: \"Analysis focus\", media: \"video\", inline: false },\n // Edit Plan (podcast editing) — its free-text editing instructions are the\n // editable prompt; the affixes wrap them. Emits an EDL (JSON), not a media\n // preview, so no inline editor.\n \"edit-plan\": { prompt: \"instructions\", promptLabel: \"Editing instructions\", media: \"video\", inline: false },\n}\n\n/** The prompt-field spec for a node type, or undefined if it has none. */\nexport function getPromptFields(nodeType: string | undefined): PromptFieldSpec | undefined {\n return nodeType ? NODE_PROMPT_FIELDS[nodeType] : undefined\n}\n\n/** True when this node type has a registered, quick-editable prompt field. */\nexport function nodeHasPromptField(nodeType: string | undefined): boolean {\n return getPromptFields(nodeType) !== undefined\n}\n\n/** Snippet modality for a node type (drives the snippet pool), or undefined\n * when the node has no prompt field. */\nexport function getSnippetMedia(nodeType: string | undefined): SnippetMedia | undefined {\n return getPromptFields(nodeType)?.media\n}\n\n/** True when this node type renders the inline on-node prompt editor — the\n * media-preview nodes (image/video/audio result body). Single source for\n * BaseNode's centralized `InlineNodePrompt` rendering and the gold nodes'\n * `showInline` derivation (`useInlinePromptActive`). */\nexport function nodeHasInlinePrompt(nodeType: string | undefined): boolean {\n return getPromptFields(nodeType)?.inline === true\n}\n\n/** True when the node type carries `promptPrefix` / `promptSuffix` — every\n * registered prompt node except explicit opt-outs (`affixes: false`). The single\n * predicate the config panel, Final view, `/v1/nodes` registry, totality tests\n * and docs tooling all gate on. */\nexport function nodeSupportsPromptAffixes(nodeType: string | undefined): boolean {\n const spec = getPromptFields(nodeType)\n return spec !== undefined && spec.affixes !== false\n}\n\n/**\n * Where the RUN-TIME prompt lives when it differs from the editor's prompt\n * field (spec §7). `generate-script`'s editable prompt is its `styleGuide`, but\n * the run wraps the topic `prompt` with the affixes — so previewing the style\n * guide must NOT show them.\n */\nexport const PROMPT_AFFIX_CORE_FIELD_OVERRIDES: Readonly<Record<string, string>> = { \"generate-script\": \"prompt\" }\n\n/** The data key whose value the run wraps with promptPrefix/promptSuffix, or\n * undefined when the node has no affixes. */\nexport function promptAffixCoreField(nodeType: string | undefined): string | undefined {\n if (!nodeSupportsPromptAffixes(nodeType)) return undefined\n return PROMPT_AFFIX_CORE_FIELD_OVERRIDES[nodeType!] ?? getPromptFields(nodeType)!.prompt\n}\n\n/**\n * True when a preview of `promptField` on this node must show the affixes —\n * i.e. that key is what the run actually wraps. THE gate for the editor's Final\n * view, so a sibling field (llm-chat's `systemPrompt`, generate-script's\n * `styleGuide`) never renders a wrap the run doesn't perform.\n *\n * Two ways a key qualifies:\n * - it IS the core field ({@link promptAffixCoreField}), or\n * - it is a run-time FALLBACK candidate for the type\n * ({@link NODE_PROMPT_CANDIDATE_FIELDS}) — `computeNodePrompt` picks the\n * first present candidate and wraps THAT, so i2v's legacy `motionPrompt`\n * genuinely receives the affixes when `data.prompt` is empty.\n *\n * The two rules never collide: no override type declares candidates (guarded in\n * `node-prompt-fields.test.ts`).\n *\n * `undefined` means \"the node's primary prompt field\" — the common call.\n */\nexport function promptFieldCarriesAffixes(nodeType: string | undefined, promptField: string | undefined): boolean {\n const core = promptAffixCoreField(nodeType)\n if (core === undefined) return false\n if (promptField === undefined) return getPromptFields(nodeType)!.prompt === core\n return promptField === core || (NODE_PROMPT_CANDIDATE_FIELDS[nodeType!] ?? []).includes(promptField)\n}\n\n/** The affix-capable node types, derived from the registry (never hand-listed). */\nexport const PROMPT_AFFIX_NODE_TYPES: ReadonlySet<string> = new Set(\n Object.entries(NODE_PROMPT_FIELDS).filter(([, s]) => s.affixes !== false).map(([t]) => t),\n)\n","import { resolveNodeRefs } from \"@nodaro/shared/workflow\"\nimport { SOCIAL_POST_NODE_TYPES } from \"@nodaro/shared/social\"\nimport { computeLlmChatFields, computeNodePrompt, computeScriptTopic } from \"./resolve-prompt.js\"\n\n/**\n * Output nodes that send exactly what is WIRED into them — a webhook's\n * parameters, a social post's caption and media. When every wire produced\n * nothing in this run (a Filter List that kept no row, a node that was itself\n * skipped), there is nothing to send: the node is skipped with\n * `skipReason: \"empty_input\"` instead of posting an empty payload (a site that\n * validates its input answers 400 and alerts its owner) or failing the run\n * (a publish route refuses a post with no text and no media). A typed caption\n * with no wire at all is the author's choice and still runs, as before. ONE\n * list, read by the orchestrator's skip rule (`empty-input-skips.ts`).\n */\nexport const WIRED_OUTPUT_NODE_TYPES: ReadonlySet<string> = new Set([\"webhook-output\", ...SOCIAL_POST_NODE_TYPES])\n\n/**\n * Node types that send TEXT to a model or a voice and fail on an empty one —\n * `userInput: Too small` (llm-chat), \"no text found\" (text-to-speech) — so a\n * run with nothing upstream can skip them instead of failing. Never an image\n * or video node (an empty prompt is a legal request there) and never a social\n * post (an empty caption is the author's choice). ONE list, read by the\n * orchestrator's skip rule (`empty-input-skips.ts`); the engine never names a\n * type on its own.\n */\nexport const TEXT_REQUIRED_NODE_TYPES: ReadonlySet<string> = new Set([\n \"llm-chat\",\n \"ai-writer\",\n \"generate-script\",\n \"text-to-speech\",\n \"generate-music\",\n \"text-to-audio\",\n // Save to Collection refuses an empty record (\"nothing to save\"): a feed\n // with nothing new wired straight into it must skip it, not fail the run.\n \"collection-write\",\n // Generate Image: an empty prompt is a legal request in general (references\n // alone), but when the text it was WIRED produced nothing this run — the skip\n // rule's precondition — an empty prompt is a paid picture of nothing.\n \"generate-image\",\n])\n\n/**\n * The node's data without its prompt pre/post text: the skip decision is about\n * the CORE text. A prefix alone (\"Write a news item about:\") is not something\n * to send — with the affixes applied, an empty core read as non-empty and an\n * idle tick paid for a model call on the prefix.\n */\nconst withoutAffixes = (data: Record<string, unknown>): Record<string, unknown> => ({ ...data, promptPrefix: undefined, promptSuffix: undefined })\n\nexport interface NodeSendTextArgs {\n /** A list fan-out item (highest precedence). */\n override?: string\n /** The text wired into the node's main text input. */\n wired?: string\n /** llm-chat only: the text wired into `system-prompt`. */\n wiredSystemPrompt?: string\n refMap: ReadonlyMap<string, string>\n}\n\nconst present = (s?: string): s is string => typeof s === \"string\" && s.trim().length > 0\n\n/**\n * ai-writer's user input — the rule its sync-HTTP body applies: a fan-out\n * item, else the wired text, else the typed `userInput`, else the legacy\n * `prompt`. (ai-writer is renamed to llm-chat on load; saved graphs that\n * never reloaded still run it.)\n */\nexport function computeAiWriterInput(\n data: Record<string, unknown>,\n { override, wired }: Pick<NodeSendTextArgs, \"override\" | \"wired\">,\n): string {\n return [override, wired, data.userInput as string | undefined, data.prompt as string | undefined].find(present) ?? \"\"\n}\n\n/**\n * The text a text-requiring node would send, by the SAME rule its executor\n * uses (`computeLlmChatFields`, `computeScriptTopic`, `computeNodePrompt`,\n * `computeAiWriterInput`) — so \"would this node run on nothing?\" is answered\n * by the function that decides what it sends, not by a second reading of it.\n * Undefined for any other node type.\n */\nexport function computeNodeSendText(\n nodeType: string,\n data: Record<string, unknown>,\n args: NodeSendTextArgs,\n): string | undefined {\n const bare = withoutAffixes(data)\n switch (nodeType) {\n case \"llm-chat\":\n return computeLlmChatFields(bare, {\n override: args.override,\n wiredUserInput: args.wired,\n wiredSystemPrompt: args.wiredSystemPrompt,\n refMap: args.refMap,\n }).userInput\n case \"ai-writer\":\n return computeAiWriterInput(bare, args)\n case \"generate-script\":\n return computeScriptTopic(bare, { override: args.override, wired: args.wired, refMap: args.refMap })\n case \"generate-music\": {\n // A music node sings typed lyrics with no prompt; only both empty is nothing to send.\n const prompt = computeNodePrompt(nodeType, bare, { override: args.override, wired: args.wired, refMap: args.refMap })\n return present(prompt) ? prompt : present(data.lyrics as string | undefined) ? (data.lyrics as string) : \"\"\n }\n case \"text-to-speech\":\n case \"text-to-audio\":\n case \"generate-image\":\n return computeNodePrompt(nodeType, bare, { override: args.override, wired: args.wired, refMap: args.refMap })\n case \"collection-write\": {\n // The record's content: the item (a fan-out row, else what reached `in`),\n // else a title, text or link typed on the node. A picture alone is a\n // record too, but it never starves the node: only a wired TEXT input that\n // produced nothing can (the skip rule's precondition).\n // A typed field counts as the engines send it, its `{Node}` references\n // resolved: `{Feed || }` over a feed with nothing new is empty, so the\n // node skips instead of failing with an empty record (#1890).\n const typed = [data.title, data.text, data.link]\n .map((v) => (typeof v === \"string\" ? resolveNodeRefs(v, args.refMap) : v))\n .find((v): v is string => present(v as string | undefined))\n return [args.override, args.wired, typed].find(present) ?? \"\"\n }\n default:\n return undefined\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/read-node-direction.ts"],"names":["DIRECTION_FIELDS","DIRECTION_ID_MAX_CHARS","DIRECTION_ARRAY_CEILING","getRegisteredSubjectKeys","SUBJECT_CUSTOM_AGE_KEY","SUBJECT_ID_MAX_CHARS","SUBJECT_ARRAY_CEILING"],"mappings":"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","file":"chunk-7XKHDXCL.cjs","sourcesContent":["/**\n * Narrow readers turning UNTRUSTED persisted node data (`workflows.nodes`\n * JSONB — import, MCP write, node preset, a Studio-emitted graph) into the\n * typed `subject` / `direction` / `structured` levers `assembleImageInput`\n * accepts.\n * `buildPayload` has no zod and workflow writes are\n * `z.record(z.string(), z.unknown())`, so this blob may have been written years\n * ago by any client.\n *\n * Used by ALL THREE image-assembly sites so what the canvas accepts cannot\n * drift between them: the frontend single-node executor (`execute-node.ts`),\n * the orchestrator (`payload-builder.ts`), and the config-panel final-prompt\n * preview (`build-image-assemble-input.ts`).\n *\n * `readDirectionFields` is DERIVED FROM `DIRECTION_FIELDS`, never a hand list:\n * a dimension added to the registry is honored here by construction, so the\n * reader can never silently drop a key the wire schema and the renderer both\n * know about. Anything unrecognised is DROPPED, never thrown on — a malformed\n * blob must not fail a canvas run, and an unknown catalog ID already degrades\n * to `\"\"` inside each `get*PromptHint`, so this validates SHAPE only.\n *\n * BOUNDS MATCH THE WIRE SCHEMA where both exist, by SHARED CONSTANT:\n * `DIRECTION_ID_MAX_CHARS` and `DIRECTION_ARRAY_CEILING` are the same two\n * literals the `generate-image` route's `directionSchema` enforces, imported\n * from the registry rather than re-typed here — a body the route accepts and a\n * node the canvas re-runs must not disagree about which strings are ids.\n *\n * DELIBERATELY STRICTER THAN THE WIRE SCHEMA on `structured` values: the\n * route's `structuredPromptFieldsSchema` declares every field as a bare\n * `z.string().optional()` (unbounded), while `MAX_STRUCTURED_VALUE_CHARS` below\n * bounds them at 200. Do not \"fix\" that asymmetry by loosening HERE — this\n * reads a blob any client may have written years ago, and the values land\n * verbatim in `jobs.input_data.prompt`. Tightening the ROUTE instead would be a\n * new 400 on currently-accepted input, so a >200-char structured value stored\n * on a node is dropped on a canvas run while `POST /v1/generate-image` still\n * renders it. That is the one known divergence; it is bounded to a field the\n * canvas UI never writes that long.\n *\n * RETURNS `undefined`, NEVER `{}`: an empty object would still be a *defined*\n * `direction`, and the call sites' `...(x !== undefined ? { x } : {})` spread\n * would then hand `assembleImageInput` a defined-but-empty lever. Returning\n * `undefined` keeps that spread honest and the exact no-op branch taken.\n */\nimport {\n DIRECTION_ARRAY_CEILING,\n DIRECTION_FIELDS,\n DIRECTION_ID_MAX_CHARS,\n type DirectionFields,\n} from \"./direction-registry.js\"\nimport {\n SUBJECT_ARRAY_CEILING,\n SUBJECT_CUSTOM_AGE_KEY,\n SUBJECT_ID_MAX_CHARS,\n getRegisteredSubjectKeys,\n type SubjectFields,\n} from \"./subject-registry.js\"\nimport type { StructuredPromptFields } from \"./prompt-builder-structured-fields.js\"\n\n/**\n * Read a node's stored cinematic-direction ids. Accepts a single id OR an array\n * on every key (multi-pick dimensions carry arrays; a single-pick key may\n * legitimately carry one).\n *\n * The SEMANTIC per-dimension cap stays the renderer's slice (`maxPicks`) — this\n * reader does not know a row's pick budget and must not guess it. But it DOES\n * bound cardinality at `DIRECTION_ARRAY_CEILING`, the same ceiling the wire\n * schema enforces: the value is untrusted persisted JSONB (a node blob is\n * validated only as `z.record(z.string(), z.unknown())` on write), and handing\n * an unbounded array to `renderDirectionHints` would put an `includes`-dedupe\n * scan in front of that slice. Keeping the FIRST `DIRECTION_ARRAY_CEILING`\n * survivors is what the wire door already does to the same input.\n *\n * Junk is filtered BEFORE the cap, so valid ids sitting behind malformed\n * entries survive rather than being crowded out by them.\n */\nexport function readDirectionFields(value: unknown): DirectionFields | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined\n const src = value as Record<string, unknown>\n const out: Record<string, string | string[]> = {}\n for (const spec of DIRECTION_FIELDS) {\n const v = src[spec.key]\n if (typeof v === \"string\") {\n if (v.length > 0 && v.length <= DIRECTION_ID_MAX_CHARS) out[spec.key] = v\n } else if (Array.isArray(v)) {\n const kept: string[] = []\n for (const x of v) {\n if (kept.length >= DIRECTION_ARRAY_CEILING) break\n if (typeof x === \"string\" && x.length > 0 && x.length <= DIRECTION_ID_MAX_CHARS) {\n kept.push(x)\n }\n }\n if (kept.length > 0) out[spec.key] = kept\n }\n }\n return Object.keys(out).length > 0 ? (out as DirectionFields) : undefined\n}\n\n// ── subject ──────────────────────────────────────────────────────────────────\n\n/**\n * Read a node's stored SUBJECT ids — the flat Person / Styling / prop bag the\n * `subject` channel carries. Same three contracts as `readDirectionFields`:\n * drop-never-throw, `undefined` never `{}`, and bounds shared with the wire\n * door by CONSTANT (`SUBJECT_ID_MAX_CHARS` / `SUBJECT_ARRAY_CEILING`, which are\n * defined AS the direction constants) so a body the route accepts and the same\n * node re-run from the canvas cannot disagree about which strings are ids.\n *\n * DERIVED from `getRegisteredSubjectKeys()`, never a hand-authored field list —\n * that is the whole lesson of `readStructuredFields` below, whose hand table is\n * only viable because `StructuredPromptFields` is a small hand-authored type.\n * The subject vocabulary is ~54 catalog-derived fields PLUS deployment-registered\n * pack dimensions unknown at compile time, so it is read from the registry at\n * call time. Iterating the KEY SET (never `Object.keys(value)`) is also what\n * keeps an adversarial blob's key count off the hot path.\n *\n * The SEMANTIC per-dimension cap stays the renderer's job\n * (`normalizeSubjectFields`), exactly as `maxPicks` does for direction: this\n * reader bounds cardinality only. `customAge` is the one number on the wire and\n * is passed through finite-only — the renderer clamps it.\n */\nexport function readSubjectFields(value: unknown): SubjectFields | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined\n const src = value as Record<string, unknown>\n const out: Record<string, string | string[] | number> = {}\n for (const key of getRegisteredSubjectKeys()) {\n const v = src[key]\n if (key === SUBJECT_CUSTOM_AGE_KEY) {\n if (typeof v === \"number\" && Number.isFinite(v)) out[key] = v\n continue\n }\n if (typeof v === \"string\") {\n if (v.length > 0 && v.length <= SUBJECT_ID_MAX_CHARS) out[key] = v\n } else if (Array.isArray(v)) {\n // Junk is filtered BEFORE the cap, so valid ids sitting behind malformed\n // entries survive rather than being crowded out by them.\n const kept: string[] = []\n for (const x of v) {\n if (kept.length >= SUBJECT_ARRAY_CEILING) break\n if (typeof x === \"string\" && x.length > 0 && x.length <= SUBJECT_ID_MAX_CHARS) {\n kept.push(x)\n }\n }\n if (kept.length > 0) out[key] = kept\n }\n }\n return Object.keys(out).length > 0 ? (out as SubjectFields) : undefined\n}\n\n// ── structured ───────────────────────────────────────────────────────────────\n\ntype FieldKind = \"string\" | \"number\" | \"gender\"\nconst GENDERS = [\"man\", \"woman\", \"child\", \"non-binary\"] as const\nconst MAX_STRUCTURED_VALUE_CHARS = 200\n\ntype Group = Exclude<keyof StructuredPromptFields, \"mood\">\ntype GroupFields<K extends Group> = Record<keyof NonNullable<StructuredPromptFields[K]>, FieldKind>\n\n/**\n * A FIELD-BY-FIELD table (not a registry walk) on purpose:\n * `StructuredPromptFields` is a small HAND-AUTHORED type, not catalog-derived,\n * and the table is what blocks `person: { age: \"drop table\" }` from rendering\n * verbatim into `jobs.input_data.prompt` — `renderStructuredFields` never\n * throws on junk, but it does render it. Totality is enforced by the\n * `GroupFields<K>` / `{ [K in Group]: … }` mapped types: a new field or group\n * on the published type fails to typecheck here.\n */\nconst PERSON_FIELDS: GroupFields<\"person\"> = {\n age: \"number\",\n gender: \"gender\",\n hair: \"string\",\n eyes: \"string\",\n expression: \"string\",\n profession: \"string\",\n warriorType: \"string\",\n}\nconst STYLING_FIELDS: GroupFields<\"styling\"> = {\n mood: \"string\",\n lighting: \"string\",\n aesthetic: \"string\",\n colorLook: \"string\",\n}\nconst SETTING_FIELDS: GroupFields<\"setting\"> = {\n era: \"string\",\n atmosphere: \"string\",\n backdrop: \"string\",\n}\nconst CAMERA_FIELDS: GroupFields<\"camera\"> = {\n framing: \"string\",\n motion: \"string\",\n format: \"string\",\n}\nconst LENS_FIELDS: GroupFields<\"lens\"> = { focalLength: \"string\", aperture: \"string\" }\n\nconst STRUCTURED_GROUPS: { [K in Group]: GroupFields<K> } = {\n person: PERSON_FIELDS,\n styling: STYLING_FIELDS,\n setting: SETTING_FIELDS,\n camera: CAMERA_FIELDS,\n lens: LENS_FIELDS,\n}\n\nfunction readField(v: unknown, kind: FieldKind): string | number | undefined {\n if (kind === \"number\") return typeof v === \"number\" && Number.isFinite(v) ? v : undefined\n if (typeof v !== \"string\" || v.length === 0 || v.length > MAX_STRUCTURED_VALUE_CHARS) {\n return undefined\n }\n if (kind === \"gender\") return (GENDERS as readonly string[]).includes(v) ? v : undefined\n return v\n}\n\n/** Read a node's stored Path-1 structured prompt fields. Same drop-never-throw\n * and `undefined`-never-`{}` contract as `readDirectionFields`. */\nexport function readStructuredFields(value: unknown): StructuredPromptFields | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined\n const src = value as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const group of Object.keys(STRUCTURED_GROUPS) as Group[]) {\n const raw = src[group]\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) continue\n const rawRec = raw as Record<string, unknown>\n const kept: Record<string, unknown> = {}\n for (const [field, kind] of Object.entries(\n STRUCTURED_GROUPS[group] as Record<string, FieldKind>,\n )) {\n const v = readField(rawRec[field], kind)\n if (v !== undefined) kept[field] = v\n }\n if (Object.keys(kept).length > 0) out[group] = kept\n }\n const mood = readField(src.mood, \"string\")\n if (mood !== undefined) out.mood = mood\n return Object.keys(out).length > 0 ? (out as StructuredPromptFields) : undefined\n}\n"]}