@slatesvideo/shared 0.6.3 → 0.6.4

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 (75) hide show
  1. package/dist/api-url.d.ts +9 -0
  2. package/dist/api-url.js +9 -0
  3. package/dist/auth.d.ts +13 -1
  4. package/dist/auth.js +9 -5
  5. package/dist/clients/cloud.d.ts +3 -0
  6. package/dist/clients/cloud.js +34 -3
  7. package/dist/clients/desktop.js +3 -0
  8. package/dist/index.d.ts +6 -2
  9. package/dist/index.js +19 -2
  10. package/dist/operations/index.d.ts +242 -30
  11. package/dist/operations/index.js +1275 -133
  12. package/dist/operations/surface.d.ts +69 -0
  13. package/dist/operations/surface.js +227 -0
  14. package/dist/prompts/agent-doctrine.js +7 -0
  15. package/dist/prompts/asset-label.d.ts +23 -0
  16. package/dist/prompts/asset-label.js +70 -0
  17. package/dist/prompts/banned-tokens.d.ts +15 -3
  18. package/dist/prompts/banned-tokens.js +76 -9
  19. package/dist/prompts/character-sheet.d.ts +0 -2
  20. package/dist/prompts/character-sheet.js +0 -2
  21. package/dist/prompts/craft-cards.d.ts +20 -0
  22. package/dist/prompts/craft-cards.js +82 -0
  23. package/dist/prompts/environment-sheet.js +16 -0
  24. package/dist/prompts/index.d.ts +1 -0
  25. package/dist/prompts/index.js +4 -0
  26. package/dist/prompts/model-capabilities.d.ts +52 -0
  27. package/dist/prompts/model-capabilities.js +42 -0
  28. package/dist/prompts/model-facts.d.ts +0 -4
  29. package/dist/prompts/model-facts.js +8 -4
  30. package/dist/prompts/partials.generated.js +2 -1
  31. package/dist/prompts/prompting-tips.d.ts +1 -1
  32. package/dist/prompts/prompting-tips.js +58 -0
  33. package/dist/prompts/reference-composer.d.ts +36 -7
  34. package/dist/prompts/reference-composer.js +75 -20
  35. package/dist/prompts/reference-rules.d.ts +15 -26
  36. package/dist/prompts/reference-rules.js +15 -93
  37. package/dist/prompts/shot-grammar.d.ts +154 -0
  38. package/dist/prompts/shot-grammar.js +184 -0
  39. package/dist/prompts/shot-spec.d.ts +265 -0
  40. package/dist/prompts/shot-spec.js +303 -0
  41. package/dist/skills/content.js +25 -23
  42. package/exports/slates-prompt-builder/generated/reference-content-policy.md +6 -0
  43. package/exports/slates-prompt-builder/generated/reference-kling.md +22 -0
  44. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +17 -0
  45. package/exports/slates-prompt-builder/generated/reference-seedance.md +17 -0
  46. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +15 -15
  47. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  48. package/package.json +83 -73
  49. package/skills/_partials/decision-log.md +5 -4
  50. package/skills/_partials/thresholds.md +19 -0
  51. package/skills/slates-content-policy.md +15 -1
  52. package/skills/slates-cost-discipline.md +26 -4
  53. package/skills/slates-model-selection.md +2 -2
  54. package/skills/slates-one-prompt-film.md +20 -12
  55. package/skills/slates-project-organization.md +1 -1
  56. package/skills/slates-prompting-elevenlabs.md +61 -2
  57. package/skills/slates-prompting-flux-2-max.md +39 -0
  58. package/skills/slates-prompting-gpt-image-2.md +109 -70
  59. package/skills/slates-prompting-inworld-tts.md +166 -0
  60. package/skills/slates-prompting-kling-v3.md +39 -0
  61. package/skills/slates-prompting-lip-sync.md +38 -0
  62. package/skills/slates-prompting-ltx-2-5.md +38 -0
  63. package/skills/slates-prompting-minimax-h3.md +39 -0
  64. package/skills/slates-prompting-motion-transfer.md +38 -0
  65. package/skills/slates-prompting-nano-banana-2.md +26 -0
  66. package/skills/slates-prompting-omni-flash.md +41 -0
  67. package/skills/slates-prompting-seed-audio.md +38 -0
  68. package/skills/slates-prompting-seedance-2-5.md +38 -0
  69. package/skills/slates-prompting-seedance.md +26 -0
  70. package/skills/slates-prompting-seedream-5-lite.md +38 -0
  71. package/skills/slates-prompting-veo-3.md +39 -0
  72. package/skills/slates-shot-variety.md +53 -0
  73. package/skills/slates-storyboard-from-script.md +31 -15
  74. package/skills/slates-style-prompting.md +1 -1
  75. package/skills/slates-vision-feedback-loop.md +1 -1
@@ -0,0 +1,69 @@
1
+ import type { z } from 'zod';
2
+ import type { OperationAnnotations, OperationGroup, OperationTier } from './index.js';
3
+ /** Structural shape of an op, declared locally so this module never imports a
4
+ * VALUE from the registry (which imports this one). */
5
+ export interface SurfaceOp {
6
+ id: string;
7
+ description: string;
8
+ input: z.ZodType<unknown>;
9
+ billable?: boolean;
10
+ tier?: OperationTier;
11
+ group?: OperationGroup;
12
+ annotations?: OperationAnnotations;
13
+ }
14
+ /**
15
+ * Derive an op's four hints.
16
+ *
17
+ * `openWorldHint` means "reaches something outside this system": every billable
18
+ * op hits a provider, and every Blender op hits a separate process over a
19
+ * socket. Everything else touches only the user's own workspace.
20
+ */
21
+ export declare function annotate(id: string, billable: boolean | undefined): OperationAnnotations;
22
+ /**
23
+ * The deferred groups, by op id. Everything NOT listed here is `core`.
24
+ *
25
+ * The cut is "what a session needs to do the work" versus "what it needs once,
26
+ * when the user asks for that thing". Creating, generating, reviewing and
27
+ * shot-listing are core; tidying the library, cutting a timeline, renaming and
28
+ * deleting, and driving Blender are not — and each is a coherent thing a user
29
+ * asks for by name, which is what makes a group loadable in one call.
30
+ *
31
+ * 🚨 A GROUP IS A BUDGET, NOT A LAW. If `final_state` falls on timeline or
32
+ * library tasks after this shipped, move those ops back to core. The point is
33
+ * the per-turn byte count, and a deferral that costs a turn every time is not
34
+ * paying for itself.
35
+ */
36
+ export declare const OPERATION_GROUPS: Record<OperationGroup, readonly string[]>;
37
+ export declare function groupFor(id: string): OperationGroup | undefined;
38
+ /** `extended` iff the op names a group. Absent from every group ⇒ `core`, so a
39
+ * new op is visible until someone deliberately defers it. */
40
+ export declare function tierFor(id: string): OperationTier;
41
+ /** One-line summary of each group, for `slates_load_tools`' own description. */
42
+ export declare const GROUP_SUMMARY: Record<OperationGroup, string>;
43
+ export interface ToolDefinition {
44
+ name: string;
45
+ description: string;
46
+ /** JSON Schema. Both surfaces read the same object under their own key name. */
47
+ inputSchema: Record<string, unknown>;
48
+ annotations: OperationAnnotations;
49
+ }
50
+ /**
51
+ * Render one op's tool definition.
52
+ *
53
+ * `$refStrategy: 'none'` is the shape both surfaces now emit: inlined, so no
54
+ * client has to resolve a `$ref`, and byte-identical between them — which is
55
+ * what makes "the two surfaces expose the same tools" checkable rather than
56
+ * merely asserted about the id set.
57
+ */
58
+ export declare function toolDefinition(op: SurfaceOp): ToolDefinition;
59
+ /**
60
+ * Render a tool surface.
61
+ *
62
+ * `desktop` sends `core` plus whatever groups have been loaded this run; `mcp`
63
+ * sends everything, because a stdio server has no run to append to.
64
+ */
65
+ export declare function toolDefinitions(ops: readonly SurfaceOp[], opts: {
66
+ surface: 'desktop' | 'mcp';
67
+ groups?: readonly OperationGroup[];
68
+ }): ToolDefinition[];
69
+ //# sourceMappingURL=surface.d.ts.map
@@ -0,0 +1,227 @@
1
+ // ============================================================
2
+ // THE TOOL SURFACE: annotations, tiers, and the ONE schema renderer.
3
+ //
4
+ // Three jobs that all answer "what does a client see, and when":
5
+ //
6
+ // 1. ANNOTATIONS (MCP spec 2025-06-18). `readOnlyHint` / `destructiveHint` /
7
+ // `idempotentHint` / `openWorldHint`, so a host can auto-approve a read
8
+ // and warn on a delete. Without them `slates_list_assets` prompts exactly
9
+ // like `slates_delete_project`, every prompt looks the same, and the user
10
+ // learns to click through all of them.
11
+ //
12
+ // 2. TIERS. 90 ops is 112 KB of descriptions and JSON schemas on EVERY
13
+ // desktop Studio Agent turn. `core` is what a session needs to work;
14
+ // `extended` is deferred behind `slates_load_tools` and appended to the
15
+ // run's tool list once a group loads. The MCP server still registers
16
+ // everything — a stdio server has no run to append to, and Claude Code
17
+ // already defers stdio tool schemas through its own tool search.
18
+ //
19
+ // 3. ONE SCHEMA RENDERER. The desktop rendered `$refStrategy: 'none'` and
20
+ // the MCP server rendered `target: 'openApi3'`, so "the two surfaces
21
+ // expose the same tools" was true of the ID SET and unproven of the
22
+ // BYTES. `toolDefinitions()` is now the only renderer either one calls.
23
+ //
24
+ // 🚨 NOTHING HERE IS HAND-SET PER OP. Annotations are derived from the op id
25
+ // by the rules below, and `scripts/agent-surface-lockstep-check.mjs` re-derives
26
+ // them INDEPENDENTLY from each op's own transport verbs — a `readOnlyHint` on
27
+ // an op whose `run` body posts is a lie that lets a host auto-approve a
28
+ // mutation, so the check has to be able to catch it, and it is mutation-tested.
29
+ // ============================================================
30
+ import { zodToJsonSchema } from 'zod-to-json-schema';
31
+ // ── 1. Annotations ──────────────────────────────────────────────────────────
32
+ /** Ops that only read. Prefixes, because the verb IS the first word of the id. */
33
+ const READ_ONLY_PREFIXES = [
34
+ 'slates_get_',
35
+ 'slates_list_',
36
+ 'slates_estimate_',
37
+ 'slates_blender_status',
38
+ 'slates_blender_scene',
39
+ 'slates_blender_docs',
40
+ 'slates_blender_search_docs',
41
+ ];
42
+ /**
43
+ * Ops whose effect a user cannot undo from inside Slates.
44
+ *
45
+ * Moves are here on purpose: `slates_move_assets_to_project` relocates files on
46
+ * disk, and an agent that guessed the wrong project has no "put it back"
47
+ * without knowing where they came from. A rename is recoverable; a move across
48
+ * projects, in practice, is not.
49
+ */
50
+ const DESTRUCTIVE_IDS = new Set([
51
+ 'slates_delete_project',
52
+ 'slates_delete_asset',
53
+ 'slates_delete_folder',
54
+ 'slates_delete_character',
55
+ 'slates_delete_environment',
56
+ 'slates_delete_style',
57
+ 'slates_delete_storyboard',
58
+ 'slates_delete_scene',
59
+ 'slates_delete_frame',
60
+ 'slates_move_assets_to_folder',
61
+ 'slates_move_assets_to_project',
62
+ 'slates_move_entity_to_project',
63
+ 'slates_merge_shots',
64
+ 'slates_split_shot',
65
+ ]);
66
+ /** Same input, same end state — `update`/`set`/`reorder`/`rename` all overwrite
67
+ * rather than append, so re-running one is a no-op rather than a second edit. */
68
+ const IDEMPOTENT_PREFIXES = [
69
+ 'slates_update_',
70
+ 'slates_set_',
71
+ 'slates_reorder_',
72
+ 'slates_rename_',
73
+ 'slates_batch_update_',
74
+ ];
75
+ const startsWithAny = (id, prefixes) => prefixes.some((p) => id.startsWith(p));
76
+ /**
77
+ * Derive an op's four hints.
78
+ *
79
+ * `openWorldHint` means "reaches something outside this system": every billable
80
+ * op hits a provider, and every Blender op hits a separate process over a
81
+ * socket. Everything else touches only the user's own workspace.
82
+ */
83
+ export function annotate(id, billable) {
84
+ const readOnlyHint = startsWithAny(id, READ_ONLY_PREFIXES);
85
+ return {
86
+ readOnlyHint,
87
+ // A read is never destructive, whatever the id looks like.
88
+ destructiveHint: !readOnlyHint && DESTRUCTIVE_IDS.has(id),
89
+ idempotentHint: readOnlyHint || startsWithAny(id, IDEMPOTENT_PREFIXES),
90
+ openWorldHint: !!billable || id.startsWith('slates_blender_'),
91
+ };
92
+ }
93
+ // ── 2. Tiers ────────────────────────────────────────────────────────────────
94
+ /**
95
+ * The deferred groups, by op id. Everything NOT listed here is `core`.
96
+ *
97
+ * The cut is "what a session needs to do the work" versus "what it needs once,
98
+ * when the user asks for that thing". Creating, generating, reviewing and
99
+ * shot-listing are core; tidying the library, cutting a timeline, renaming and
100
+ * deleting, and driving Blender are not — and each is a coherent thing a user
101
+ * asks for by name, which is what makes a group loadable in one call.
102
+ *
103
+ * 🚨 A GROUP IS A BUDGET, NOT A LAW. If `final_state` falls on timeline or
104
+ * library tasks after this shipped, move those ops back to core. The point is
105
+ * the per-turn byte count, and a deferral that costs a turn every time is not
106
+ * paying for itself.
107
+ */
108
+ export const OPERATION_GROUPS = {
109
+ // Folders, styles, and moving assets around. Reached when the user says
110
+ // "tidy this up", never while making the thing.
111
+ library: [
112
+ 'slates_list_folders',
113
+ 'slates_create_folder',
114
+ 'slates_rename_folder',
115
+ 'slates_delete_folder',
116
+ 'slates_set_folder_cover',
117
+ 'slates_move_assets_to_folder',
118
+ 'slates_move_assets_to_project',
119
+ 'slates_copy_assets_to_project',
120
+ 'slates_move_entity_to_project',
121
+ 'slates_list_styles',
122
+ 'slates_create_style',
123
+ 'slates_update_style',
124
+ 'slates_delete_style',
125
+ 'slates_get_project_directory',
126
+ 'slates_reveal_file',
127
+ ],
128
+ // The cut and the export. A generation session never touches these.
129
+ timeline: [
130
+ 'slates_get_timeline',
131
+ 'slates_add_clip_to_timeline',
132
+ 'slates_reorder_clips',
133
+ 'slates_remove_clip',
134
+ 'slates_add_timeline_track',
135
+ 'slates_update_timeline_track',
136
+ 'slates_remove_timeline_track',
137
+ 'slates_update_timeline_settings',
138
+ 'slates_export_video',
139
+ 'slates_export_timeline_xml',
140
+ 'slates_trim_video',
141
+ ],
142
+ // Rename, delete, reorder. The 63-op CRUD tail the review measured at 39.5 KB.
143
+ admin: [
144
+ 'slates_update_project',
145
+ 'slates_delete_project',
146
+ 'slates_delete_asset',
147
+ 'slates_update_character',
148
+ 'slates_delete_character',
149
+ 'slates_update_environment',
150
+ 'slates_delete_environment',
151
+ 'slates_update_storyboard',
152
+ 'slates_delete_storyboard',
153
+ 'slates_update_scene',
154
+ 'slates_delete_scene',
155
+ 'slates_reorder_scenes',
156
+ 'slates_delete_frame',
157
+ 'slates_batch_update_frames',
158
+ 'slates_duplicate_shot',
159
+ 'slates_split_shot',
160
+ 'slates_merge_shots',
161
+ ],
162
+ // A third transport nobody without Blender installed can reach.
163
+ blender: [
164
+ 'slates_blender_status',
165
+ 'slates_blender_execute',
166
+ 'slates_blender_scene',
167
+ 'slates_blender_docs',
168
+ 'slates_blender_search_docs',
169
+ 'slates_blender_render_blocking',
170
+ ],
171
+ };
172
+ const GROUP_BY_OP = new Map();
173
+ for (const [group, ids] of Object.entries(OPERATION_GROUPS)) {
174
+ for (const id of ids)
175
+ GROUP_BY_OP.set(id, group);
176
+ }
177
+ export function groupFor(id) {
178
+ return GROUP_BY_OP.get(id);
179
+ }
180
+ /** `extended` iff the op names a group. Absent from every group ⇒ `core`, so a
181
+ * new op is visible until someone deliberately defers it. */
182
+ export function tierFor(id) {
183
+ return GROUP_BY_OP.has(id) ? 'extended' : 'core';
184
+ }
185
+ /** One-line summary of each group, for `slates_load_tools`' own description. */
186
+ export const GROUP_SUMMARY = {
187
+ library: 'folders, styles, moving and copying assets between projects, revealing files on disk',
188
+ timeline: 'the timeline (tracks, clips, settings), video export and NLE XML export, clip trimming',
189
+ admin: 'rename / delete / reorder for projects, characters, environments, storyboards, scenes and frames; shot duplicate, split and merge',
190
+ blender: 'the Blender previs bridge — scene inspection, bpy execution, API docs, grey-box render',
191
+ };
192
+ /**
193
+ * Render one op's tool definition.
194
+ *
195
+ * `$refStrategy: 'none'` is the shape both surfaces now emit: inlined, so no
196
+ * client has to resolve a `$ref`, and byte-identical between them — which is
197
+ * what makes "the two surfaces expose the same tools" checkable rather than
198
+ * merely asserted about the id set.
199
+ */
200
+ export function toolDefinition(op) {
201
+ const schema = zodToJsonSchema(op.input, { $refStrategy: 'none' });
202
+ delete schema.$schema;
203
+ return {
204
+ name: op.id,
205
+ description: op.description,
206
+ inputSchema: schema,
207
+ annotations: op.annotations ?? annotate(op.id, op.billable),
208
+ };
209
+ }
210
+ /**
211
+ * Render a tool surface.
212
+ *
213
+ * `desktop` sends `core` plus whatever groups have been loaded this run; `mcp`
214
+ * sends everything, because a stdio server has no run to append to.
215
+ */
216
+ export function toolDefinitions(ops, opts) {
217
+ const loaded = new Set(opts.groups ?? []);
218
+ return ops
219
+ .filter((op) => {
220
+ if (opts.surface === 'mcp')
221
+ return true;
222
+ const group = groupFor(op.id);
223
+ return group === undefined || loaded.has(group);
224
+ })
225
+ .map(toolDefinition);
226
+ }
227
+ //# sourceMappingURL=surface.js.map
@@ -101,6 +101,7 @@ export const HARD_RULES = [
101
101
  fork(`- COST DISCIPLINE (slates-cost-discipline): never fire a billable generation outside an approved plan. Estimate before you promise. Batch related generations into one plan.`, `- COST DISCIPLINE (slates-cost-discipline): never fire a billable generation the user has not agreed to. Estimate before you promise. Batch related generations into one quote so the user approves a total, not a drip.`),
102
102
  both(`- CONTENT POLICY: before writing prompts involving real people/celebrities, minors, brands/logos, weapons, or gore, load slates-content-policy and build the scene safe from the first word. If a provider rejects (e.g. real-face detection), explain in plain language — refunds for provider rejections are automatic.`),
103
103
  both(`- PROMPT IS LAW (reference doctrine): references are cited inline by name and image number ("Marcus (images 1 and 2)"); the prompt text leads. Never write role-essays about what each reference is "for".`),
104
+ both(`- NAMES AND DESCRIPTIONS ARE THE USER'S UI, NOT YOUR NOTEPAD. A project, storyboard or shot name is rendered at the top of the user's screen at all times. Name it after the piece ("Kaiju selfie"), never after your process, and NEVER append your own status or housekeeping — no "shot list", no "v2", no "Written from IMG-A172 and IMG-A182", no "Delete freely", no "scratch". Leave the description empty unless the user gave you one worth keeping: a note-to-self at the top of the screen reads as part of the product, and the user has to look at it every day. Say that kind of thing in your reply to them instead.`),
104
105
  both(`- RESOLUTION DEFAULT IS UNIFORM: 1080p on the best available video model. Do not crank resolution the user didn't ask for.`),
105
106
  // ⛔ THE MODEL ROUTING BLOCK IS GONE FROM HERE, DELIBERATELY (2026-08-30).
106
107
  //
@@ -131,6 +132,12 @@ export const HARD_RULES = [
131
132
  // fabricate: what the generation LOOKS like.
132
133
  fork(`- REAL NUMBERS ONLY — NEVER approximate, estimate, or recall any figure. Every cost, balance, count, or duration you state must be copied verbatim from a tool result IN THIS SESSION: generation costs come from the cost_credits field in generate/status results (whole credits); the balance comes from a fresh slates_get_credit_balance call at summary time (never arithmetic you did yourself); orchestration cost is displayed by the app automatically — NEVER state or estimate it. No "approx", no "~", no rounded guesses. A number you cannot point to in a tool result is a number you do not say.`, `- REAL NUMBERS ONLY — NEVER approximate, estimate, or recall any figure. Every cost, balance, count, or duration you state must be copied verbatim from a tool result IN THIS SESSION: generation costs come from the cost_credits field in generate/status results (whole credits); the balance comes from a fresh slates_get_credit_balance call at summary time (never arithmetic you did yourself). No "approx", no "~", no rounded guesses. A number you cannot point to in a tool result is a number you do not say. The same rule covers what you SAW: never describe how a generation looks unless you fetched it this session with slates_get_asset_image / slates_get_asset_video_frames.`),
133
134
  both(`- MODEL IDS ARE FIXED: slates_generate_video takes exactly ${VIDEO_MODELS.join(' | ')}, with duration + videoResolution as separate params. slates_generate_audio takes exactly ${AUDIO_MODELS.join(' | ')}. Registry entries like "kling-v3-standard-8s" or "seed-audio-15s" are billing keys, not model ids.`),
135
+ // ONE LINE, NOT A SECTION. The counts themselves ride the op RESULT, where
136
+ // the agent is already reading — measured: a rule inlined into an op moved
137
+ // compliance 0/8 → 30/32, while the same guidance behind a guide fetch sat at
138
+ // 13% before and after. This sentence exists only to say the numbers are
139
+ // there and must be looked at; the craft is the skill.
140
+ both(`- READ THE VARIETY COUNTS BEFORE FIRING A SET: slates_list_shots and slates_get_storyboard_with_frames return the distribution (shot sizes, camera moves, runs of three or more) with the rows. If one bucket is the plurality, fix the board before you spend — slates-shot-variety is the craft.`),
134
141
  both(`- AUDIO LENGTH IS PROMPT-DRIVEN ON SEED AUDIO: it has no duration parameter, so the durationSeconds you pass is written into the prompt AND is what the user is charged, whatever comes back. Choose it deliberately and load slates-prompting-seed-audio before the first call.`),
135
142
  ];
136
143
  // ── Surface-only footers ───────────────────────────────────────────
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A readable label for a generated asset, from its prompt.
3
+ *
4
+ * Strips a reference preamble, then takes the first CLAUSE rather than a fixed
5
+ * character slice — so the label ends on a word instead of mid-syllable.
6
+ * Returns null for a prompt with nothing usable in it; the caller decides what
7
+ * to show instead (a Shot name, "Untitled").
8
+ */
9
+ export declare function labelFromPrompt(prompt: string | null | undefined): string | null;
10
+ /**
11
+ * The caption for one asset row, in priority order: the label it was given, the
12
+ * Shot it came from, then the prompt reduced to a clause.
13
+ *
14
+ * The Shot name sits second because an author who named a Shot chose that word
15
+ * on purpose, and it is the word on the row in the storyboard — so the gallery
16
+ * and the document call the same thing the same thing.
17
+ */
18
+ export declare function assetCaption(a: {
19
+ label?: string | null;
20
+ shotName?: string | null;
21
+ prompt?: string | null;
22
+ }): string;
23
+ //# sourceMappingURL=asset-label.d.ts.map
@@ -0,0 +1,70 @@
1
+ // ============================================================
2
+ // ASSET LABELS — what a generated thing is CALLED in a list.
3
+ //
4
+ // 🚨 THE FIRST 40 CHARACTERS OF A PROMPT IS NOT A LABEL, and on
5
+ // reference-driven work it is actively misleading: every one of those prompts
6
+ // opens with the same composed preamble, so a whole gallery reads
7
+ // "Reference image 1 is a character ide" — the same string on every row, which
8
+ // is worse than no label because it looks like information.
9
+ //
10
+ // One rule, two consumers: the op surface's `compactAsset` (what an agent reads
11
+ // back from `slates_list_assets`) and the desktop gallery caption. A second
12
+ // copy of this would drift the way every other hand-mirrored rule in this
13
+ // workspace has.
14
+ //
15
+ // Dependency-free LEAF, same as `shot-grammar`: the desktop RENDERER imports it
16
+ // directly, so it may never reach for `node:` anything.
17
+ // ============================================================
18
+ /**
19
+ * A composed prompt's reference preamble.
20
+ *
21
+ * The composer writes "Reference image 1 is a character identity sheet." — so
22
+ * the optional leading "Reference" has to be matched SEPARATELY from the noun,
23
+ * or the pattern only catches the bare "Image 1 is …" form and every real
24
+ * composed prompt walks straight past it. (It did, until the labels were
25
+ * exercised on actual prompts.)
26
+ */
27
+ const REFERENCE_PREAMBLE_RE = /^\s*(?:reference\s*)?(?:image|video|audio|clip)?\s*\d*\s*is\b[^.;]*[.;]\s*/i;
28
+ /** A sentence boundary always ends the label. */
29
+ const SENTENCE_BOUNDARY_RE = /(?<=[.;:])\s/;
30
+ /**
31
+ * An em-dash ends it too — but only once there is enough label to be worth
32
+ * keeping. Composed prompts join a subject to its treatment with one, and the
33
+ * subject alone is the label; a hand-written "wide shot — the barn at dawn"
34
+ * would otherwise be cut down to "wide shot", which names nothing.
35
+ */
36
+ const DASH_BOUNDARY_RE = /\s—\s/;
37
+ const MIN_BEFORE_DASH = 25;
38
+ const MAX = 60;
39
+ /**
40
+ * A readable label for a generated asset, from its prompt.
41
+ *
42
+ * Strips a reference preamble, then takes the first CLAUSE rather than a fixed
43
+ * character slice — so the label ends on a word instead of mid-syllable.
44
+ * Returns null for a prompt with nothing usable in it; the caller decides what
45
+ * to show instead (a Shot name, "Untitled").
46
+ */
47
+ export function labelFromPrompt(prompt) {
48
+ if (!prompt)
49
+ return null;
50
+ const body = (prompt.replace(REFERENCE_PREAMBLE_RE, '').trim() || prompt).trim();
51
+ let clause = body.split(SENTENCE_BOUNDARY_RE)[0].trim();
52
+ const dash = clause.split(DASH_BOUNDARY_RE)[0].trim();
53
+ if (dash.length >= MIN_BEFORE_DASH)
54
+ clause = dash;
55
+ if (!clause)
56
+ return null;
57
+ return clause.length > MAX ? `${clause.slice(0, MAX - 1).trimEnd()}…` : clause;
58
+ }
59
+ /**
60
+ * The caption for one asset row, in priority order: the label it was given, the
61
+ * Shot it came from, then the prompt reduced to a clause.
62
+ *
63
+ * The Shot name sits second because an author who named a Shot chose that word
64
+ * on purpose, and it is the word on the row in the storyboard — so the gallery
65
+ * and the document call the same thing the same thing.
66
+ */
67
+ export function assetCaption(a) {
68
+ return a.label?.trim() || a.shotName?.trim() || labelFromPrompt(a.prompt) || 'Untitled';
69
+ }
70
+ //# sourceMappingURL=asset-label.js.map
@@ -10,8 +10,20 @@ export interface BannedToken {
10
10
  /** Every banned token, in skill-document order. Integrity asserted at load. */
11
11
  export declare const BANNED_PROMPT_TOKENS: readonly BannedToken[];
12
12
  export declare function bannedTokensFor(scope: BannedTokenScope): readonly BannedToken[];
13
- /** The tokens a submitted prompt actually contains. */
14
- export declare function findBannedTokens(prompt: string, scope: BannedTokenScope): BannedToken[];
13
+ /** The never-use list a single skill declares. Empty for a skill with no block. */
14
+ export declare function bannedTokensForSkill(skill: string): readonly BannedToken[];
15
+ /**
16
+ * A single model's never-use list, for the estimate RESULT.
17
+ *
18
+ * Deliberately NOT the cross-model list — that one already rides the op
19
+ * description on every call. This is the half that could not: the quirk that is
20
+ * true of Veo and false of Kling.
21
+ */
22
+ export declare function describeBannedTokensForSkill(skill: string): string;
23
+ /** The tokens a submitted prompt actually contains. `skill` adds that model's
24
+ * own list to the modality-wide one — the two overlap for nano-banana-2 and
25
+ * seedance, so hits are deduplicated by token. */
26
+ export declare function findBannedTokens(prompt: string, scope: BannedTokenScope, skill?: string): BannedToken[];
15
27
  /**
16
28
  * The list as it appears INSIDE an op description — always in context, on both
17
29
  * surfaces, with no call required to see it. Generated, never hand-typed.
@@ -24,5 +36,5 @@ export declare function describeBannedTokens(scope: BannedTokenScope): string;
24
36
  * rather than raised as an error. The generation proceeds either way: the
25
37
  * sandbox doctrine says make state visible, never block.
26
38
  */
27
- export declare function bannedTokenWarning(prompt: string, scope: BannedTokenScope): string;
39
+ export declare function bannedTokenWarning(prompt: string, scope: BannedTokenScope, skill?: string): string;
28
40
  //# sourceMappingURL=banned-tokens.d.ts.map
@@ -30,18 +30,52 @@
30
30
  // ============================================================
31
31
  import { SKILLS } from '../skills/content.js';
32
32
  /**
33
- * Where each scope's list lives.
33
+ * Where each scope's CROSS-MODEL list lives.
34
34
  *
35
35
  * One skill per scope on purpose: these are the two lists that are GENERIC to
36
36
  * their modality (Stable-Diffusion-era tag soup for images, quality
37
- * incantations for video), not model-specific quirks. A per-model list would
38
- * mean the op description changed with the `model` argument, which it cannot —
39
- * a description is one static string for every call.
37
+ * incantations for video), not model-specific quirks. A per-model list cannot
38
+ * ride the op DESCRIPTION — a description is one static string for every call,
39
+ * so it cannot change with the `model` argument. That is what
40
+ * `bannedTokensForSkill` below is for: the per-model list rides the estimate
41
+ * RESULT, where the model has just been named.
40
42
  */
41
43
  const BANNED_TOKEN_SOURCES = [
42
44
  { skill: 'slates-prompting-nano-banana-2', scope: 'image' },
43
45
  { skill: 'slates-prompting-seedance', scope: 'video' },
44
46
  ];
47
+ /**
48
+ * 🚨 THE ENFORCEMENT THAT WORKED COVERED TWO SKILLS OF FIFTEEN.
49
+ *
50
+ * `describeBannedTokens('image')` was Nano Banana's list and `('video')` was
51
+ * Seedance's, so a Veo, Kling, LTX, MiniMax, FLUX, Seedream, GPT-Image or audio
52
+ * generation was matched against another model's never-use list and its own was
53
+ * enforced by nothing — while four skills (content-policy, lip-sync,
54
+ * minimax-h3, motion-transfer) carried never-use prose with no markers at all,
55
+ * which is a rule an LLM has to notice.
56
+ *
57
+ * Every skill that carries an `@banned` block now contributes to a per-skill
58
+ * list, delivered on the estimate result beside the craft card. The two above
59
+ * stay ALSO on the op descriptions, because a modality-wide list is true of
60
+ * every call that op can make.
61
+ */
62
+ function extractPerSkill() {
63
+ const out = new Map();
64
+ for (const [skill, content] of Object.entries(SKILLS)) {
65
+ // Cheap pre-test: only pay the regex for files that carry the marker.
66
+ if (!content.includes('@banned:start'))
67
+ continue;
68
+ const scope = inferScope(skill);
69
+ out.set(skill, extractFromSkill(skill).map((token) => ({ token, skill, scope })));
70
+ }
71
+ return out;
72
+ }
73
+ /** Image-lane skills prompt for pixels; everything else is a time-based lane.
74
+ * Only used to tag a token for the warning text — the per-skill list is
75
+ * matched by SKILL, never by scope, so a wrong guess here cannot mis-enforce. */
76
+ function inferScope(skill) {
77
+ return /nano-banana|gpt-image|flux|seedream/.test(skill) ? 'image' : 'video';
78
+ }
45
79
  const FENCE_RE = /<!--\s*@banned:start\s*-->([\s\S]*?)<!--\s*@banned:end\s*-->/g;
46
80
  const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
47
81
  const BACKTICKED_RE = /`([^`\n]+)`/g;
@@ -115,9 +149,42 @@ for (const { token, skill } of BANNED_PROMPT_TOKENS) {
115
149
  `matcherFor() the new shape.`);
116
150
  }
117
151
  }
118
- /** The tokens a submitted prompt actually contains. */
119
- export function findBannedTokens(prompt, scope) {
120
- return bannedTokensFor(scope).filter((b) => matcherFor(b.token).test(prompt));
152
+ /** Every per-skill list, keyed by skill name. */
153
+ const bySkill = extractPerSkill();
154
+ /** The never-use list a single skill declares. Empty for a skill with no block. */
155
+ export function bannedTokensForSkill(skill) {
156
+ return bySkill.get(skill) ?? [];
157
+ }
158
+ /**
159
+ * A single model's never-use list, for the estimate RESULT.
160
+ *
161
+ * Deliberately NOT the cross-model list — that one already rides the op
162
+ * description on every call. This is the half that could not: the quirk that is
163
+ * true of Veo and false of Kling.
164
+ */
165
+ export function describeBannedTokensForSkill(skill) {
166
+ const list = bannedTokensForSkill(skill);
167
+ if (list.length === 0)
168
+ return '';
169
+ return (`NEVER put these in a ${skill.replace('slates-prompting-', '')} prompt: ` +
170
+ list.map((b) => `"${b.token}"`).join(', ') +
171
+ `. Describe specifically instead (${skill}).`);
172
+ }
173
+ /** The tokens a submitted prompt actually contains. `skill` adds that model's
174
+ * own list to the modality-wide one — the two overlap for nano-banana-2 and
175
+ * seedance, so hits are deduplicated by token. */
176
+ export function findBannedTokens(prompt, scope, skill) {
177
+ const candidates = [...bannedTokensFor(scope), ...(skill ? bannedTokensForSkill(skill) : [])];
178
+ const seen = new Set();
179
+ const hits = [];
180
+ for (const b of candidates) {
181
+ if (seen.has(b.token))
182
+ continue;
183
+ seen.add(b.token);
184
+ if (matcherFor(b.token).test(prompt))
185
+ hits.push(b);
186
+ }
187
+ return hits;
121
188
  }
122
189
  /**
123
190
  * The list as it appears INSIDE an op description — always in context, on both
@@ -139,8 +206,8 @@ export function describeBannedTokens(scope) {
139
206
  * rather than raised as an error. The generation proceeds either way: the
140
207
  * sandbox doctrine says make state visible, never block.
141
208
  */
142
- export function bannedTokenWarning(prompt, scope) {
143
- const hits = findBannedTokens(prompt, scope);
209
+ export function bannedTokenWarning(prompt, scope, skill) {
210
+ const hits = findBannedTokens(prompt, scope, skill);
144
211
  if (hits.length === 0)
145
212
  return '';
146
213
  const skills = [...new Set(hits.map((b) => b.skill))].join(', ');
@@ -13,8 +13,6 @@
13
13
  * removal and must be SCOPED TO THE FACE rather than to the whole body.
14
14
  */
15
15
  export declare const CHARACTER_SHEET_PANELS_DESC: string;
16
- /** Panel identifiers, in sheet order. */
17
- export declare const BODY_POSE_LABELS: readonly ["portrait", "front", "back"];
18
16
  /**
19
17
  * The character identity sheet — one asset, three panels.
20
18
  *
@@ -139,8 +139,6 @@ export const CHARACTER_SHEET_PANELS_DESC = 'a large chest-up portrait on the lef
139
139
  'a full-body front view in a relaxed A-pose in the centre, cropped at the collarbone — ' +
140
140
  'an invisible-mannequin presentation with just the face cropped out, ' +
141
141
  'and a full-body back view on the right with the head and hair fully visible';
142
- /** Panel identifiers, in sheet order. */
143
- export const BODY_POSE_LABELS = ['portrait', 'front', 'back'];
144
142
  // The sheet's style directive: a user transform REPLACES the inherit-source
145
143
  // instruction (so the model isn't told to both preserve the medium AND change
146
144
  // it); otherwise inherit the source medium.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Hard ceiling per card, in characters.
3
+ *
4
+ * A card is a CARD. The largest skill is 5,736 words and loading it whole is
5
+ * exactly the cost this mechanism exists to avoid — if a card needs more than
6
+ * this, the extra belongs in the body of the skill, which is one
7
+ * `slates_get_prompting_guide` call away. Asserted at load, so an over-long
8
+ * card fails the build rather than quietly inflating every estimate result.
9
+ */
10
+ export declare const CRAFT_CARD_CEILING = 2400;
11
+ /** Every card, keyed by skill name. Built once at load; integrity asserted. */
12
+ export declare const CRAFT_CARDS: Readonly<Record<string, string>>;
13
+ /** The card for a skill, or null when that skill carries none. */
14
+ export declare function craftCard(skill: string): string | null;
15
+ /**
16
+ * The card as it appears in an op RESULT: the body, plus one line naming where
17
+ * the rest lives so the agent knows the card is a summary and not the guide.
18
+ */
19
+ export declare function describeCraftCard(skill: string): string;
20
+ //# sourceMappingURL=craft-cards.d.ts.map