@nodaro/prompts 1.6.0 → 1.7.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/prompts",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Nodaro's prompt-engineering layer — person/picker catalogs with prompt hints, identity-lock clauses, entity prompt builders, brand presets, and prompt/reference assembly shared by the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { MODEL_CATALOG } from "@nodaro/shared"
3
+ import { getPromptDoctrine } from "../provider-prompt-doctrine.js"
4
+
5
+ /**
6
+ * The badge's truthfulness guarantee (Cine build-brief §7 — never overclaim):
7
+ * every video GENERATION model is either doctrine-covered or DELIBERATELY
8
+ * generic. A new video model that is neither fails this test — forcing the
9
+ * author to write a sourced doctrine or consciously add it to the generic
10
+ * set with a reason.
11
+ */
12
+
13
+ /** Models deliberately left WITHOUT a doctrine, with the reason. */
14
+ const DELIBERATELY_GENERIC: ReadonlyMap<string, string> = new Map([
15
+ // Older ByteDance engine (pre-Seedance-2 surface: single image, no refs/audio
16
+ // levers) — mapping the Seedance family doctrine would overclaim.
17
+ ["bytedance-lite", "older engine, different surface"],
18
+ ["bytedance-pro", "older engine, different surface"],
19
+ ["bytedance-pro-fast", "older engine, different surface"],
20
+ // Hailuo pre-H3 tiers: single-image i2v without the multimodal reference
21
+ // surface the H3 doctrine teaches.
22
+ ["hailuo-2.3", "pre-H3 tier without the multimodal surface"],
23
+ ["hailuo-2.3-pro", "pre-H3 tier without the multimodal surface"],
24
+ ["hailuo-standard", "pre-H3 tier without the multimodal surface"],
25
+ // Legacy/simple tiers with no vendor guidance beyond the platform default.
26
+ ["minimax", "legacy 5s tier, no vendor guide"],
27
+ ["seedance", "legacy Seedance 1.x, superseded"],
28
+ ["ltx-2.3-fast", "no vendor prompt guide published"],
29
+ ["sora2", "roster utility — no first-party guide via KIE"],
30
+ ["sora2-pro", "roster utility — no first-party guide via KIE"],
31
+ ])
32
+
33
+ /** Utility modes that are driven by inputs, not prose prompting — doctrine
34
+ * coverage isn't meaningful for them (spec excludes them from the roster). */
35
+ const UTILITY_ONLY_MODES = new Set(["extend", "motion-transfer", "lip-sync", "video-upscale", "v2v", "video-analysis", "video-audit"])
36
+
37
+ describe("doctrine roster completeness", () => {
38
+ const videoGenerationIds = Object.values(MODEL_CATALOG)
39
+ .filter((m) => m.kind === "video")
40
+ .filter((m) => m.modes?.some((mode) => (mode === "t2v" || mode === "i2v") && !UTILITY_ONLY_MODES.has(mode)))
41
+ .map((m) => m.id)
42
+
43
+ it("covers every video generation model — or lists it as deliberately generic", () => {
44
+ const uncovered = videoGenerationIds.filter(
45
+ (id) => getPromptDoctrine(id) === undefined && !DELIBERATELY_GENERIC.has(id),
46
+ )
47
+ expect(
48
+ uncovered,
49
+ `New video model(s) with neither a doctrine nor a deliberate-generic entry: ${uncovered.join(", ")}. ` +
50
+ "Write a sourced doctrine or add to DELIBERATELY_GENERIC with a reason.",
51
+ ).toEqual([])
52
+ })
53
+
54
+ it("the deliberate-generic set stays honest (no entry that is actually covered)", () => {
55
+ for (const id of DELIBERATELY_GENERIC.keys()) {
56
+ expect(getPromptDoctrine(id), `${id} is covered — remove it from DELIBERATELY_GENERIC`).toBeUndefined()
57
+ }
58
+ })
59
+ })
@@ -77,7 +77,17 @@ describe("registry membership (Task 2: person only)", () => {
77
77
  describe("registry invariants (all analyzable pickers)", () => {
78
78
  it("registers the batch", () => {
79
79
  expect(new Set(PICKER_TYPES)).toEqual(
80
- new Set(["person", "styling", "framing", "lens", "camera-format"]),
80
+ new Set([
81
+ // multi-dim (discriminated)
82
+ "person", "styling", "framing", "lighting", "temporal", "exposure-settings",
83
+ "music-genre", "music-mood", "instrumentation", "voice-character", "voice-delivery",
84
+ // single-value (flat)
85
+ "lens", "camera-format", "setting", "atmosphere", "style", "mood", "color-look",
86
+ "photographer", "aesthetic", "era", "photo-genre", "backdrop", "render-quality",
87
+ "composition-effects", "post-process-effects", "action-fx", "loop-subject",
88
+ "transition", "character-fx", "pose", "material", "held-prop", "camera-motion",
89
+ "animal", "vehicle", "weapon", "furniture",
90
+ ]),
81
91
  )
82
92
  })
83
93
  it.each(PICKER_TYPES)("%s: every dimension enum equals its catalog ids and is non-empty", (t) => {
@@ -13,7 +13,7 @@ describe("PROVIDER_PROMPT_DOCTRINES", () => {
13
13
  expect(d.providers.length).toBeGreaterThan(0)
14
14
  for (const p of d.providers) expect(MODEL_CATALOG[p]).toBeDefined()
15
15
  expect(d.tips.length).toBeGreaterThanOrEqual(3)
16
- expect(d.tips.length).toBeLessThanOrEqual(6)
16
+ expect(d.tips.length).toBeLessThanOrEqual(7)
17
17
  for (const t of d.tips) expect(t.length).toBeLessThanOrEqual(220)
18
18
  expect(d.doctrine.length).toBeGreaterThan(500)
19
19
  expect(d.heading.length).toBeGreaterThan(0)
@@ -23,9 +23,11 @@ describe("PROVIDER_PROMPT_DOCTRINES", () => {
23
23
  it("resolves by provider id, returns undefined/[] for providers without doctrine", () => {
24
24
  expect(getPromptDoctrine("seedance-2")).toBeDefined()
25
25
  expect(getPromptDoctrine("seedance-2-fast")).toBeDefined()
26
- expect(getPromptDoctrine("veo3.1")).toBeUndefined()
26
+ // bytedance-lite/pro + hailuo-2.3 are DELIBERATELY uncovered (older engines
27
+ // whose surfaces differ — mapping the family doctrine would overclaim).
28
+ expect(getPromptDoctrine("bytedance-lite")).toBeUndefined()
27
29
  expect(getPromptTips("seedance-2").length).toBeGreaterThan(0)
28
- expect(getPromptTips("veo3.1")).toEqual([])
30
+ expect(getPromptTips("bytedance-lite")).toEqual([])
29
31
  })
30
32
 
31
33
  it("seedance doctrine encodes the official rules and bans the unstable patterns", () => {
package/src/index.ts CHANGED
@@ -65,3 +65,4 @@ export * from "./factory-presets.js"
65
65
  export * from "./style-presets.js"
66
66
  export * from "./object-asset-presets.js"
67
67
  export * from "./factory-snippets/index.js"
68
+ export * from "./picker-wiring.js"
@@ -10,6 +10,36 @@ import { STYLINGS, STYLING_DIMENSION_ORDER, STYLING_DIMENSION_LABELS, STYLING_FI
10
10
  import { FRAMINGS, FRAMING_CATEGORY_ORDER, FRAMING_CATEGORY_LABELS, FRAMING_FIELD_BY_CATEGORY, getFramingCategoryLimit } from "./framing.js"
11
11
  import { LENSES } from "./lens.js"
12
12
  import { CAMERA_FORMATS } from "./camera-format.js"
13
+ import { ANIMALS, VEHICLES, WEAPONS, FURNITURE } from "@nodaro/shared"
14
+ import { SETTINGS } from "./setting.js"
15
+ import { ATMOSPHERES } from "./atmosphere.js"
16
+ import { STYLES } from "./style.js"
17
+ import { MOODS } from "./mood.js"
18
+ import { COLOR_LOOKS } from "./color-look.js"
19
+ import { PHOTOGRAPHERS } from "./photographer.js"
20
+ import { AESTHETICS } from "./aesthetic.js"
21
+ import { ERAS } from "./era.js"
22
+ import { PHOTO_GENRES } from "./photo-genre.js"
23
+ import { BACKDROPS } from "./backdrop.js"
24
+ import { RENDER_QUALITIES } from "./render-quality.js"
25
+ import { COMPOSITION_EFFECTS } from "./composition-effects.js"
26
+ import { POST_PROCESS_EFFECTS } from "./post-process-effects.js"
27
+ import { ACTION_FX } from "./action-fx.js"
28
+ import { LOOP_SUBJECTS } from "./loop-subject.js"
29
+ import { TRANSITIONS } from "./transitions.js"
30
+ import { CHARACTER_FX } from "./character-fx.js"
31
+ import { POSES } from "./pose.js"
32
+ import { MATERIALS } from "./materials.js"
33
+ import { HELD_PROPS } from "./held-prop.js"
34
+ import { CAMERA_MOTIONS } from "./camera-motions.js"
35
+ import { LIGHTINGS, LIGHTING_CATEGORY_ORDER, LIGHTING_CATEGORY_LABELS, LIGHTING_FIELD_BY_CATEGORY } from "./lighting.js"
36
+ import { TEMPORALS } from "./temporal.js"
37
+ import { EXPOSURE_SETTINGS } from "./exposure-settings.js"
38
+ import { MUSIC_GENRES, MUSIC_ERAS } from "./music-genre.js"
39
+ import { MUSIC_ENERGIES, MUSIC_EMOTIONS, MUSIC_VIBES } from "./music-mood.js"
40
+ import { INSTRUMENTS, PRODUCTION_STYLES, VOCAL_PRESENCE, SINGING_STYLES } from "./instrumentation.js"
41
+ import { VOICE_AGES, VOICE_GENDERS, VOICE_LANGUAGES, VOICE_ACCENTS, VOICE_TIMBRES } from "./voice-character.js"
42
+ import { VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES } from "./voice-delivery.js"
13
43
 
14
44
  // ─── Descriptor model ────────────────────────────────────────────────────────
15
45
 
@@ -57,6 +87,16 @@ export type PickerAnalyzerDescriptor =
57
87
 
58
88
  const PERSON_EXCLUDED = new Set<string>(["age-custom"])
59
89
 
90
+ /** Tag catalog entries with an explicit dimension — for discriminated pickers
91
+ * whose fields live in SEPARATE catalogs (music/voice) rather than one
92
+ * discriminated catalog. */
93
+ function tagDim<T extends { id: string; label: string; description: string }>(
94
+ arr: ReadonlyArray<T>,
95
+ dimension: string,
96
+ ): ReadonlyArray<AnalyzerEntry> {
97
+ return arr.map((e) => ({ id: e.id, label: e.label, description: e.description, dimension }))
98
+ }
99
+
60
100
  const personCleanup: ApplyCleanup = (patch, mode) => {
61
101
  if (mode === "override") {
62
102
  patch.customAge = undefined
@@ -113,10 +153,160 @@ export const PICKER_ANALYZER_REGISTRY = {
113
153
  label: "Camera / Film Stock",
114
154
  entries: CAMERA_FORMATS as ReadonlyArray<AnalyzerEntry>,
115
155
  },
156
+
157
+ // ─── Text-to-picker expansion (Cine AI Fill): every remaining catalog ─────
158
+ // Flat single-value pickers — field names match the picker wiring's
159
+ // valueField (and the node-data shape the published-app input card writes).
160
+ setting: { kind: "flat", toolName: "emit_setting", field: "setting", label: "Setting", entries: SETTINGS as ReadonlyArray<AnalyzerEntry> },
161
+ atmosphere: { kind: "flat", toolName: "emit_atmosphere", field: "atmosphere", label: "Atmosphere", entries: ATMOSPHERES as ReadonlyArray<AnalyzerEntry> },
162
+ style: { kind: "flat", toolName: "emit_style", field: "style", label: "Style", entries: STYLES as ReadonlyArray<AnalyzerEntry> },
163
+ mood: { kind: "flat", toolName: "emit_mood", field: "mood", label: "Mood", entries: MOODS as ReadonlyArray<AnalyzerEntry> },
164
+ "color-look": { kind: "flat", toolName: "emit_color_look", field: "colorLook", label: "Color / Look", entries: COLOR_LOOKS as ReadonlyArray<AnalyzerEntry> },
165
+ photographer: { kind: "flat", toolName: "emit_photographer", field: "photographer", label: "Photographer / Artist", entries: PHOTOGRAPHERS as ReadonlyArray<AnalyzerEntry> },
166
+ aesthetic: { kind: "flat", toolName: "emit_aesthetic", field: "aesthetic", label: "Aesthetic / Microtrend", entries: AESTHETICS as ReadonlyArray<AnalyzerEntry> },
167
+ era: { kind: "flat", toolName: "emit_era", field: "era", label: "Era / Period", entries: ERAS as ReadonlyArray<AnalyzerEntry> },
168
+ "photo-genre": { kind: "flat", toolName: "emit_photo_genre", field: "photoGenre", label: "Photo Genre", entries: PHOTO_GENRES as ReadonlyArray<AnalyzerEntry> },
169
+ backdrop: { kind: "flat", toolName: "emit_backdrop", field: "backdrop", label: "Backdrop", entries: BACKDROPS as ReadonlyArray<AnalyzerEntry> },
170
+ "render-quality": { kind: "flat", toolName: "emit_render_quality", field: "renderQuality", label: "Render Quality", entries: RENDER_QUALITIES as ReadonlyArray<AnalyzerEntry> },
171
+ "composition-effects": { kind: "flat", toolName: "emit_composition_effects", field: "compositionEffect", label: "Composition Effect", entries: COMPOSITION_EFFECTS as ReadonlyArray<AnalyzerEntry> },
172
+ "post-process-effects": { kind: "flat", toolName: "emit_post_process_effects", field: "postProcess", label: "Post-Process Effect", entries: POST_PROCESS_EFFECTS as ReadonlyArray<AnalyzerEntry> },
173
+ "action-fx": { kind: "flat", toolName: "emit_action_fx", field: "actionFx", label: "Action FX", entries: ACTION_FX as ReadonlyArray<AnalyzerEntry> },
174
+ "loop-subject": { kind: "flat", toolName: "emit_loop_subject", field: "loopSubject", label: "Loop Subject", entries: LOOP_SUBJECTS as ReadonlyArray<AnalyzerEntry> },
175
+ transition: { kind: "flat", toolName: "emit_transition", field: "transition", label: "Transition", entries: TRANSITIONS as ReadonlyArray<AnalyzerEntry> },
176
+ "character-fx": { kind: "flat", toolName: "emit_character_fx", field: "characterFx", label: "Character FX", entries: CHARACTER_FX as ReadonlyArray<AnalyzerEntry> },
177
+ pose: { kind: "flat", toolName: "emit_pose", field: "pose", label: "Pose", entries: POSES as ReadonlyArray<AnalyzerEntry> },
178
+ material: { kind: "flat", toolName: "emit_material", field: "material", label: "Material", entries: MATERIALS as ReadonlyArray<AnalyzerEntry> },
179
+ "held-prop": { kind: "flat", toolName: "emit_held_prop", field: "heldProp", label: "Held Prop", entries: HELD_PROPS as ReadonlyArray<AnalyzerEntry> },
180
+ "camera-motion": { kind: "flat", toolName: "emit_camera_motion", field: "cameraMotion", label: "Camera Motion", entries: CAMERA_MOTIONS as ReadonlyArray<AnalyzerEntry> },
181
+ animal: { kind: "flat", toolName: "emit_animal", field: "animal", label: "Animal", entries: ANIMALS as ReadonlyArray<AnalyzerEntry> },
182
+ vehicle: { kind: "flat", toolName: "emit_vehicle", field: "vehicle", label: "Vehicle", entries: VEHICLES as ReadonlyArray<AnalyzerEntry> },
183
+ weapon: { kind: "flat", toolName: "emit_weapon", field: "weapon", label: "Weapon", entries: WEAPONS as ReadonlyArray<AnalyzerEntry> },
184
+ furniture: { kind: "flat", toolName: "emit_furniture", field: "furniture", label: "Furniture", entries: FURNITURE as ReadonlyArray<AnalyzerEntry> },
185
+
186
+ // Discriminated multi-dim pickers. lighting/temporal/exposure discriminate
187
+ // on the single catalog's `category`; the sound/voice pickers span several
188
+ // per-field catalogs, so their entries are synthesized with an explicit
189
+ // `dimension` tag (tagDim below) — same wire shape either way.
190
+ lighting: {
191
+ kind: "discriminated",
192
+ toolName: "emit_lighting",
193
+ discriminator: "category",
194
+ order: LIGHTING_CATEGORY_ORDER as ReadonlyArray<string>,
195
+ fieldByKey: LIGHTING_FIELD_BY_CATEGORY as Readonly<Record<string, string>>,
196
+ labels: LIGHTING_CATEGORY_LABELS as Readonly<Record<string, string>>,
197
+ entries: LIGHTINGS as ReadonlyArray<AnalyzerEntry>,
198
+ limitFn: () => 1,
199
+ },
200
+ temporal: {
201
+ kind: "discriminated",
202
+ toolName: "emit_temporal",
203
+ discriminator: "category",
204
+ order: ["speed", "freeze", "direction", "shutter"],
205
+ fieldByKey: { speed: "temporalSpeed", freeze: "temporalFreeze", direction: "temporalDirection", shutter: "temporalShutter" },
206
+ labels: { speed: "Playback Speed", freeze: "Freeze", direction: "Direction", shutter: "Shutter" },
207
+ entries: TEMPORALS as ReadonlyArray<AnalyzerEntry>,
208
+ limitFn: () => 1,
209
+ },
210
+ "exposure-settings": {
211
+ kind: "discriminated",
212
+ toolName: "emit_exposure_settings",
213
+ discriminator: "category",
214
+ order: ["aperture", "shutter-speed", "iso"],
215
+ fieldByKey: { aperture: "aperture", "shutter-speed": "shutterSpeed", iso: "isoValue" },
216
+ labels: { aperture: "Aperture", "shutter-speed": "Shutter Speed", iso: "ISO" },
217
+ entries: EXPOSURE_SETTINGS as ReadonlyArray<AnalyzerEntry>,
218
+ limitFn: () => 1,
219
+ },
220
+ "music-genre": {
221
+ kind: "discriminated",
222
+ toolName: "emit_music_genre",
223
+ discriminator: "dimension",
224
+ order: ["genre", "subgenre", "era"],
225
+ fieldByKey: { genre: "genre", subgenre: "subgenre", era: "era" },
226
+ labels: { genre: "Genre", subgenre: "Subgenre", era: "Era" },
227
+ entries: [
228
+ ...tagDim(MUSIC_GENRES, "genre"),
229
+ // Subgenres carry promptHint but no description — the hint doubles as
230
+ // the legend text (it describes the sound well enough for matching).
231
+ ...MUSIC_GENRES.flatMap((g) =>
232
+ g.subgenres.map((s) => ({ id: s.id, label: s.label, description: s.promptHint, dimension: "subgenre" })),
233
+ ),
234
+ ...tagDim(MUSIC_ERAS, "era"),
235
+ ],
236
+ limitFn: (k) => (k === "genre" ? 2 : 1),
237
+ },
238
+ "music-mood": {
239
+ kind: "discriminated",
240
+ toolName: "emit_music_mood",
241
+ discriminator: "dimension",
242
+ order: ["energy", "emotion", "vibe"],
243
+ fieldByKey: { energy: "energy", emotion: "emotion", vibe: "vibe" },
244
+ labels: { energy: "Energy", emotion: "Emotion", vibe: "Vibe" },
245
+ entries: [...tagDim(MUSIC_ENERGIES, "energy"), ...tagDim(MUSIC_EMOTIONS, "emotion"), ...tagDim(MUSIC_VIBES, "vibe")],
246
+ limitFn: (k) => (k === "energy" ? 1 : 2),
247
+ },
248
+ instrumentation: {
249
+ kind: "discriminated",
250
+ toolName: "emit_instrumentation",
251
+ discriminator: "dimension",
252
+ order: ["instruments", "production", "vocalPresence", "singingStyle"],
253
+ fieldByKey: { instruments: "instruments", production: "production", vocalPresence: "vocalPresence", singingStyle: "singingStyle" },
254
+ labels: { instruments: "Instruments", production: "Production Style", vocalPresence: "Vocal Presence", singingStyle: "Singing Style" },
255
+ entries: [
256
+ ...tagDim(INSTRUMENTS, "instruments"),
257
+ ...tagDim(PRODUCTION_STYLES, "production"),
258
+ ...tagDim(VOCAL_PRESENCE, "vocalPresence"),
259
+ ...tagDim(SINGING_STYLES, "singingStyle"),
260
+ ],
261
+ limitFn: (k) => (k === "instruments" ? 3 : k === "production" ? 1 : 2),
262
+ },
263
+ "voice-character": {
264
+ kind: "discriminated",
265
+ toolName: "emit_voice_character",
266
+ discriminator: "dimension",
267
+ order: ["age", "gender", "language", "accent", "timbre"],
268
+ fieldByKey: { age: "age", gender: "gender", language: "language", accent: "accent", timbre: "timbre" },
269
+ labels: { age: "Age", gender: "Gender", language: "Language", accent: "Accent", timbre: "Timbre" },
270
+ entries: [
271
+ ...tagDim(VOICE_AGES, "age"),
272
+ ...tagDim(VOICE_GENDERS, "gender"),
273
+ ...tagDim(VOICE_LANGUAGES, "language"),
274
+ ...tagDim(VOICE_ACCENTS, "accent"),
275
+ ...tagDim(VOICE_TIMBRES, "timbre"),
276
+ ],
277
+ limitFn: (k) => (k === "language" ? 2 : 1),
278
+ },
279
+ "voice-delivery": {
280
+ kind: "discriminated",
281
+ toolName: "emit_voice_delivery",
282
+ discriminator: "dimension",
283
+ order: ["pace", "emotion", "archetype"],
284
+ fieldByKey: { pace: "pace", emotion: "emotion", archetype: "archetype" },
285
+ labels: { pace: "Pace", emotion: "Emotion", archetype: "Archetype" },
286
+ entries: [...tagDim(VOICE_PACES, "pace"), ...tagDim(VOICE_EMOTIONS, "emotion"), ...tagDim(VOICE_ARCHETYPES, "archetype")],
287
+ limitFn: () => 1,
288
+ },
116
289
  } satisfies Record<string, PickerAnalyzerDescriptor>
117
290
 
118
291
  export type PickerType = keyof typeof PICKER_ANALYZER_REGISTRY
119
292
  export const PICKER_TYPES = Object.keys(PICKER_ANALYZER_REGISTRY) as PickerType[]
293
+
294
+ /**
295
+ * Family grouping for BATCHED analysis. A single call across all 38 catalogs
296
+ * carries a ~211k-char legend (~53k tokens — measured 2026-08-09, the
297
+ * measure-first probe from the text-to-picker spec), which is slow, costly,
298
+ * and dilutes per-section accuracy. The text-to-picker route fans out one
299
+ * structured call per family (6-15k tokens each) and merges. Mirrors the
300
+ * build-brief's §5 UI grouping so Cine can reuse the same partition.
301
+ */
302
+ export const PICKER_ANALYZER_FAMILIES: Readonly<Record<string, ReadonlyArray<PickerType>>> = {
303
+ scene: ["setting", "atmosphere", "backdrop", "era", "temporal"],
304
+ look: ["style", "color-look", "mood", "aesthetic", "photographer", "photo-genre", "render-quality", "composition-effects", "post-process-effects"],
305
+ camera: ["framing", "camera-motion", "lens", "camera-format", "lighting", "exposure-settings"],
306
+ character: ["person", "styling", "pose", "character-fx"],
307
+ elements: ["animal", "vehicle", "weapon", "furniture", "held-prop", "material", "action-fx", "loop-subject", "transition"],
308
+ audio: ["music-genre", "music-mood", "instrumentation", "voice-character", "voice-delivery"],
309
+ }
120
310
  export const ANALYZABLE_PICKER_TYPES: ReadonlySet<string> = new Set(PICKER_TYPES)
121
311
  export function isAnalyzablePicker(t: string): t is PickerType {
122
312
  return ANALYZABLE_PICKER_TYPES.has(t)