@slatesvideo/shared 0.6.11 → 0.7.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.
Files changed (83) hide show
  1. package/dist/auth.js +2 -2
  2. package/dist/clients/cloud.js +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/manual/content.d.ts +1 -1
  6. package/dist/manual/content.js +1 -1
  7. package/dist/operations/index.d.ts +817 -16
  8. package/dist/operations/index.js +1413 -360
  9. package/dist/operations/surface.d.ts +4 -1
  10. package/dist/operations/surface.js +41 -10
  11. package/dist/prompts/ad-presets.d.ts +77 -0
  12. package/dist/prompts/ad-presets.js +43 -0
  13. package/dist/prompts/agent-doctrine.js +27 -5
  14. package/dist/prompts/banned-tokens.d.ts +4 -29
  15. package/dist/prompts/banned-tokens.js +29 -204
  16. package/dist/prompts/craft-cards.js +2 -2
  17. package/dist/prompts/generation-policy.d.ts +41 -0
  18. package/dist/prompts/generation-policy.js +53 -0
  19. package/dist/prompts/guide-retrieval.d.ts +9 -0
  20. package/dist/prompts/guide-retrieval.js +53 -0
  21. package/dist/prompts/index.d.ts +1 -0
  22. package/dist/prompts/index.js +1 -0
  23. package/dist/prompts/model-capabilities.d.ts +18 -1
  24. package/dist/prompts/model-capabilities.js +72 -19
  25. package/dist/prompts/model-facts.d.ts +34 -2
  26. package/dist/prompts/model-facts.js +66 -5
  27. package/dist/prompts/partials.generated.js +8 -2
  28. package/dist/prompts/prompting-tips.d.ts +1 -1
  29. package/dist/prompts/prompting-tips.js +61 -16
  30. package/dist/prompts/reference-composer.d.ts +2 -0
  31. package/dist/prompts/reference-composer.js +51 -50
  32. package/dist/prompts/script-document.d.ts +165 -0
  33. package/dist/prompts/script-document.js +11 -0
  34. package/dist/prompts/shot-grammar.d.ts +4 -4
  35. package/dist/prompts/shot-grammar.js +3 -3
  36. package/dist/prompts/shot-spec.d.ts +13 -0
  37. package/dist/prompts/shot-spec.js +23 -5
  38. package/dist/skills/content.js +27 -24
  39. package/exports/slates-chatgpt-images/generated/SKILL.md +107 -0
  40. package/exports/slates-chatgpt-images/generated/slates-chatgpt-images.skill +0 -0
  41. package/exports/slates-prompt-builder/generated/SKILL.md +1 -1
  42. package/exports/slates-prompt-builder/generated/reference-character.md +9 -1
  43. package/exports/slates-prompt-builder/generated/reference-kling.md +3 -3
  44. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +22 -10
  45. package/exports/slates-prompt-builder/generated/reference-seedance.md +4 -4
  46. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +17 -17
  47. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  48. package/package.json +9 -3
  49. package/skills/_partials/cinematic-card.md +8 -0
  50. package/skills/_partials/cinematic-routes-short.md +2 -0
  51. package/skills/_partials/cinematic-tips-short.md +2 -0
  52. package/skills/_partials/decision-log.md +1 -13
  53. package/skills/_partials/image-defaults.md +11 -0
  54. package/skills/_partials/lens-video-split.md +1 -0
  55. package/skills/_partials/reference-rules-core.md +1 -1
  56. package/skills/_partials/sheet-tool-defaults.md +6 -0
  57. package/skills/slates-character-identity.md +9 -1
  58. package/skills/slates-chatgpt-images.md +107 -0
  59. package/skills/slates-cinematic-look.md +237 -0
  60. package/skills/slates-cost-discipline.md +18 -12
  61. package/skills/slates-direct-response-ad.md +13 -53
  62. package/skills/slates-edit-and-iterate.md +1 -1
  63. package/skills/slates-model-selection.md +20 -14
  64. package/skills/slates-one-prompt-film.md +19 -77
  65. package/skills/slates-project-organization.md +7 -3
  66. package/skills/slates-prompting-flux-2-max.md +15 -4
  67. package/skills/slates-prompting-gpt-image-2-5.md +41 -28
  68. package/skills/slates-prompting-inworld-tts.md +174 -174
  69. package/skills/slates-prompting-kling-v3.md +3 -3
  70. package/skills/slates-prompting-lip-sync.md +1 -1
  71. package/skills/slates-prompting-minimax-h3.md +30 -17
  72. package/skills/slates-prompting-motion-transfer.md +1 -1
  73. package/skills/slates-prompting-nano-banana-2.md +24 -11
  74. package/skills/slates-prompting-seedance-2-5.md +7 -6
  75. package/skills/slates-prompting-seedance.md +5 -5
  76. package/skills/slates-prompting-seedream-5-lite.md +14 -3
  77. package/skills/slates-prompting-veo-3.md +1 -1
  78. package/skills/slates-script-craft.md +45 -0
  79. package/skills/slates-shot-variety.md +11 -40
  80. package/skills/slates-storyboard-from-script.md +14 -66
  81. package/skills/slates-style-prompting.md +4 -4
  82. package/skills/slates-ugc-influencer-ad.md +32 -309
  83. package/skills/slates-vision-feedback-loop.md +2 -1
@@ -1,219 +1,44 @@
1
- // ============================================================
2
- // BANNED PROMPT TOKENS — the "load the guide" rule, made structural.
3
- //
4
- // THE PROBLEM: "before prompting any model, load the matching guide" is a
5
- // sentence in a system prompt with nothing checking it. Measured 2026-08-30 in
6
- // a real Studio Agent session: slates_get_prompting_guide was called ZERO
7
- // times, and the prompt that shipped tripped the skill's own never-use list
8
- // twice (`photorealistic`, `cinematic`). A rule with no check is a suggestion,
9
- // and an LLM is the least reliable enforcer you could pick.
10
- //
11
- // THE FIX, in two halves, neither of which the model can skip:
12
- // (a) the never-use list is INLINED into the generate ops' descriptions.
13
- // Op descriptions are always in context on BOTH surfaces — there is no
14
- // call to omit and no discretion to exercise.
15
- // (b) the submitted prompt is MATCHED against the list and a warning comes
16
- // back in the op result. Non-blocking: the generation proceeds
17
- // (PRODUCT_PHILOSOPHY.md → make state visible, never block).
18
- //
19
- // 🚨 THE LIST IS NEVER HAND-TYPED HERE. It is extracted from the skill files
20
- // themselves, between `<!-- @banned:start -->` / `<!-- @banned:end -->`
21
- // markers. That is this workspace's LLM-docs doctrine — never hand-type a fact
22
- // an LLM will read — and it is the only way the op description and the skill
23
- // cannot drift apart. Change the skill; the description follows on the next
24
- // build. scripts/agent-surface-lockstep-check.mjs fails the build if a token
25
- // in a description no longer appears in its source skill.
26
- //
27
- // Deterministic by construction (document order, no sorting, no dedupe
28
- // reshuffle) because these strings land in the desktop's prompt-cached tool
29
- // prefix, whose byte-stability IS the cache mechanism.
30
- // ============================================================
31
- import { SKILLS } from '../skills/content.js';
32
- /**
33
- * Where each scope's CROSS-MODEL list lives.
34
- *
35
- * One skill per scope on purpose: these are the two lists that are GENERIC to
36
- * their modality (Stable-Diffusion-era tag soup for images, quality
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.
42
- */
43
- const BANNED_TOKEN_SOURCES = [
44
- { skill: 'slates-prompting-nano-banana-2', scope: 'image' },
45
- { skill: 'slates-prompting-seedance', scope: 'video' },
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
- }
79
- const FENCE_RE = /<!--\s*@banned:start\s*-->([\s\S]*?)<!--\s*@banned:end\s*-->/g;
80
- const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
81
- const BACKTICKED_RE = /`([^`\n]+)`/g;
82
- /**
83
- * Pull every backticked phrase out of a skill's fenced block(s).
84
- *
85
- * HTML comments are stripped FIRST: the fence carries a "MACHINE-READ" note to
86
- * whoever edits the skill next, and that note itself contains backticks.
1
+ /** Model-specific prompt advice, extracted from each skill's @banned blocks.
2
+ * Warnings never rewrite or block a prompt. No model's list is universal.
87
3
  */
88
- function extractFromSkill(skill) {
89
- const content = SKILLS[skill];
90
- if (content === undefined) {
91
- throw new Error(`[banned-tokens] no such skill: ${skill}. BANNED_TOKEN_SOURCES must name files in packages/shared/skills/.`);
92
- }
93
- const out = [];
94
- let fence;
95
- FENCE_RE.lastIndex = 0;
96
- let fences = 0;
97
- while ((fence = FENCE_RE.exec(content)) !== null) {
98
- fences += 1;
99
- const body = fence[1].replace(HTML_COMMENT_RE, '');
100
- let m;
101
- BACKTICKED_RE.lastIndex = 0;
102
- while ((m = BACKTICKED_RE.exec(body)) !== null) {
103
- const token = m[1].trim();
104
- if (token && !out.includes(token))
105
- out.push(token);
106
- }
107
- }
108
- if (fences === 0) {
109
- throw new Error(`[banned-tokens] ${skill}.md has no <!-- @banned:start --> / <!-- @banned:end --> block. ` +
110
- `The op description and the prompt warnings are GENERATED from it — restore the markers, ` +
111
- `or drop the skill from BANNED_TOKEN_SOURCES.`);
112
- }
113
- if (out.length === 0) {
114
- throw new Error(`[banned-tokens] ${skill}.md has a @banned block with no backticked tokens in it. ` +
115
- `An empty list would silently disable the check instead of failing it.`);
116
- }
117
- return out;
118
- }
119
- /** Every banned token, in skill-document order. Integrity asserted at load. */
120
- export const BANNED_PROMPT_TOKENS = BANNED_TOKEN_SOURCES.flatMap(({ skill, scope }) => extractFromSkill(skill).map((token) => ({ token, skill, scope })));
121
- const byScope = new Map();
122
- for (const entry of BANNED_PROMPT_TOKENS) {
123
- const list = byScope.get(entry.scope) ?? [];
124
- list.push(entry);
125
- byScope.set(entry.scope, list);
126
- }
127
- export function bannedTokensFor(scope) {
128
- return byScope.get(scope) ?? [];
129
- }
130
- /** Regex cache — one word-boundary matcher per token, built once. */
4
+ import { SKILLS } from '../skills/content.js';
5
+ const bySkill = new Map();
6
+ for (const [skill, content] of Object.entries(SKILLS)) {
7
+ const fences = [...content.matchAll(/<!--\s*@banned:start\s*-->([\s\S]*?)<!--\s*@banned:end\s*-->/g)];
8
+ if (!fences.length)
9
+ continue;
10
+ const tokens = [...new Set(fences.flatMap((f) => [...f[1].replace(/<!--[\s\S]*?-->/g, '').matchAll(/`([^`\n]+)`/g)].map((m) => m[1].trim())))];
11
+ if (!tokens.length)
12
+ throw new Error(`${skill}: @banned block contains no tokens`);
13
+ const scope = /nano-banana|gpt-image|flux|seedream/.test(skill) ? 'image' : 'video';
14
+ bySkill.set(skill, tokens.map((token) => ({ token, skill, scope })));
15
+ }
16
+ /** Compatibility exports: there is no cross-model blacklist. */
17
+ export const BANNED_PROMPT_TOKENS = Object.freeze([]);
18
+ export function bannedTokensFor(_scope) { return BANNED_PROMPT_TOKENS; }
19
+ export function describeBannedTokens(_scope) { return ''; }
20
+ export function bannedTokensForSkill(skill) { return bySkill.get(skill) ?? []; }
131
21
  const matchers = new Map();
132
22
  function matcherFor(token) {
133
- let re = matchers.get(token);
134
- if (!re) {
23
+ let matcher = matchers.get(token);
24
+ if (!matcher) {
135
25
  const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
136
- // \b at both ends so `4k` does not fire inside "84king" and `flawless`
137
- // does not fire inside "flawlessly". Every token starts and ends with a
138
- // word character today; if one ever starts with punctuation, \b would
139
- // anchor wrong — the load-time check below is what would catch it.
140
- re = new RegExp(`\\b${escaped}\\b`, 'i');
141
- matchers.set(token, re);
142
- }
143
- return re;
144
- }
145
- for (const { token, skill } of BANNED_PROMPT_TOKENS) {
146
- if (!/^\w/.test(token) || !/\w$/.test(token)) {
147
- throw new Error(`[banned-tokens] ${skill}: token ${JSON.stringify(token)} does not start and end with a word ` +
148
- `character, so the \\b word-boundary match would never fire. Rewrite the entry or teach ` +
149
- `matcherFor() the new shape.`);
26
+ matcher = new RegExp(`(?<!\\w)${escaped}(?!\\w)`, 'i');
27
+ matchers.set(token, matcher);
150
28
  }
29
+ return matcher;
151
30
  }
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) ?? [];
31
+ export function findBannedTokens(prompt, _scope, skill) {
32
+ return skill ? bannedTokensForSkill(skill).filter((b) => matcherFor(b.token).test(prompt)) : [];
157
33
  }
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
34
  export function describeBannedTokensForSkill(skill) {
166
35
  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}).`);
36
+ return list.length ? `Avoid these phrases for ${skill}: ${list.map((b) => `"${b.token}"`).join(', ')}. Describe the intended result specifically.` : '';
172
37
  }
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;
188
- }
189
- /**
190
- * The list as it appears INSIDE an op description — always in context, on both
191
- * surfaces, with no call required to see it. Generated, never hand-typed.
192
- */
193
- export function describeBannedTokens(scope) {
194
- const list = bannedTokensFor(scope);
195
- if (list.length === 0)
196
- return '';
197
- const skills = [...new Set(list.map((b) => b.skill))].join(' / ');
198
- return (`NEVER put these in a prompt (they measurably degrade output — full rationale in ${skills}): ` +
199
- list.map((b) => `"${b.token}"`).join(', ') +
200
- `. Describe specifically instead.`);
201
- }
202
- /**
203
- * Non-blocking warning for a submitted prompt. Empty string when clean.
204
- *
205
- * Returned in the op RESULT — the one place the agent cannot avoid reading —
206
- * rather than raised as an error. The generation proceeds either way: the
207
- * sandbox doctrine says make state visible, never block.
208
- */
209
38
  export function bannedTokenWarning(prompt, scope, skill) {
210
39
  const hits = findBannedTokens(prompt, scope, skill);
211
- if (hits.length === 0)
40
+ if (!hits.length)
212
41
  return '';
213
- const skills = [...new Set(hits.map((b) => b.skill))].join(', ');
214
- return (`⚠️ PROMPT WARNING: your prompt contains ${hits.map((b) => `"${b.token}"`).join(', ')} — ` +
215
- `on the never-use list in ${skills}. Not blocked, and this generation ran as submitted. ` +
216
- `Load that guide with slates_get_prompting_guide and rewrite with specific description ` +
217
- `(named lens, light direction, stock, composition) before the next generation.`);
42
+ return `⚠️ PROMPT WARNING: ${hits.map((b) => `"${b.token}"`).join(', ')} appear in ${skill}'s avoid list. This warning does not change or block your prompt. Describe the intended result specifically; query that guide for details.`;
218
43
  }
219
44
  //# sourceMappingURL=banned-tokens.js.map
@@ -57,7 +57,7 @@ function extractCard(skill, content) {
57
57
  if (body.length > CRAFT_CARD_CEILING) {
58
58
  throw new Error(`[craft-cards] ${skill}.md's @card block is ${body.length} chars, over the ` +
59
59
  `${CRAFT_CARD_CEILING} ceiling. A card rides EVERY estimate result for that model — ` +
60
- `move the overflow into the body of the skill, which slates_get_prompting_guide returns whole.`);
60
+ `move the overflow into the body of the skill, retrieved with slates_get_prompting_guide depth: full.`);
61
61
  }
62
62
  return body;
63
63
  }
@@ -77,6 +77,6 @@ export function describeCraftCard(skill) {
77
77
  const card = craftCard(skill);
78
78
  if (!card)
79
79
  return '';
80
- return `${card}\n\nFull guide (examples, failure modes, sources): slates_get_prompting_guide("${skill}").`;
80
+ return `${card}\n\nFull guide (examples, failure modes, sources): slates_get_prompting_guide({topic: "${skill}", depth: "full"}); use query for one section.`;
81
81
  }
82
82
  //# sourceMappingURL=craft-cards.js.map
@@ -0,0 +1,41 @@
1
+ /** Product fan-out policy: one provider request and debit per output.
2
+ * This is NOT a model capability or provider batch limit. Desktop mirror is
3
+ * generated by slate/scripts/sync-generation-policy.mjs; never edit it there. */
4
+ export declare const IMAGE_QUANTITIES: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
5
+ export declare const MAX_IMAGE_VARIATIONS: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
6
+ export type ImageQuantity = typeof IMAGE_QUANTITIES[number];
7
+ export declare const clampImageQuantity: (count?: number) => ImageQuantity;
8
+ /** Default saved-recipe framing, shared by composer restore, quote and dispatch. */
9
+ export declare const DEFAULT_SHOT_ASPECT_RATIO: "16:9";
10
+ /**
11
+ * The built-in tools, and the seat each one renders on when its caller names no
12
+ * model. THE ONE HOME, for both repos: the desktop reads it through its generated
13
+ * mirror (`toolModel` in slate/src/shared/pricing.ts), this package reads it for
14
+ * the op descriptions and the generated skill partial.
15
+ *
16
+ * `'default-image'` means "follow the app's default image model"
17
+ * (`defaultModelFor('image')` in model-facts.ts), so promoting a better image
18
+ * model moves these tools with it and nothing else is edited. A model id pins a
19
+ * tool instead, for a reason recorded beside it. This file stays a
20
+ * dependency-free leaf (it is copied verbatim into the desktop), which is why
21
+ * the seat is a token each consumer resolves rather than a call made here.
22
+ *
23
+ * Before 2026-09-21 the sheet tools' model was typed at every use on the desktop
24
+ * and twice more here (an op description and a skill), so three screens quoted
25
+ * a sheet from one typed id while the handler billed from another.
26
+ */
27
+ export declare const BUILT_IN_TOOLS: readonly ["character-sheet", "environment-plate", "grid-extract", "image-edit"];
28
+ export type BuiltInTool = typeof BUILT_IN_TOOLS[number];
29
+ export declare const DEFAULT_IMAGE_SEAT: "default-image";
30
+ export declare const TOOL_SEAT: Record<BuiltInTool, typeof DEFAULT_IMAGE_SEAT | string>;
31
+ /** Both sheet tools frame one wide image. On GPT Image the aspect is part of the price. */
32
+ export declare const SHEET_TOOL_ASPECT_RATIO: "16:9";
33
+ /**
34
+ * The GPT Image tier a sheet tool pins. It read 'medium' until 2026-09-09: GPT
35
+ * Image 2.5 renamed the ladder, so the tier GPT Image 2 called `medium` is now
36
+ * `high`, and a stored 'medium' would have kept compiling while naming a
37
+ * materially cheaper tier. Sheets are the identity lane (every downstream shot
38
+ * inherits this frame's likeness), so this is deliberately not the draft tier.
39
+ */
40
+ export declare const SHEET_TOOL_GPT_QUALITY: "high";
41
+ //# sourceMappingURL=generation-policy.d.ts.map
@@ -0,0 +1,53 @@
1
+ /** Product fan-out policy: one provider request and debit per output.
2
+ * This is NOT a model capability or provider batch limit. Desktop mirror is
3
+ * generated by slate/scripts/sync-generation-policy.mjs; never edit it there. */
4
+ export const IMAGE_QUANTITIES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
5
+ export const MAX_IMAGE_VARIATIONS = IMAGE_QUANTITIES[IMAGE_QUANTITIES.length - 1];
6
+ export const clampImageQuantity = (count = 1) => Math.max(1, Math.min(MAX_IMAGE_VARIATIONS, Number.isFinite(count) ? Math.floor(count) : 1));
7
+ /** Default saved-recipe framing, shared by composer restore, quote and dispatch. */
8
+ export const DEFAULT_SHOT_ASPECT_RATIO = '16:9';
9
+ /**
10
+ * The built-in tools, and the seat each one renders on when its caller names no
11
+ * model. THE ONE HOME, for both repos: the desktop reads it through its generated
12
+ * mirror (`toolModel` in slate/src/shared/pricing.ts), this package reads it for
13
+ * the op descriptions and the generated skill partial.
14
+ *
15
+ * `'default-image'` means "follow the app's default image model"
16
+ * (`defaultModelFor('image')` in model-facts.ts), so promoting a better image
17
+ * model moves these tools with it and nothing else is edited. A model id pins a
18
+ * tool instead, for a reason recorded beside it. This file stays a
19
+ * dependency-free leaf (it is copied verbatim into the desktop), which is why
20
+ * the seat is a token each consumer resolves rather than a call made here.
21
+ *
22
+ * Before 2026-09-21 the sheet tools' model was typed at every use on the desktop
23
+ * and twice more here (an op description and a skill), so three screens quoted
24
+ * a sheet from one typed id while the handler billed from another.
25
+ */
26
+ export const BUILT_IN_TOOLS = ['character-sheet', 'environment-plate', 'grid-extract', 'image-edit'];
27
+ export const DEFAULT_IMAGE_SEAT = 'default-image';
28
+ export const TOOL_SEAT = {
29
+ 'character-sheet': DEFAULT_IMAGE_SEAT,
30
+ 'environment-plate': DEFAULT_IMAGE_SEAT,
31
+ // Pinned: the image viewer offers grid extraction a fixed short list of seats,
32
+ // and the cell-extraction prompt has a variant per seat family. Moving it is a
33
+ // picker and a prompt change first.
34
+ 'grid-extract': 'nano-banana-2',
35
+ // Follows the default image model (2026-09-29). The viewer's Edit box lists
36
+ // every image model from the registry, and the edit handler sends references
37
+ // to each one up to its own cap, so nothing ties this tool to one family. Until
38
+ // then it was pinned to Nano Banana 2 while the app's default was GPT Image 2.5
39
+ // Sunburst, so Edit and the prompt bar opened on different models for the
40
+ // same picture.
41
+ 'image-edit': DEFAULT_IMAGE_SEAT,
42
+ };
43
+ /** Both sheet tools frame one wide image. On GPT Image the aspect is part of the price. */
44
+ export const SHEET_TOOL_ASPECT_RATIO = '16:9';
45
+ /**
46
+ * The GPT Image tier a sheet tool pins. It read 'medium' until 2026-09-09: GPT
47
+ * Image 2.5 renamed the ladder, so the tier GPT Image 2 called `medium` is now
48
+ * `high`, and a stored 'medium' would have kept compiling while naming a
49
+ * materially cheaper tier. Sheets are the identity lane (every downstream shot
50
+ * inherits this frame's likeness), so this is deliberately not the draft tier.
51
+ */
52
+ export const SHEET_TOOL_GPT_QUALITY = 'high';
53
+ //# sourceMappingURL=generation-policy.js.map
@@ -0,0 +1,9 @@
1
+ export type GuideDepth = 'card' | 'index' | 'section' | 'full';
2
+ /** Parse headings outside code fences; maintainer comments never reach agents. */
3
+ export declare function guideSections(content: string): Array<{
4
+ title: string;
5
+ body: string;
6
+ }>;
7
+ /** Bounded retrieval. A missing card never silently expands to the full guide. */
8
+ export declare function retrieveGuide(skill: string, content: string, depth: GuideDepth, query?: string): string;
9
+ //# sourceMappingURL=guide-retrieval.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { craftCard } from './craft-cards.js';
2
+ /** Parse headings outside code fences; maintainer comments never reach agents. */
3
+ export function guideSections(content) {
4
+ const clean = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').replace(/<!--[\s\S]*?-->/g, '').trim();
5
+ const sections = [];
6
+ let current = { title: 'Overview', body: '' };
7
+ let fenced = false;
8
+ for (const line of clean.split(/\r?\n/)) {
9
+ if (/^\s*```/.test(line))
10
+ fenced = !fenced;
11
+ if (!fenced && /^#{1,3} /.test(line)) {
12
+ if (current.body.trim())
13
+ sections.push(current);
14
+ current = { title: line.replace(/^#+ /, ''), body: line };
15
+ }
16
+ else
17
+ current.body += '\n' + line;
18
+ }
19
+ if (current.body.trim())
20
+ sections.push(current);
21
+ return sections;
22
+ }
23
+ /** Bounded retrieval. A missing card never silently expands to the full guide. */
24
+ export function retrieveGuide(skill, content, depth, query) {
25
+ const sections = guideSections(content);
26
+ const index = sections.map((s) => `- ${s.title}`).join('\n');
27
+ if (depth === 'full')
28
+ return sections.map((s) => s.body.trim()).join('\n\n');
29
+ if (depth === 'index')
30
+ return index;
31
+ if (query?.trim()) {
32
+ const q = query.trim().toLowerCase();
33
+ // Exact technique IDs return one complete row with its source heading.
34
+ for (const s of sections) {
35
+ const row = s.body.split('\n').find((line) => line.toLowerCase().startsWith(`| \`${q}\` |`));
36
+ if (row)
37
+ return `${s.title}\n\n| Technique | Evidence | What it does | Reach for · skip | Say |\n|---|---|---|---|---|\n${row}`;
38
+ }
39
+ const words = q.split(/[^\p{L}\p{N}-]+/u).filter(Boolean);
40
+ const ranked = sections.map((s) => ({ s, score: words.reduce((n, w) => n + (s.title.toLowerCase().includes(w) ? 4 : s.body.toLowerCase().includes(w) ? 1 : 0), 0) }))
41
+ .filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
42
+ if (!ranked.length)
43
+ return `No section matches "${query}". Available sections:\n${index}`;
44
+ const body = ranked[0].s.body.trim();
45
+ if (body.length <= 6000)
46
+ return body;
47
+ // Never cut a table row or a worked prompt mid-sentence.
48
+ return `Section "${ranked[0].s.title}" is too large for a selective response. Use a technique ID or depth "full".\n${index}`;
49
+ }
50
+ const card = craftCard(skill);
51
+ return `${card ?? sections[0]?.body.trim().slice(0, 1600) ?? 'No overview available.'}\n\nSections (request with query, or depth "full"):\n${index}`;
52
+ }
53
+ //# sourceMappingURL=guide-retrieval.js.map
@@ -8,4 +8,5 @@ export * from './character-sheet.js';
8
8
  export * from './environment-sheet.js';
9
9
  export * from './prompting-tips.js';
10
10
  export * from './asset-label.js';
11
+ export * from './generation-policy.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -28,4 +28,5 @@ export * from './prompting-tips.js';
28
28
  // gallery. Also its own leaf subpath (`@slatesvideo/shared/asset-label`) for the
29
29
  // renderer, which cannot import the root barrel.
30
30
  export * from './asset-label.js';
31
+ export * from './generation-policy.js';
31
32
  //# sourceMappingURL=index.js.map
@@ -47,6 +47,14 @@ export interface VideoResolutionCapability {
47
47
  fixed?: VideoResolution;
48
48
  /** Default when this model is chosen (falls back to `options[0]`). */
49
49
  default?: VideoResolution;
50
+ /**
51
+ * Tiers the provider makes by UPSCALING a smaller native render, keyed to that
52
+ * render. They stay selectable; every surface that offers one says so and says
53
+ * we do not recommend it (Eric, 2026-09-30: "more expensive and they actually
54
+ * look worse"). A refinement pass the provider documents as its own stage, such
55
+ * as H3 Max's 1080p, is not an upscale and is not listed.
56
+ */
57
+ upscaledFrom?: Partial<Record<VideoResolution, VideoResolution>>;
50
58
  }
51
59
  /** Everything a model will ACCEPT. Capability only — never a price. */
52
60
  /**
@@ -86,6 +94,7 @@ export interface VoiceCloneCapability {
86
94
  }
87
95
  export declare const GPT_QUALITY_TIERS: readonly ["low", "medium", "high", "xhigh", "max"];
88
96
  export type GptQuality = (typeof GPT_QUALITY_TIERS)[number];
97
+ export declare const DEFAULT_GPT_QUALITY: GptQuality;
89
98
  export declare const GPT_BACKGROUNDS: readonly ["auto", "transparent", "opaque"];
90
99
  export type GptBackground = (typeof GPT_BACKGROUNDS)[number];
91
100
  export type ImageResolution = '1k' | '2k' | '3k' | '4k';
@@ -128,6 +137,8 @@ export declare function falImageSize(model: string, aspectRatio?: string, resolu
128
137
  };
129
138
  export interface ModelCapability {
130
139
  imageResolutions?: ImageResolution[];
140
+ /** Product default shared by estimates and generation. */
141
+ defaultImageResolution?: ImageResolution;
131
142
  /**
132
143
  * Images ONE request may ask the provider for in a single batch.
133
144
  *
@@ -204,6 +215,8 @@ export declare function voiceCloneFor(model: string): VoiceCloneCapability | und
204
215
  /** Aspect ratios a model accepts, honouring the provider override. */
205
216
  export declare function aspectRatiosFor(model: string, provider?: string): AspectRatio[];
206
217
  /** Video resolutions a model accepts. A FIXED model reports exactly its one value. */
218
+ /** The native render an upscaled tier is made from, or undefined for a native tier. */
219
+ export declare function upscaledFrom(model: string, resolution: string | undefined): VideoResolution | undefined;
207
220
  export declare function videoResolutionsFor(model: string): VideoResolution[];
208
221
  /** The resolution a model would actually run at. Fixed wins; else keep a legal
209
222
  * current value; else the model's own default. Mirrors `clampVideoResolution`. */
@@ -250,7 +263,9 @@ export declare function describeDurations(models: readonly string[]): string;
250
263
  export declare function describeReferenceImageCaps(models: readonly string[]): string;
251
264
  /** H3 Max reference accounting, fal's worked tables read 2026-09-09.
252
265
  * https://fal.ai/models/minimax/h3-max/reference-to-video
253
- * 1080p video-reference pricing is unpublished; never infer it from output rates.
266
+ * 1080p: fal's page, read 2026-09-29: "768p and 1080p output use the same
267
+ * reference-video token counts." So 1080p takes the 768p figure; it was absent
268
+ * until fal published that sentence, and must never be inferred from output rates.
254
269
  */
255
270
  export declare const MINIMAX_MAX_REFERENCE: {
256
271
  readonly freeTokens: 4096;
@@ -265,4 +280,6 @@ export declare function minimaxMaxReferenceTokens(input: {
265
280
  audioSeconds: number;
266
281
  resolution: string;
267
282
  }): number;
283
+ /** Quotes and generation use the same model default. */
284
+ export declare function defaultImageResolutionFor(model: string): ImageResolution;
268
285
  //# sourceMappingURL=model-capabilities.d.ts.map