stillmade-sdk 0.1.25 → 0.1.27

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/SDK.md CHANGED
@@ -12192,7 +12192,6 @@ Music, sound effects and catalog voices are stored as the provider's measured MP
12192
12192
  | `task` | A Project Type Step. Its primary input and output connect automatically to neighbouring Steps. Use this for almost every Block. |
12193
12193
  | `workspace` | A complete project workspace that owns a project document (like Shots or the Editor). Never connected automatically: neighbours only reach it through project context or explicit connections. |
12194
12194
  | `editor-extension` | Accepted by the validator; no host behaviour depends on it today. Use `task`. |
12195
- | `composite` | Accepted by the validator; no host behaviour depends on it today. Use `task`. |
12196
12195
 
12197
12196
  ### Generated controls
12198
12197
  `manifest.ui` binds generated controls to inputs. A custom view replaces them in the interface, but keep them as the fallback schema.
@@ -179,7 +179,7 @@ export default {
179
179
  },
180
180
  "view": {
181
181
  "runtime": "frame",
182
- "html": "<main class=\"sm-block nr\">\n <section class=\"sm-stage nr-stage\" id=\"stage\" aria-live=\"polite\">\n <div class=\"sm-empty\" id=\"empty\">\n <span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span>\n <p>No narration yet</p>\n </div>\n <div class=\"nr-take\" id=\"take\" hidden>\n <audio id=\"player\" controls preload=\"metadata\"></audio>\n <p class=\"nr-meta\"><span id=\"length\"></span><span id=\"from\"></span></p>\n </div>\n </section>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"facts\">Facts</label><details class=\"sm-hint\"><summary aria-label=\"About the facts\" title=\"About the facts\"></summary><p>What the voice may say: the thing, its details, who it is for, how long, and how to reach you. Chat fills this from your conversation; change it here.</p></details><span class=\"sm-status\" id=\"facts-state\"></span></span>\n <textarea id=\"facts\" rows=\"3\" maxlength=\"6000\" data-stillmade-share=\"facts\" placeholder=\"2021 Toyota RAV4 XLE, $27,995, 31,200 miles, one owner. Sunrise Motors, (555) 201-7788.\"></textarea>\n </div>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"words\">Script</label><span class=\"sm-status\" id=\"words-state\"></span></span>\n <textarea id=\"words\" rows=\"7\" maxlength=\"3800\" data-stillmade-share=\"words\" placeholder=\"Written from the facts, or type what the voice says.\"></textarea>\n </div>\n <fieldset class=\"sm-choices\" data-compact id=\"voice\" data-stillmade-share=\"voice\">\n <legend>Voice</legend>\n <label><input type=\"radio\" name=\"voice\" value=\"onyx\" checked><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Onyx <small>deep</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"nova\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Nova <small>bright</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"alloy\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Alloy <small>even</small></span></label>\n </fieldset>\n <div class=\"sm-actions\">\n <button type=\"button\" class=\"sm-primary\" id=\"action\" data-stillmade-action=\"run\">Write script</button>\n <button type=\"button\" class=\"sm-quiet\" id=\"again\" hidden>Write again</button>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n <details class=\"sm-hint\" id=\"why\" hidden><summary aria-label=\"Why\" title=\"Why\"></summary><p id=\"why-text\"></p></details>\n </div>\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-settings\">\n <fieldset class=\"sm-choices\" data-compact id=\"tone\" data-stillmade-share=\"tone\">\n <legend>Tone</legend>\n <label><input type=\"radio\" name=\"tone\" value=\"warm\" checked><span>Warm</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"friendly\"><span>Friendly</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"no-nonsense\"><span>No-nonsense</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"luxury\"><span>Luxury</span></label>\n </fieldset>\n <fieldset class=\"sm-choices\" data-segmented id=\"speed\" data-stillmade-share=\"speed\">\n <legend>Speed</legend>\n <label><input type=\"radio\" name=\"speed\" value=\"0.9\"><span>Slower</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1\" checked><span>Normal</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1.15\"><span>Faster</span></label>\n </fieldset>\n <div class=\"sm-field\"><label for=\"rules\">Own rules</label><input id=\"rules\" type=\"text\" maxlength=\"300\" data-stillmade-share=\"rules\" placeholder=\"Short sentences, no exclamation marks\"></div>\n </div>\n </details>\n</main>\n",
182
+ "html": "<main class=\"sm-block nr\">\n <section class=\"sm-stage nr-stage\" id=\"stage\" aria-live=\"polite\">\n <div class=\"sm-empty\" id=\"empty\">\n <span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span>\n <p>No narration yet</p>\n </div>\n <div class=\"nr-take\" id=\"take\" hidden>\n <audio id=\"player\" controls preload=\"metadata\"></audio>\n <p class=\"nr-meta\"><span id=\"length\"></span><span id=\"from\"></span></p>\n </div>\n </section>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"facts\">Facts</label><details class=\"sm-hint\"><summary aria-label=\"About the facts\" title=\"About the facts\"></summary><p>What the voice may say: the thing, its details, who it is for, how long, and how to reach you. Chat fills this from your conversation; change it here.</p></details><span class=\"sm-status\" id=\"facts-state\"></span></span>\n <textarea id=\"facts\" rows=\"3\" maxlength=\"6000\" data-stillmade-share=\"facts\" placeholder=\"What it is, its details, who it is for, and how to reach you.\"></textarea>\n </div>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"words\">Script</label><span class=\"sm-status\" id=\"words-state\"></span></span>\n <textarea id=\"words\" rows=\"7\" maxlength=\"3800\" data-stillmade-share=\"words\" placeholder=\"Written from the facts, or type what the voice says.\"></textarea>\n </div>\n <fieldset class=\"sm-choices\" data-compact id=\"voice\" data-stillmade-share=\"voice\">\n <legend>Voice</legend>\n <label><input type=\"radio\" name=\"voice\" value=\"onyx\" checked><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Onyx <small>deep</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"nova\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Nova <small>bright</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"alloy\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Alloy <small>even</small></span></label>\n </fieldset>\n <div class=\"sm-actions\">\n <button type=\"button\" class=\"sm-primary\" id=\"action\" data-stillmade-action=\"run\">Write script</button>\n <button type=\"button\" class=\"sm-quiet\" id=\"again\" hidden>Write again</button>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n <details class=\"sm-hint\" id=\"why\" hidden><summary aria-label=\"Why\" title=\"Why\"></summary><p id=\"why-text\"></p></details>\n </div>\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-settings\">\n <fieldset class=\"sm-choices\" data-compact id=\"tone\" data-stillmade-share=\"tone\">\n <legend>Tone</legend>\n <label><input type=\"radio\" name=\"tone\" value=\"warm\" checked><span>Warm</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"friendly\"><span>Friendly</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"no-nonsense\"><span>No-nonsense</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"luxury\"><span>Luxury</span></label>\n </fieldset>\n <fieldset class=\"sm-choices\" data-segmented id=\"speed\" data-stillmade-share=\"speed\">\n <legend>Speed</legend>\n <label><input type=\"radio\" name=\"speed\" value=\"0.9\"><span>Slower</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1\" checked><span>Normal</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1.15\"><span>Faster</span></label>\n </fieldset>\n <div class=\"sm-field\"><label for=\"rules\">Own rules</label><input id=\"rules\" type=\"text\" maxlength=\"300\" data-stillmade-share=\"rules\" placeholder=\"Short sentences, no exclamation marks\"></div>\n </div>\n </details>\n</main>\n",
183
183
  "css": "/* The Block kit (injected by the host) carries the layout; this file only draws\n the voiceover on the stage: the player and its two facts. */\n.nr [hidden] { display: none !important; }\n.nr-stage { display: grid; align-content: center; justify-items: stretch; gap: var(--s-2, 8px); min-height: 180px; }\n/* A voiceover is short: on a wide screen the stage hugs the player instead of stretching down the column. */\n@container (min-width: 860px) { .sm-block:not([data-layout]) > .nr-stage { align-self: start; min-height: 220px; } }\n.nr-stage > .sm-empty { justify-self: center; }\n.nr-take { display: grid; gap: var(--s-2, 8px); width: 100%; }\n.nr-take audio { width: 100%; }\n.nr-meta { display: flex; justify-content: space-between; gap: var(--s-3, 12px); margin: 0; color: var(--fg-muted); font-size: var(--fs-body-s, .875rem); font-variant-numeric: tabular-nums; }\n#voice { --sm-tile: 120px; }\n#voice small { color: var(--fg-subtle); font-size: var(--fs-caption, .8125rem); margin-inline-start: 4px; }\n#tone { --sm-tile: 110px; }\n",
184
184
  "javascript": "(()=>{\n\"use strict\";\n// The Narration interface on the Block kit: the voiceover on the stage, the\n// facts and the script on the left, the voice as chips, and one button that\n// names the next step: Write script, then Make voiceover, or Use my recording.\n// The status beside it says the state in a few words; the price is StillMade's.\nconst $ = id => document.getElementById(id);\nconst state = { input: {}, outputs: {}, busy: false, previewUrl: null, previewFor: null, factsFromChat: false };\nconst message = error => String(error && error.message || error || 'Something went wrong.');\nconst say = (id, text) => { $(id).textContent = text || ''; };\nconst trimmed = value => String(value || '').trim();\nconst wordCount = text => trimmed(text) ? trimmed(text).split(/\\s+/).length : 0;\nconst clock = seconds => { const whole = Math.max(0, Math.round(seconds)); return Math.floor(whole / 60) + ':' + String(whole % 60).padStart(2, '0'); };\nconst spoken = text => 'about ' + clock(wordCount(text) / 150 * 60);\nconst picked = name => { const node = document.querySelector('input[name=\"' + name + '\"]:checked'); return node ? node.value : ''; };\nconst pick = (name, value) => { const node = document.querySelector('input[name=\"' + name + '\"][value=\"' + value + '\"]'); if (node) node.checked = true; };\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nconst words = value => typeof value === 'string' ? value : value && typeof value === 'object' ? trimmed(value.text || value.topic || value.summary || '') : '';\nconst facts = () => trimmed($('facts').value);\nconst script = () => trimmed($('words').value);\nconst sameWords = (a, b) => trimmed(a).replace(/\\s+/g, ' ') === trimmed(b).replace(/\\s+/g, ' ');\n\nfunction status(text, tone, why) {\n const node = $('status');\n node.textContent = text || '';\n if (tone) node.dataset.state = tone; else delete node.dataset.state;\n $('why').hidden = !why; $('why').open = false; say('why-text', why || '');\n}\n// A field the host shares is written through its own input event, so the edit is kept.\nfunction setField(id, value) {\n const node = $(id);\n if (node.value === value) return;\n node.value = value;\n node.dispatchEvent(new Event('input', { bubbles: true }));\n node.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\n// ── What the person is about to do ──────────────────────────────────────\nfunction phase() {\n if (!script()) return 'write';\n if (state.input.take) return 'take';\n const audio = state.outputs.audio, made = state.outputs.script;\n return audio && made && sameWords(words(made), script()) ? 'ready' : 'voice';\n}\nfunction render() {\n const p = phase(), count = wordCount(script());\n say('words-state', count ? count + ' words · ' + spoken(script()) : '');\n say('facts-state', facts() ? (state.factsFromChat ? 'From Chat' : '') : 'Needed');\n const labels = { write: 'Write script', voice: 'Make voiceover', ready: 'Make voiceover again', take: 'Use my recording' };\n say('action', labels[p]);\n $('again').hidden = p === 'write';\n $('action').disabled = state.busy || (p === 'write' && !facts());\n $('again').disabled = state.busy;\n if (state.busy) return;\n if (p === 'write') status(facts() ? 'Written from the facts' : 'Add the facts first', facts() ? '' : 'waiting', facts() ? 'The script uses only what the facts say; you can change every word before the voice runs.' : 'Chat usually fills the facts from your conversation. Type them here when it has not.');\n else if (p === 'take') status('Your recording will be used', '', 'The audio you supplied becomes the voiceover; nothing is generated.');\n else if (p === 'voice') status(state.outputs.audio ? 'Script changed, so the voice runs again' : 'Script is done, so only the voice runs', '', 'Only the voice is generated now; the words stay as written.');\n else status('Voiceover ready', 'success', '');\n}\n\n// ── The stage: the voiceover, when there is one ───────────────────────────\nasync function showAudio(reference, from) {\n if (!reference) { $('take').hidden = true; $('empty').hidden = false; return; }\n $('empty').hidden = true; $('take').hidden = false;\n say('from', from || '');\n say('length', Number.isFinite(reference.duration) ? clock(reference.duration) : spoken(script()));\n const key = reference.assetId + '/' + reference.versionId;\n if (state.previewFor === key) return;\n if (state.previewUrl) { try { StillMade.releaseMedia(state.previewUrl); } catch {} state.previewUrl = null; }\n state.previewFor = key;\n try {\n const preview = await StillMade.previewMedia(reference);\n if (state.previewFor !== key) { try { StillMade.releaseMedia(preview.url); } catch {} return; }\n state.previewUrl = preview.url; $('player').src = preview.url;\n } catch (error) {\n if (typeof reference.url === 'string' && /^https?:|^\\//.test(reference.url)) $('player').src = reference.url;\n else status(message(error), 'error', '');\n }\n}\n\n// ── Running ───────────────────────────────────────────────────────────────\nfunction values(forVoice) {\n const value = {};\n if (facts()) value.brief = { schemaVersion: 1, text: facts() };\n value.tone = picked('tone') || 'warm';\n const rules = trimmed($('rules').value); if (rules) value.rules = rules;\n if (forVoice) {\n value.script = { schemaVersion: 1, text: script() };\n value.voice = picked('voice') || 'onyx';\n value.speed = Number(picked('speed')) || 1;\n if (state.input.take) value.take = state.input.take;\n }\n return value;\n}\nasync function act(writeAgain) {\n if (state.busy) return;\n const p = writeAgain ? 'write' : phase();\n state.busy = true; render();\n status(p === 'write' ? 'Writing the script' : p === 'take' ? 'Keeping your recording' : 'Making the voice', 'loading', '');\n try {\n const result = await StillMade.run(values(p !== 'write'));\n state.outputs = { ...state.outputs, ...result };\n if (p === 'write') { setField('words', words(result.script)); await showAudio(null); }\n else await showAudio(result.audio, p === 'take' ? 'Your recording' : voiceName());\n } catch (error) {\n status(message(error), 'error', '');\n state.busy = false; render(); return;\n }\n state.busy = false; render();\n}\nconst voiceName = () => { const node = document.querySelector('input[name=\"voice\"]:checked'); return node ? node.parentElement.textContent.trim().split(/\\s+/)[0] : 'Voice'; };\n$('action').addEventListener('click', () => act(false));\n$('again').addEventListener('click', () => act(true));\nfor (const id of ['facts', 'words', 'rules']) $(id).addEventListener('input', () => { if (id === 'facts') state.factsFromChat = false; render(); });\nfor (const name of ['tone', 'voice', 'speed']) for (const node of document.querySelectorAll('input[name=\"' + name + '\"]')) node.addEventListener('change', render);\n\n// ── What the host gives: the Step's inputs and what was saved before ──────\nStillMade.onInput(input => {\n state.input = input || {};\n fillFromInput();\n if (typeof state.input.tone === 'string' && state.input.tone in { warm: 1, friendly: 1, 'no-nonsense': 1, luxury: 1 }) pick('tone', state.input.tone);\n if (typeof state.input.voice === 'string') pick('voice', state.input.voice);\n render();\n});\n// The host's shared fields start from the saved shared state, which can arrive after the\n// Step's input did and leave a bound field empty; what the input gave is put back then.\nfunction fillFromInput() {\n const brief = words(state.input.brief);\n if (brief && !facts()) { setField('facts', brief); state.factsFromChat = true; }\n const given = words(state.input.script);\n if (given && !script()) setField('words', given);\n}\nfunction adopt(snapshot) {\n const outputs = snapshot && snapshot.outputs || {};\n state.outputs = outputs;\n fillFromInput();\n if (outputs.script && !script()) setField('words', words(outputs.script));\n if (outputs.audio) showAudio(outputs.audio, state.input.take ? 'Your recording' : voiceName()); else showAudio(null);\n render();\n}\nif (typeof StillMade.onShared === 'function') StillMade.onShared(adopt);\ntry { if (typeof StillMade.getShared === 'function') { const shared = StillMade.getShared(); if (shared && typeof shared.then === 'function') shared.then(adopt, () => {}); else adopt(shared); } } catch {}\nrender();\n\n})();\n"
185
185
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stillmade-sdk",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "StillMade Block SDK, CLI and examples",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "type": "module",
@@ -248,7 +248,11 @@ export async function main(rawArgs) {
248
248
  const source=await defineBrowserApplicationSource(pkg);
249
249
  throw Object.assign(new Error(`${source.manifest.name} passed the local browser-application source contract. Browser applications are judged only by StillMade's isolated review: import this exact package in the StillMade desktop app and run its review. It is not ready until that review passes.`),{code:'APPLICATION_REVIEW_REQUIRED'});
250
250
  }
251
- const readiness=await assessBlockReadiness(pkg);
251
+ // On the creator's own machine a module Block runs in the SDK's Node executor with the
252
+ // stand-in host that answers from tests/mocks.json, as test and preview do (October 20,
253
+ // 2026: ready refused every module Block at DOD-01, so none could pass the gate from the SDK).
254
+ const mock=pkg.manifest?.runtime==='module'?await standIns(path,pkg,true):null;
255
+ const readiness=await assessBlockReadiness(pkg,{moduleExecution:true,files:moduleFiles.get(pkg),...(mock?{host:mock.host}:{})});
252
256
  if(['FAILED','BLOCKED BY PLATFORM CAPABILITY'].includes(readiness.status))throw Object.assign(new Error(`Block readiness: ${readiness.status}`),{report:readiness});
253
257
  return readiness;
254
258
  }
@@ -9,6 +9,7 @@ import {readFile,readdir,writeFile,mkdir,lstat} from 'node:fs/promises';
9
9
  import {realpathSync} from 'node:fs';
10
10
  import {join,relative,resolve,sep} from 'node:path';
11
11
  import {fileURLToPath} from 'node:url';
12
+ import {BUILDER_CONTRACT_STAMP} from '../block-sdk/builder-contract.js';
12
13
 
13
14
  export const READY_GATE_STATE='.stillmade';
14
15
  // Shipped in the SDK download: Claude Code and Cursor run the gate whenever the
@@ -113,7 +114,9 @@ export async function runReadyGate({root=process.cwd(),main}={}){
113
114
  const name=posix(relative(root,path)),digest=await packageDigest(path);
114
115
  if(baseline[name]===digest)continue;
115
116
  const cached=cache.entries[name];
116
- const verdict=cached?.digest===digest?cached:{digest,...await judge(path,main),checkedAt:new Date().toISOString()};
117
+ // A verdict is reused only for the same package under the same contract: a newer SDK judges again
118
+ // (October 20, 2026: a FAILED verdict from the SDK that could not run module Blocks outlived the fix).
119
+ const verdict=cached?.digest===digest&&cached?.contract===BUILDER_CONTRACT_STAMP?cached:{digest,contract:BUILDER_CONTRACT_STAMP,...await judge(path,main),checkedAt:new Date().toISOString()};
117
120
  cache.entries[name]=verdict;
118
121
  results.push({package:name,...verdict});
119
122
  }
@@ -81,7 +81,8 @@ function rehearsalContext(packages){
81
81
  * @param options.liveReceipt {digest} of a reviewed live hosted run for this exact package
82
82
  * @param options.unsupported {reason,gapClass,primitive} when authoring reported a platform gap
83
83
  */
84
- export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES,admission,platformChecks,sharedEditing,liveReceipt,unsupported,budgetMs=20000,signal,moduleExecution=false}={}){
84
+ // files: a module Block's folder files (path -> bytes), run only when moduleExecution is true (the creator's own machine).
85
+ export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES,admission,platformChecks,sharedEditing,liveReceipt,unsupported,budgetMs=20000,signal,moduleExecution=false,files,host}={}){
85
86
  const deadline=Date.now()+budgetMs,left=()=>deadline-Date.now(),items=new Map(),composition={};
86
87
  const set=(id,state,detail={})=>{if(!items.has(id)||items.get(id).state!=='failed')items.set(id,{id,title:TITLES[id],state,...detail});};
87
88
  const verified=(id,evidence)=>set(id,'verified',{evidence});
@@ -103,7 +104,7 @@ export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES
103
104
 
104
105
  const manifest=pkg.manifest,sourceDigest=await digest(pkg),hosted=!offline(pkg);
105
106
  let report=admission;
106
- if(!report){try{report=await admitLocalPackage(pkg,{timeoutMs:Math.max(1000,Math.min(15000,left())),moduleExecution:moduleExecution===true});}catch(error){
107
+ if(!report){try{report=await admitLocalPackage(pkg,{timeoutMs:Math.max(1000,Math.min(15000,left())),moduleExecution:moduleExecution===true,...(files?{files}:{}),...(host?{host}:{})});}catch(error){
107
108
  // Admission running out of this check's own time budget is not a Block defect.
108
109
  const exhausted=error.code==='TIMEOUT'&&/shared budget/.test(error.message||'');
109
110
  report={passed:false,results:[{name:'Admission',passed:false,code:exhausted?'READINESS_BUDGET':error.code,message:message(error)}]};
@@ -1,7 +1,4 @@
1
- import {REVENUE_REVIEW_PROJECT_TYPES} from './revenue-review-project-types.js';
2
- import {PRODUCT_AD_VARIANTS_PROJECT_TYPE,PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE} from './product-ad-variants-project-type.js';
3
1
  import {FIRST_PARTY_SDK_PACKAGES} from '../block-sdk/first-party-packages.js';
4
- import {FIRST_REVENUE_PROJECT_TYPES} from './first-revenue-project-types.js';
5
2
  import {DEALER_INVENTORY_VIDEO_PROJECT_TYPE} from './dealer-inventory-video-project-type.js';
6
3
  import { SDK_VERSION, ROOT_CHAT_ID } from '../block-sdk/contracts.js';
7
4
  import {SCRIPT_WRITER_BLOCK} from './script-writer.js';
@@ -188,8 +185,19 @@ const PRIOR_CURRENT_TYPES = currentBuiltinTypes('1.13.0','2.2.3',SCRIPT_WRITER_B
188
185
  const PREVIOUS_PORTABLE_TYPES=currentBuiltinTypes('1.14.0','2.2.4',SCRIPT_WRITER_BLOCK.manifest.version,{'blueprint':'2.1.8','panel-strip':'2.3.6','board-editor':'2.2.9','animated-scene':'2.3.7','editor':'2.2.8'});
189
186
  // 1.16.0: every built-in type ends in the original Editor (1.0), the full video editor.
190
187
  const PORTABLE_EDITOR_TYPES=currentBuiltinTypes('1.15.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'2.2.9'});
191
- export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.16.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'1.0.0'}),...REVENUE_REVIEW_PROJECT_TYPES,PRODUCT_AD_VARIANTS_PROJECT_TYPE,...FIRST_REVENUE_PROJECT_TYPES,DEALER_INVENTORY_VIDEO_PROJECT_TYPE];
192
- const HISTORICAL_CURRENT_TYPES=[...PORTABLE_EDITOR_TYPES,PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE,...PREVIOUS_PORTABLE_TYPES,...PRIOR_CURRENT_TYPES,...SHARED_WORKSPACE_TYPES,...SHARED_CANVAS_TYPES,...SHARED_BLUEPRINT_TYPES,...PRIOR_SHARED_TYPES,...MEDIA_LIBRARY_TYPES,...EDITOR_AUDIO_TYPES,...PLAYABLE_EDITOR_TYPES,...ADVANCED_PORTABLE_TYPES,...PORTABLE_MVP_TYPES,...PRE_PROMOTION_CURRENT_TYPES,...PREVIOUS_BUILTIN_TYPES.filter(type=>flows[type.legacyMode].includes('shot-plan')),...currentBuiltinTypes('1.1.0','1.1.0','1.1.0',PRIOR_HOST_WORKSPACE_VERSIONS)];
188
+ export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.16.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'1.0.0'}),DEALER_INVENTORY_VIDEO_PROJECT_TYPE];
189
+ const HISTORICAL_CURRENT_TYPES=[...PORTABLE_EDITOR_TYPES,...PREVIOUS_PORTABLE_TYPES,...PRIOR_CURRENT_TYPES,...SHARED_WORKSPACE_TYPES,...SHARED_CANVAS_TYPES,...SHARED_BLUEPRINT_TYPES,...PRIOR_SHARED_TYPES,...MEDIA_LIBRARY_TYPES,...EDITOR_AUDIO_TYPES,...PLAYABLE_EDITOR_TYPES,...ADVANCED_PORTABLE_TYPES,...PORTABLE_MVP_TYPES,...PRE_PROMOTION_CURRENT_TYPES,...PREVIOUS_BUILTIN_TYPES.filter(type=>flows[type.legacyMode].includes('shot-plan')),...currentBuiltinTypes('1.1.0','1.1.0','1.1.0',PRIOR_HOST_WORKSPACE_VERSIONS)];
193
190
  export const BUILTIN_TYPES = [...CURRENT_BUILTIN_TYPES,...HISTORICAL_CURRENT_TYPES,...LEGACY_TYPES];
191
+ // Hidden from discovery (October 20, 2026, owner decision: keep only Guided, Image Panels, Blank
192
+ // Canvas and White Board Motion; the rest start again on the updated SDK). Animated Studio and the
193
+ // Dealer Type keep their releases here, so saved projects keep their pins and run and every lookup
194
+ // by id still resolves; the Type shelf, Chat's Types-first check, the marketplace, the sitemap,
195
+ // search and the catalog read DISCOVERABLE_TYPES. The eight revenue-review Types, Product Footage
196
+ // Ad Variants and the two photo reels are no longer built-in Types at all: their definitions stay
197
+ // in the repository only as acceptance fixtures (revenue-review-project-types.js and friends), a
198
+ // saved project carries its own copy of its Type, and the Blocks they used remain Blocks.
199
+ export const HIDDEN_TYPES=new Set(['sm.animated','stillmade.dealer-inventory-video']);
200
+ export const isHiddenType=type=>HIDDEN_TYPES.has((type?.manifest||type)?.id);
201
+ export const DISCOVERABLE_TYPES=Object.freeze(CURRENT_BUILTIN_TYPES.filter(type=>!isHiddenType(type)));
194
202
  export const workspaceBlock = (id,version)=>DISCOVERABLE_BLOCKS.find(block=>block.manifest.id===id && (!version || block.manifest.version===version));
195
203
  export const discoverableBuiltin = item=>!!workspaceBlock((item.manifest || item).id,(item.manifest || item).version);
@@ -7,11 +7,10 @@ import {PROPERTY_LISTING_REEL_PROJECT_TYPE,PRODUCT_PROMO_REEL_PROJECT_TYPE} from
7
7
 
8
8
  // These are editable starting configurations of the existing production Blocks,
9
9
  // not separate generators or new marketplace identities.
10
+ // October 20, 2026 (owner): the revenue-review, ad-variant and photo-reel starters are off the
11
+ // Create page; their definitions stay as acceptance fixtures, so newProjectTypeFromStarter still
12
+ // resolves them by id for those drivers and for the creator experiments.
10
13
  export const PROJECT_TYPE_STARTERS=Object.freeze([
11
- ...REVENUE_REVIEW_STARTERS,
12
- {id:'product-ad-variants',name:'Product Footage Ad Variants',description:'Make three distinct, editable opening variants from your real product demonstration and approved hooks.'},
13
- {id:'property-listing-reel',name:'Property Listing Reel',description:'Turn 3–8 original property photos into an editable vertical listing reel with your approved facts and contact details.'},
14
- {id:'product-promo-reel',name:'Product Promo Reel',description:'Turn original product photos and approved copy into an editable vertical promo, while preserving the real product.'},
15
14
  {id:'faceless-youtube',name:'Faceless YouTube',description:'Write a narrated story, plan the visuals, and assemble a complete video.',blocks:['script','voiceover','shot-plan','blueprint','pipeline','editor'],brief:'Create a faceless YouTube video. Start with a clear hook, develop the story with concrete visual beats, and end with a useful conclusion. Ask for the topic, audience, target duration and sources. Do not invent factual claims.'},
16
15
  {id:'short-form',name:'Short-form vertical',description:'Build a concise story with a strong opening and visuals for a vertical screen.',blocks:['script','voiceover','shot-plan','pipeline','editor'],brief:'Create a short-form vertical video. Ask for the subject and intended platform. Keep narration concise, place the hook immediately, and plan readable 9:16 compositions with room for captions. Confirm the target length before writing.'},
17
16
  {id:'ai-commercial',name:'AI commercial',description:'Develop a campaign idea, maintain its visual direction, and edit the final spot.',blocks:['script','voiceover','shot-plan','blueprint','pipeline','editor'],brief:'Develop an AI commercial from the supplied campaign brief. Establish the audience, promise, tone, product proof and call to action. Ask for brand assets and target duration. Use only supported product claims; preserve a consistent visual direction.'},
@@ -50,9 +49,10 @@ export function newProjectTypeFromStarter(starter,id){
50
49
  const selected=PROJECT_TYPE_STARTERS.find(item=>item.id===starter);
51
50
  const reviewed=REVENUE_REVIEW_PROJECT_TYPES.find(type=>type.id==='stillmade.'+starter);
52
51
  if(reviewed)return {...structuredClone(reviewed),id};
53
- if(selected?.id==='product-ad-variants')return {...structuredClone(PRODUCT_AD_VARIANTS_PROJECT_TYPE),id};
54
- if(selected?.id==='property-listing-reel')return {...structuredClone(PROPERTY_LISTING_REEL_PROJECT_TYPE),id};
55
- if(selected?.id==='product-promo-reel')return {...structuredClone(PRODUCT_PROMO_REEL_PROJECT_TYPE),id};
52
+ // Fixture Types (no longer on the Create page) still resolve by id for the acceptance drivers.
53
+ if(starter==='product-ad-variants')return {...structuredClone(PRODUCT_AD_VARIANTS_PROJECT_TYPE),id};
54
+ if(starter==='property-listing-reel')return {...structuredClone(PROPERTY_LISTING_REEL_PROJECT_TYPE),id};
55
+ if(starter==='product-promo-reel')return {...structuredClone(PRODUCT_PROMO_REEL_PROJECT_TYPE),id};
56
56
  if(selected?.id==='saas-launch')return {...structuredClone(PRODUCT_LAUNCH_UPDATED),id};
57
57
  if(selected?.id==='screenshot-motion')return {...structuredClone(SCREENSHOT_TO_MOTION_PROJECT_TYPE),id};
58
58
  if(selected?.id==='product-photo-ad')return {...structuredClone(PRODUCT_PHOTO_AD_PROJECT_TYPE),id};
@@ -32,7 +32,10 @@ const networkValid=entries=>!networkProblem(entries);
32
32
  export const ROOT_CHAT_ID = 'stillmade.starting-chat';
33
33
  export const TYPES = Object.freeze(['text', 'number', 'integer', 'boolean', 'json', 'object', 'image', 'video', 'audio', 'asset', 'script', 'transcript', 'brief', 'bible', 'shot-plan', 'panel-document', 'board-document', 'scene-document', 'timeline', 'timeline-edit', 'file-bundle', 'workspace-edit', 'timeline-range', 'research', 'character', 'location', 'style', 'pipeline', 'approval', 'scene', 'shot', 'mask', 'depth', 'pose', 'metadata', 'project-context', 'file', 'document', 'table', 'url', 'date', 'color']);
34
34
  export const PORTABLE_ACTIONS = Object.freeze(['final.render','stock.search','presenter.generate','quality.analyze','image.generate','image.generate.batch','media.generate','youtube.upload']);
35
- export const KINDS = ['task', 'workspace', 'editor-extension', 'composite'];
35
+ // The composite kind (a Block made of other Blocks) is off for now (October 20, 2026, owner):
36
+ // a Block is one thing with its own compact interface. No built-in uses it; a package that
37
+ // declares it fails validation with 'Unsupported block kind'.
38
+ export const KINDS = ['task', 'workspace', 'editor-extension'];
36
39
  // Generated manifest.ui controls (inputs) and output presentations; the docs list these.
37
40
  export const UI_INPUT_CONTROLS = Object.freeze(['text', 'number', 'slider', 'checkbox', 'asset', 'select', 'multiselect', 'file', 'date', 'color', 'url', 'richtext', 'list', 'grid']);
38
41
  export const UI_OUTPUT_PRESENTATIONS = Object.freeze(['gallery', 'before-after', 'table', 'document', 'chart', 'code', 'player', 'download']);
@@ -18,7 +18,9 @@ function moduleFilesOf(pkg,files){
18
18
  // isolation boundary. Only a caller that owns the code asks for it: the SDK CLI
19
19
  // on the creator's own machine (`moduleExecution:true`), never a server path
20
20
  // admitting someone else's upload (imports, AI authoring, readiness from the API).
21
- export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecution=false}={}){
21
+ // host: the stand-in host a module runs against (the CLI's, which answers from tests/mocks.json);
22
+ // without one, the recorded stand-ins.
23
+ export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecution=false,host}={}){
22
24
  const moduleFiles=moduleExecution===true&&pkg?.manifest?.runtime==='module'?moduleFilesOf(pkg,files):null;
23
25
  assert(['recipe','javascript','capability','comfyui'].includes(pkg?.manifest?.runtime)||moduleFiles,'RUNTIME_RESTRICTED',pkg?.manifest?.runtime==='module'?'Local admission runs a module Block only on your own machine (the SDK CLI); StillMade runs it in the browser sandbox after review.':'Local admission supports recipes, isolated JavaScript, declarative capabilities and ComfyUI workflows. Hosted execution still requires account review.');
24
26
  assert(Number.isFinite(timeoutMs)&&timeoutMs>0&&timeoutMs<=15000,'TIMEOUT','Local admission needs a positive budget of at most 15 seconds');
@@ -36,7 +38,7 @@ export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecuti
36
38
  // hosted calls, media and files; the digests are checked before anything runs.
37
39
  // Loaded only for a module package: the Node executor needs worker threads, which the
38
40
  // App's bundle (this file is part of it) must never pull in statically.
39
- const module=moduleFiles?await (async()=>{const [{createNodeModuleExecutor},{createMockHost}]=await Promise.all([import('./module-node.js'),import('./mock-host.js')]);return createNodeModuleExecutor({files:moduleFiles,host:createMockHost({manifest:pkg.manifest}).host});})():undefined;
41
+ const module=moduleFiles?await (async()=>{const [{createNodeModuleExecutor},{createMockHost}]=await Promise.all([import('./module-node.js'),import('./mock-host.js')]);return createNodeModuleExecutor({files:moduleFiles,host:host||createMockHost({manifest:pkg.manifest}).host});})():undefined;
40
42
  const report=await admitPackage(pkg,{
41
43
  test:content=>testPackage(content,{timeoutMs:remaining(),...(module?{module}:{})}),
42
44
  run:(content,input,options)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal,...(module?{module}:{})});},