stillmade-sdk 0.1.38 → 0.1.40

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 (33) hide show
  1. package/AGENTS.md +5 -1
  2. package/MODULES.md +29 -11
  3. package/PASTE_TO_LLM.md +13 -8
  4. package/SDK.md +22 -13
  5. package/UI_AESTHETIC.md +6 -0
  6. package/package.json +1 -1
  7. package/packages/block-cli/cli.js +28 -17
  8. package/packages/block-cli/dev-host/assets/host.js +17 -17
  9. package/packages/block-cli/dev-host/assets/index.css +1 -1
  10. package/packages/block-cli/dev-server.js +14 -2
  11. package/packages/block-cli/ready-gate.js +3 -2
  12. package/packages/block-platform/dealer-inventory-video-project-type.js +10 -5
  13. package/packages/block-platform/fixture-starters.js +13 -0
  14. package/packages/block-platform/import-eligibility.js +10 -1
  15. package/packages/block-platform/motion-starters.js +14 -0
  16. package/packages/block-platform/project-type-starters.js +26 -16
  17. package/packages/block-platform/revenue-review-project-types.js +14 -11
  18. package/packages/block-platform/revenue-review-starters.js +13 -0
  19. package/packages/block-platform/type-preview.js +16 -3
  20. package/packages/block-platform/type-testing.js +3 -2
  21. package/packages/block-sdk/builder-contract.js +6 -2
  22. package/packages/block-sdk/capabilities.js +1 -1
  23. package/packages/block-sdk/example-media-data.js +2 -1
  24. package/packages/block-sdk/example-media.js +1 -0
  25. package/packages/block-sdk/index.d.ts +1 -1
  26. package/packages/block-sdk/media-import-formats.js +2 -0
  27. package/packages/block-sdk/media-transform.js +17 -4
  28. package/packages/block-sdk/mock-host.js +24 -5
  29. package/packages/block-sdk/module-hosted.js +12 -4
  30. package/packages/block-sdk/platform-checks.js +1 -1
  31. package/packages/block-sdk/semantic-relationships.js +6 -0
  32. package/packages/block-sdk/testing.js +5 -3
  33. package/packages/block-sdk/view-design.js +1 -1
@@ -4,7 +4,7 @@
4
4
  // executed and checked exactly like one. The module Block must list the
5
5
  // operation in `permissions.capabilities`.
6
6
  import {assert,plain,stableStringify} from './contracts.js';
7
- import {defaultCapabilityExpectations} from './capabilities.js';
7
+ import {defaultCapabilityExpectations,MEDIA_ANALYZE_LIMITS} from './capabilities.js';
8
8
  import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS} from './generation-catalog.js';
9
9
 
10
10
  const text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max;
@@ -47,8 +47,11 @@ const SHAPES={
47
47
  return {inputs:{prompt:port('text','Sound effect prompt')},outputs:{result:port('audio','Generated sound effect')},capability:{prompt:{$input:'prompt'},output:'result',...(request.durationSec?{settings:{durationSec:request.durationSec}}:{})},input:{prompt:request.prompt}};
48
48
  },
49
49
  'audio.transcribe':request=>{
50
- if(!media(request.audio,'audio'))fail('audio.transcribe needs a saved audio reference');
51
- return {inputs:{audio:port('audio','Audio to transcribe'),language:{type:'text',description:'Language code or auto',required:false,default:'auto'}},outputs:{result:port('transcript','Timed transcript')},capability:{audio:{$input:'audio'},language:{$input:'language'},output:'result'},input:{audio:request.audio,language:typeof request.language==='string'?request.language:'auto'}};
50
+ // A video's sound is transcribed as a capability Block's is (November 1, 2026: a sermon clip Block had to extract the
51
+ // audio first because only audio was accepted here).
52
+ const kind=request.audio?.kind==='video'?'video':'audio';
53
+ if(!media(request.audio,kind))fail('audio.transcribe needs a saved audio or video reference');
54
+ return {inputs:{audio:port(kind,kind==='video'?'Video whose sound to transcribe':'Audio to transcribe'),language:{type:'text',description:'Language code or auto',required:false,default:'auto'}},outputs:{result:port('transcript','Timed transcript')},capability:{audio:{$input:'audio'},language:{$input:'language'},output:'result'},input:{audio:request.audio,language:typeof request.language==='string'?request.language:'auto'}};
52
55
  },
53
56
  'image.describe':request=>{
54
57
  const images=Array.isArray(request.images)?request.images:[request.image];
@@ -63,8 +66,12 @@ const SHAPES={
63
66
  if(!text(request.question,2000))fail('media.analyze needs a question');
64
67
  // October 30, 2026: detail:'check' is the short photo check, priced per call of up to four photos.
65
68
  if(request.detail!==undefined&&(request.detail!=='check'||kind!=='image'))fail('media.analyze detail:"check" checks one to four saved images, never a video');
69
+ // range: one part of a video looked at closely, as a capability Block binds it (November 1, 2026: a sermon clip Block
70
+ // could only look at the whole sermon at once).
71
+ const range=request.range;
72
+ if(range!==undefined&&!(kind==='video'&&plain(range)&&Object.keys(range).sort().join(',')==='end,start'&&Number.isFinite(range.start)&&Number.isFinite(range.end)&&range.start>=0&&range.end>range.start&&range.end-range.start<=MEDIA_ANALYZE_LIMITS.range+1e-6))fail(`media.analyze range is {start, end} in seconds of one video, at most ${MEDIA_ANALYZE_LIMITS.range} seconds apart`);
66
73
  const many=Array.isArray(request.media);
67
- return {inputs:{media:port(kind==='video'?'video':many?'image[]':'image','Media to analyze')},outputs:{result:port('json','Visual evidence')},capability:{media:{$input:'media'},question:request.question,...(request.detail?{detail:request.detail}:{}),output:'result'},input:{media:many?list:list[0]}};
74
+ return {inputs:{media:port(kind==='video'?'video':many?'image[]':'image','Media to analyze'),...(range?{start:port('number','Start of the range, in seconds'),end:port('number','End of the range, in seconds')}:{})},outputs:{result:port('json','Visual evidence')},capability:{media:{$input:'media'},question:request.question,...(range?{range:{start:{$input:'start'},end:{$input:'end'}}}:{}),...(request.detail?{detail:request.detail}:{}),output:'result'},input:{media:many?list:list[0],...(range?{start:range.start,end:range.end}:{})}};
68
75
  },
69
76
  'web.fetch':request=>{
70
77
  if(!text(request.url,2048)||!text(request.instruction,4000))fail('web.fetch needs a url and an instruction');
@@ -105,6 +112,7 @@ export function isModuleHostedPackage(content){
105
112
  const input=content.tests?.[0]?.input||{},request={};
106
113
  // The request: the capability's own fields, each input reference replaced by the call's value.
107
114
  for(const [key,value] of Object.entries(content.capability))if(!['schemaVersion','operation','output'].includes(key))request[key]=plain(value)&&typeof value.$input==='string'?input[value.$input]:value;
115
+ if(plain(request.range))request.range={start:input[request.range.start?.$input],end:input[request.range.end?.$input]};
108
116
  if(Number.isFinite(request.settings?.durationSec))request.durationSec=request.settings.durationSec;
109
117
  if(Array.isArray(request.image)){request.images=request.image;delete request.image;}
110
118
  const source={id:manifest.id.slice(0,-'.hosted'.length),version:manifest.version,name:manifest.name.slice(0,-suffix.length),license:manifest.license,...(manifest.provenance?{provenance:manifest.provenance}:{}),permissions:{capabilities:[operation]}};
@@ -88,7 +88,7 @@ export function assessPlatformCapture(capture) {
88
88
  if(!capture.frames?.length || !capture.frames.some(frame=>frame.controls>0) || capture.frames.some(frame=>frame.interface==='guest') && !capture.frames.some(frame=>frame.interface==='guest' && frame.controls>0))return {passed:false,message:'The preview did not expose usable controls.'};
89
89
  if(capture.interactionPassed!==true)return {passed:null,message:'Configure an input, perform the main action, and inspect the output with touch-compatible controls.'};
90
90
  if(capture.embedded!==true)return {passed:false,message:'The UI was not checked inside the real Block or Project Type host.'};
91
- if(capture.continuityPassed===false)return {passed:false,message:'Editing state or completed output changed after resizing, rotating, or toggling a panel.'};
91
+ if(capture.continuityPassed===false)return {passed:false,message:`Editing state or completed output changed after resizing, rotating, or toggling a panel.${capture.continuityChanges?.length?` Changed: ${capture.continuityChanges.join(', ')}.`:''}`};
92
92
  if(capture.continuityPassed!==true)return {passed:null,message:'Resize and panel continuity still needs an interaction check.'};
93
93
  const states=capture.states||{};
94
94
  for(const state of ['empty','populated','loading','error','success','long-labels','enlarged-text'])if(states[state]!==true)return {passed:null,message:`The ${state} UI state still needs deterministic mobile coverage.`};
@@ -15,6 +15,12 @@ export const STANDARD_SEMANTIC_RELATIONSHIPS=Object.freeze([
15
15
  Object.freeze({name:'legacy.timeline.updatededitor',parents:Object.freeze(['timeline'])}),
16
16
  // A narration is audio that can be transcribed.
17
17
  Object.freeze({name:'narration_audio',parents:Object.freeze(['source_audio'])}),
18
+ // Generated, edited and rendered videos are videos to work from, as generated and processed images are images
19
+ // (November 1, 2026: a new Block taking any video a Step hands on had no meaning that accepts them all).
20
+ Object.freeze({name:'generated_video',parents:Object.freeze(['source_video'])}),
21
+ Object.freeze({name:'edited_video',parents:Object.freeze(['source_video'])}),
22
+ Object.freeze({name:'rendered_video',parents:Object.freeze(['source_video'])}),
23
+ Object.freeze({name:'rendered_audio',parents:Object.freeze(['source_audio'])}),
18
24
  ]);
19
25
 
20
26
  const parents=new Map(STANDARD_SEMANTIC_RELATIONSHIPS.map(entry=>[entry.name,entry.parents]));
@@ -36,9 +36,11 @@ function fixtureExpectedMessage(outputs,fixture,error){
36
36
  // (assetId local-written-file-N, versionId, url, bytes) that no model can predict. A module fixture compares it by
37
37
  // shape: an expected {kind, mimeType, name}, with those fields left out or given, matches when kind, mimeType and name
38
38
  // match. Every other value is compared exactly.
39
- const WRITTEN_KINDS=['file','image','video','audio'],HOST_MADE=['assetId','versionId','url','bytes'];
39
+ // A written file a Block passes on in an asset output carries kind 'asset' and matches the same way (November 1, 2026:
40
+ // a sermon clips Block's JSON in its asset[] output failed every test on the stand-in assetId).
41
+ const WRITTEN_KINDS=['file','image','video','audio'],SHAPE_KINDS=[...WRITTEN_KINDS,'asset'],HOST_MADE=['assetId','versionId','url','bytes'];
40
42
  const standInWritten=value=>plain(value)&&/^local-written-(?:file|image|video|audio)-\d+$/.test(String(value.assetId));
41
- const writtenShape=value=>plain(value)&&WRITTEN_KINDS.includes(value.kind)&&typeof value.mimeType==='string'&&Object.keys(value).every(key=>['kind','mimeType','name',...HOST_MADE].includes(key));
43
+ const writtenShape=value=>plain(value)&&SHAPE_KINDS.includes(value.kind)&&typeof value.mimeType==='string'&&Object.keys(value).every(key=>['kind','mimeType','name',...HOST_MADE].includes(key));
42
44
  // expected, with each reference that matches a stand-in-written output by shape replaced by that output.
43
45
  function matchWritten(actual,expected){
44
46
  if(standInWritten(actual))return writtenShape(expected)&&['kind','mimeType','name'].every(key=>expected[key]===actual[key])?actual:expected;
@@ -50,7 +52,7 @@ function matchWritten(actual,expected){
50
52
  // for the host-made fields it leaves out.
51
53
  function withStandInHostFields(outputs,expected){
52
54
  if(!plain(outputs)||!plain(expected))return expected;
53
- const fill=(value,type)=>type.endsWith('[]')?(Array.isArray(value)?value.map(item=>fill(item,type.slice(0,-2))):value):WRITTEN_KINDS.includes(type)&&value?.kind===type&&writtenShape(value)?{assetId:`local-written-${type}-1`,versionId:'v1',...value}:value;
55
+ const fill=(value,type)=>type.endsWith('[]')?(Array.isArray(value)?value.map(item=>fill(item,type.slice(0,-2))):value):writtenShape(value)&&(WRITTEN_KINDS.includes(type)&&value.kind===type||type==='asset'&&value.kind==='asset')?{assetId:`local-written-${WRITTEN_KINDS.includes(type)?type:'file'}-1`,versionId:'v1',...value}:value;
54
56
  return Object.fromEntries(Object.entries(expected).map(([name,value])=>[name,Object.hasOwn(outputs,name)?fill(value,String(outputs[name]?.type)):value]));
55
57
  }
56
58
  export function validatePackage(pkg) {
@@ -27,7 +27,7 @@ export const BLOCK_KIT_CLASSES=Object.freeze({
27
27
  // The design principles for every Block interface, in priority order.
28
28
  export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
29
29
  'Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit.',
30
- 'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
30
+ 'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
31
31
  'One obvious next action (`sm-primary`); peers are quiet buttons or named icons. Seven or fewer choices in view.',
32
32
  'The action button names what it does ("Write", "Generate image", "Make video"), never "Run", "Run step", "Submit" or "Review cost". Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher.',
33
33
  'Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji.',