@nodaro/prompts 1.0.1
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/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import {
|
|
3
|
+
PEOPLE,
|
|
4
|
+
PERSON_DIMENSION_ORDER,
|
|
5
|
+
PERSON_DIMENSION_LABELS,
|
|
6
|
+
PERSON_FIELD_BY_DIMENSION,
|
|
7
|
+
getPersonDimensionLimit,
|
|
8
|
+
} from "./person.js"
|
|
9
|
+
import { STYLINGS, STYLING_DIMENSION_ORDER, STYLING_DIMENSION_LABELS, STYLING_FIELD_BY_DIMENSION, getStylingDimensionLimit } from "./styling.js"
|
|
10
|
+
import { FRAMINGS, FRAMING_CATEGORY_ORDER, FRAMING_CATEGORY_LABELS, FRAMING_FIELD_BY_CATEGORY, getFramingCategoryLimit } from "./framing.js"
|
|
11
|
+
import { LENSES } from "./lens.js"
|
|
12
|
+
import { CAMERA_FORMATS } from "./camera-format.js"
|
|
13
|
+
|
|
14
|
+
// ─── Descriptor model ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** A catalog entry as the analyzer consumes it. Flat catalogs lack
|
|
17
|
+
* dimension/category; discriminated catalogs carry exactly one. */
|
|
18
|
+
interface AnalyzerEntry {
|
|
19
|
+
readonly id: string
|
|
20
|
+
readonly label: string
|
|
21
|
+
readonly description: string
|
|
22
|
+
readonly dimension?: string
|
|
23
|
+
readonly category?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PickerApplyMode = "override" | "overwrite-detected" | "fill-empty"
|
|
27
|
+
type ApplyCleanup = (patch: Record<string, unknown>, mode: PickerApplyMode) => void
|
|
28
|
+
|
|
29
|
+
/** Describes how to build an analyzer spec for one picker type. Three shapes:
|
|
30
|
+
* - "discriminated": ONE catalog whose entries carry `dimension` or `category`;
|
|
31
|
+
* `order`/`fieldByKey`/`labels` translate keys → fields/labels; `limitFn`
|
|
32
|
+
* gives the per-key cardinality.
|
|
33
|
+
* - "flat": a single-value catalog (lens, camera-format) → one limit-1
|
|
34
|
+
* dimension whose key == the node-data field. */
|
|
35
|
+
export type PickerAnalyzerDescriptor =
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: "discriminated"
|
|
38
|
+
readonly toolName: string
|
|
39
|
+
readonly discriminator: "dimension" | "category"
|
|
40
|
+
readonly order: ReadonlyArray<string>
|
|
41
|
+
readonly fieldByKey: Readonly<Record<string, string>>
|
|
42
|
+
readonly labels: Readonly<Record<string, string>>
|
|
43
|
+
readonly entries: ReadonlyArray<AnalyzerEntry>
|
|
44
|
+
readonly limitFn: (key: string) => number
|
|
45
|
+
readonly excludedIds?: ReadonlySet<string>
|
|
46
|
+
readonly cleanup?: ApplyCleanup
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: "flat"
|
|
50
|
+
readonly toolName: string
|
|
51
|
+
readonly field: string
|
|
52
|
+
readonly label: string
|
|
53
|
+
readonly entries: ReadonlyArray<AnalyzerEntry>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Registry (person only in Task 2; +4 in Task 3) ─────────────────────────
|
|
57
|
+
|
|
58
|
+
const PERSON_EXCLUDED = new Set<string>(["age-custom"])
|
|
59
|
+
|
|
60
|
+
const personCleanup: ApplyCleanup = (patch, mode) => {
|
|
61
|
+
if (mode === "override") {
|
|
62
|
+
patch.customAge = undefined
|
|
63
|
+
patch.lips = undefined
|
|
64
|
+
} else if ("age" in patch && patch.age !== "age-custom") {
|
|
65
|
+
patch.customAge = undefined
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const PICKER_ANALYZER_REGISTRY = {
|
|
70
|
+
person: {
|
|
71
|
+
kind: "discriminated",
|
|
72
|
+
toolName: "emit_person",
|
|
73
|
+
discriminator: "dimension",
|
|
74
|
+
order: PERSON_DIMENSION_ORDER as ReadonlyArray<string>,
|
|
75
|
+
fieldByKey: PERSON_FIELD_BY_DIMENSION as Readonly<Record<string, string>>,
|
|
76
|
+
labels: PERSON_DIMENSION_LABELS as Readonly<Record<string, string>>,
|
|
77
|
+
entries: PEOPLE as ReadonlyArray<AnalyzerEntry>,
|
|
78
|
+
limitFn: (k) => getPersonDimensionLimit(k as never),
|
|
79
|
+
excludedIds: PERSON_EXCLUDED,
|
|
80
|
+
cleanup: personCleanup,
|
|
81
|
+
},
|
|
82
|
+
styling: {
|
|
83
|
+
kind: "discriminated",
|
|
84
|
+
toolName: "emit_styling",
|
|
85
|
+
discriminator: "dimension",
|
|
86
|
+
order: STYLING_DIMENSION_ORDER as ReadonlyArray<string>,
|
|
87
|
+
fieldByKey: STYLING_FIELD_BY_DIMENSION as Readonly<Record<string, string>>,
|
|
88
|
+
labels: STYLING_DIMENSION_LABELS as Readonly<Record<string, string>>,
|
|
89
|
+
entries: STYLINGS as ReadonlyArray<AnalyzerEntry>,
|
|
90
|
+
limitFn: (k) => getStylingDimensionLimit(k as never),
|
|
91
|
+
},
|
|
92
|
+
framing: {
|
|
93
|
+
kind: "discriminated",
|
|
94
|
+
toolName: "emit_framing",
|
|
95
|
+
discriminator: "category",
|
|
96
|
+
order: FRAMING_CATEGORY_ORDER as ReadonlyArray<string>,
|
|
97
|
+
fieldByKey: FRAMING_FIELD_BY_CATEGORY as Readonly<Record<string, string>>,
|
|
98
|
+
labels: FRAMING_CATEGORY_LABELS as Readonly<Record<string, string>>,
|
|
99
|
+
entries: FRAMINGS as ReadonlyArray<AnalyzerEntry>,
|
|
100
|
+
limitFn: (k) => getFramingCategoryLimit(k as never),
|
|
101
|
+
},
|
|
102
|
+
lens: {
|
|
103
|
+
kind: "flat",
|
|
104
|
+
toolName: "emit_lens",
|
|
105
|
+
field: "lens",
|
|
106
|
+
label: "Lens",
|
|
107
|
+
entries: LENSES as ReadonlyArray<AnalyzerEntry>,
|
|
108
|
+
},
|
|
109
|
+
"camera-format": {
|
|
110
|
+
kind: "flat",
|
|
111
|
+
toolName: "emit_camera_format",
|
|
112
|
+
field: "cameraFormat",
|
|
113
|
+
label: "Camera / Film Stock",
|
|
114
|
+
entries: CAMERA_FORMATS as ReadonlyArray<AnalyzerEntry>,
|
|
115
|
+
},
|
|
116
|
+
} satisfies Record<string, PickerAnalyzerDescriptor>
|
|
117
|
+
|
|
118
|
+
export type PickerType = keyof typeof PICKER_ANALYZER_REGISTRY
|
|
119
|
+
export const PICKER_TYPES = Object.keys(PICKER_ANALYZER_REGISTRY) as PickerType[]
|
|
120
|
+
export const ANALYZABLE_PICKER_TYPES: ReadonlySet<string> = new Set(PICKER_TYPES)
|
|
121
|
+
export function isAnalyzablePicker(t: string): t is PickerType {
|
|
122
|
+
return ANALYZABLE_PICKER_TYPES.has(t)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Spec model ──────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export interface PickerDimensionSpec {
|
|
128
|
+
/** Catalog dimension id, e.g. "hair-color". Also the JSON key in the emitted pickerJson. */
|
|
129
|
+
readonly dimension: string
|
|
130
|
+
/** Target node-data field, e.g. "hairColor". */
|
|
131
|
+
readonly field: string
|
|
132
|
+
/** Human label/description, for the system-prompt legend. */
|
|
133
|
+
readonly label: string
|
|
134
|
+
/** 1 = single select; 2/3 = array with maxItems. */
|
|
135
|
+
readonly limit: number
|
|
136
|
+
/** Allowed catalog entry ids (the forced enum). */
|
|
137
|
+
readonly entryIds: ReadonlyArray<string>
|
|
138
|
+
/** id → human label/description, for the system-prompt legend. */
|
|
139
|
+
readonly legend: ReadonlyArray<{ id: string; label: string; description: string }>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface PickerAnalyzerSpec {
|
|
143
|
+
readonly pickerType: PickerType
|
|
144
|
+
readonly toolName: string
|
|
145
|
+
readonly dimensions: ReadonlyArray<PickerDimensionSpec>
|
|
146
|
+
readonly cleanup?: ApplyCleanup
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildPickerAnalyzerSpec(pickerType: PickerType): PickerAnalyzerSpec {
|
|
150
|
+
const d = PICKER_ANALYZER_REGISTRY[pickerType] as PickerAnalyzerDescriptor
|
|
151
|
+
if (d.kind === "flat") {
|
|
152
|
+
return {
|
|
153
|
+
pickerType,
|
|
154
|
+
toolName: d.toolName,
|
|
155
|
+
dimensions: [
|
|
156
|
+
{
|
|
157
|
+
dimension: d.field,
|
|
158
|
+
field: d.field,
|
|
159
|
+
label: d.label,
|
|
160
|
+
limit: 1,
|
|
161
|
+
entryIds: d.entries.map((e) => e.id),
|
|
162
|
+
legend: d.entries.map((e) => ({ id: e.id, label: e.label, description: e.description })),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const dimensions: PickerDimensionSpec[] = d.order.map((key) => {
|
|
168
|
+
const entries = d.entries.filter(
|
|
169
|
+
(e) =>
|
|
170
|
+
(d.discriminator === "dimension" ? e.dimension : e.category) === key &&
|
|
171
|
+
!(d.excludedIds?.has(e.id)),
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
dimension: key,
|
|
175
|
+
field: d.fieldByKey[key],
|
|
176
|
+
label: d.labels[key] ?? key,
|
|
177
|
+
limit: d.limitFn(key),
|
|
178
|
+
entryIds: entries.map((e) => e.id),
|
|
179
|
+
legend: entries.map((e) => ({ id: e.id, label: e.label, description: e.description })),
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
return { pickerType, toolName: d.toolName, dimensions, cleanup: d.cleanup }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Zod schema / legend / analyzer cache (unchanged logic) ──────────────────
|
|
186
|
+
|
|
187
|
+
/** Zod object: each dimension is an optional enum (single) or capped enum array
|
|
188
|
+
* (multi). `.strict()` blocks unknown keys. Mirrors the field cardinality. */
|
|
189
|
+
export function buildPickerZodSchema(
|
|
190
|
+
spec: PickerAnalyzerSpec,
|
|
191
|
+
): z.ZodType<Record<string, string | string[]>, z.ZodTypeDef, unknown> {
|
|
192
|
+
const shape: Record<string, z.ZodTypeAny> = {}
|
|
193
|
+
for (const d of spec.dimensions) {
|
|
194
|
+
const ids = d.entryIds as unknown as [string, ...string[]]
|
|
195
|
+
const enumZ = z.enum(ids)
|
|
196
|
+
shape[d.dimension] = d.limit > 1 ? z.array(enumZ).max(d.limit).optional() : enumZ.optional()
|
|
197
|
+
}
|
|
198
|
+
// dynamic shape → Zod can't infer the narrowed output type; runtime-validated by picker-analyzer-registry.test.ts
|
|
199
|
+
return z.object(shape).strict() as unknown as z.ZodType<
|
|
200
|
+
Record<string, string | string[]>,
|
|
201
|
+
z.ZodTypeDef,
|
|
202
|
+
unknown
|
|
203
|
+
>
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface PickerAnalyzer {
|
|
207
|
+
readonly spec: PickerAnalyzerSpec
|
|
208
|
+
readonly schema: z.ZodType<Record<string, string | string[]>, z.ZodTypeDef, unknown>
|
|
209
|
+
readonly legend: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ANALYZER_CACHE = new Map<PickerType, PickerAnalyzer>()
|
|
213
|
+
|
|
214
|
+
/** Memoized analyzer build: spec → Zod schema → legend, computed once per
|
|
215
|
+
* picker type and cached at module level. The three artifacts are catalog-
|
|
216
|
+
* derived and stable, so the per-request route handler can reuse them instead
|
|
217
|
+
* of rebuilding all three on every analysis call. */
|
|
218
|
+
export function getPickerAnalyzer(pickerType: PickerType): PickerAnalyzer {
|
|
219
|
+
const cached = ANALYZER_CACHE.get(pickerType)
|
|
220
|
+
if (cached) return cached
|
|
221
|
+
const spec = buildPickerAnalyzerSpec(pickerType)
|
|
222
|
+
const analyzer: PickerAnalyzer = {
|
|
223
|
+
spec,
|
|
224
|
+
schema: buildPickerZodSchema(spec),
|
|
225
|
+
legend: buildPickerLegend(spec),
|
|
226
|
+
}
|
|
227
|
+
ANALYZER_CACHE.set(pickerType, analyzer)
|
|
228
|
+
return analyzer
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Gaps + multi-picker union spec ──────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
export const GAPS_SCHEMA = z
|
|
234
|
+
.object({
|
|
235
|
+
missingItems: z
|
|
236
|
+
.array(
|
|
237
|
+
z.object({
|
|
238
|
+
picker: z.string(),
|
|
239
|
+
dimension: z.string(),
|
|
240
|
+
observed: z.string().max(120),
|
|
241
|
+
}),
|
|
242
|
+
)
|
|
243
|
+
.max(8)
|
|
244
|
+
.default([]),
|
|
245
|
+
missingCategories: z
|
|
246
|
+
.array(
|
|
247
|
+
z.object({
|
|
248
|
+
picker: z.string(),
|
|
249
|
+
suggestedDimension: z.string(),
|
|
250
|
+
observed: z.string().max(120),
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
.max(8)
|
|
254
|
+
.default([]),
|
|
255
|
+
})
|
|
256
|
+
.default({ missingItems: [], missingCategories: [] })
|
|
257
|
+
|
|
258
|
+
export interface PickerGaps {
|
|
259
|
+
readonly missingItems: ReadonlyArray<{ picker: string; dimension: string; observed: string }>
|
|
260
|
+
readonly missingCategories: ReadonlyArray<{
|
|
261
|
+
picker: string
|
|
262
|
+
suggestedDimension: string
|
|
263
|
+
observed: string
|
|
264
|
+
}>
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface MultiPickerAnalyzerSpec {
|
|
268
|
+
readonly schema: z.ZodType<Record<string, unknown>, z.ZodTypeDef, unknown>
|
|
269
|
+
readonly toolName: string
|
|
270
|
+
readonly legend: string
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const MULTI_CACHE = new Map<string, MultiPickerAnalyzerSpec>()
|
|
274
|
+
|
|
275
|
+
/** Build ONE forced-tool schema spanning the given pickers (each section
|
|
276
|
+
* optional so an omitted picker doesn't trigger a validation retry) plus the
|
|
277
|
+
* capped `gaps` sidecar. Memoized by the sorted picker-set key. */
|
|
278
|
+
export function buildMultiPickerAnalyzerSpec(types: ReadonlyArray<PickerType>): MultiPickerAnalyzerSpec {
|
|
279
|
+
const sorted = [...new Set(types)].sort()
|
|
280
|
+
const key = sorted.join(",")
|
|
281
|
+
const cached = MULTI_CACHE.get(key)
|
|
282
|
+
if (cached) return cached
|
|
283
|
+
|
|
284
|
+
const shape: Record<string, z.ZodTypeAny> = {}
|
|
285
|
+
const legendParts: string[] = []
|
|
286
|
+
for (const t of sorted) {
|
|
287
|
+
const spec = buildPickerAnalyzerSpec(t)
|
|
288
|
+
shape[t] = buildPickerZodSchema(spec).optional()
|
|
289
|
+
legendParts.push(`# ${t.toUpperCase()} PICKER\n${buildPickerLegend(spec)}`)
|
|
290
|
+
}
|
|
291
|
+
shape.gaps = GAPS_SCHEMA
|
|
292
|
+
const result: MultiPickerAnalyzerSpec = {
|
|
293
|
+
schema: z.object(shape).strict() as unknown as MultiPickerAnalyzerSpec["schema"],
|
|
294
|
+
toolName: "emit_pickers",
|
|
295
|
+
legend: legendParts.join("\n\n"),
|
|
296
|
+
}
|
|
297
|
+
MULTI_CACHE.set(key, result)
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Human-readable legend appended to the system prompt so enum ids are meaningful. */
|
|
302
|
+
export function buildPickerLegend(spec: PickerAnalyzerSpec): string {
|
|
303
|
+
const lines: string[] = []
|
|
304
|
+
for (const d of spec.dimensions) {
|
|
305
|
+
const cap = d.limit > 1 ? `up to ${d.limit}` : "one"
|
|
306
|
+
lines.push(`## ${d.label} (key: "${d.dimension}", choose ${cap})`)
|
|
307
|
+
for (const e of d.legend) {
|
|
308
|
+
lines.push(`- ${e.id}: ${e.label}${e.description ? ` — ${e.description}` : ""}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return lines.join("\n")
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── applyPickerJson (cleanup now via spec, not hardcoded person) ────────────
|
|
315
|
+
|
|
316
|
+
function isEmptyValue(v: unknown): boolean {
|
|
317
|
+
if (v === undefined || v === null) return true
|
|
318
|
+
if (Array.isArray(v)) return v.length === 0
|
|
319
|
+
return String(v).length === 0
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function coerce(value: unknown, limit: number): string | string[] {
|
|
323
|
+
const arr = Array.isArray(value) ? value.map(String) : [String(value)]
|
|
324
|
+
return limit > 1 ? arr.slice(0, limit) : arr[0]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Produces the patch to merge into the picker node's data. Touches ONLY the
|
|
329
|
+
* dimension fields (never label/preText/postText/maxItemsPerRow). `override`
|
|
330
|
+
* also clears undetected dimension fields, and runs the picker's `cleanup`
|
|
331
|
+
* (e.g. person resets customAge + clears the deprecated `lips` field).
|
|
332
|
+
*/
|
|
333
|
+
export function applyPickerJson(
|
|
334
|
+
current: Record<string, unknown>,
|
|
335
|
+
pickerJson: Record<string, unknown>,
|
|
336
|
+
mode: PickerApplyMode,
|
|
337
|
+
spec: PickerAnalyzerSpec,
|
|
338
|
+
): Record<string, unknown> {
|
|
339
|
+
const patch: Record<string, unknown> = {}
|
|
340
|
+
for (const d of spec.dimensions) {
|
|
341
|
+
const incoming = pickerJson[d.dimension]
|
|
342
|
+
const present = !isEmptyValue(incoming)
|
|
343
|
+
if (mode === "override") {
|
|
344
|
+
patch[d.field] = present ? coerce(incoming, d.limit) : undefined
|
|
345
|
+
} else if (mode === "overwrite-detected") {
|
|
346
|
+
if (present) patch[d.field] = coerce(incoming, d.limit)
|
|
347
|
+
} else {
|
|
348
|
+
if (present && isEmptyValue(current[d.field])) patch[d.field] = coerce(incoming, d.limit)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
spec.cleanup?.(patch, mode)
|
|
352
|
+
return patch
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Edge-derived fan-out selection (shared by frontend + backend) ───────────
|
|
356
|
+
|
|
357
|
+
/** The analyzable picker node types wired to a producer's `picker-json` output
|
|
358
|
+
* (deduped). The ONE definition of edge-derived selection; the frontend
|
|
359
|
+
* execute path and the backend orchestrator both call this so they can't
|
|
360
|
+
* drift. Accepts minimal structural node/edge shapes so both layers' richer
|
|
361
|
+
* types satisfy it. */
|
|
362
|
+
export function pickerFanoutTargets(
|
|
363
|
+
producerId: string,
|
|
364
|
+
edges: ReadonlyArray<{ source: string; target: string; sourceHandle?: string | null }>,
|
|
365
|
+
nodes: ReadonlyArray<{ id: string; type?: string }>,
|
|
366
|
+
): PickerType[] {
|
|
367
|
+
const out = new Set<string>()
|
|
368
|
+
for (const e of edges) {
|
|
369
|
+
if (e.source !== producerId || e.sourceHandle !== "picker-json") continue
|
|
370
|
+
const t = nodes.find((n) => n.id === e.target)?.type
|
|
371
|
+
if (t && isAnalyzablePicker(t)) out.add(t)
|
|
372
|
+
}
|
|
373
|
+
return [...out] as PickerType[]
|
|
374
|
+
}
|