stillmade-sdk 0.1.39 → 0.1.41
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/AGENTS.md +10 -2
- package/FRAME_VIEWS.md +41 -0
- package/INTERFACE_DESIGN.md +95 -8
- package/MOBILE_UI.md +3 -1
- package/PASTE_TO_LLM.md +14 -5
- package/SDK.md +172 -22
- package/UI_AESTHETIC.md +86 -4
- package/package.json +1 -1
- package/packages/block-cli/dev-host/assets/host.js +203 -37
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-platform/block-readiness.js +22 -2
- package/packages/block-platform/canvas.js +17 -0
- package/packages/block-platform/catalog.js +21 -6
- package/packages/block-platform/dealer-inventory-video-project-type.js +10 -5
- package/packages/block-platform/fixture-starters.js +13 -0
- package/packages/block-platform/host-release-manifests.js +95 -0
- package/packages/block-platform/import-eligibility.js +10 -1
- package/packages/block-platform/native-host-inventory.js +29 -4
- package/packages/block-platform/node-defaults.js +1 -0
- package/packages/block-platform/project-context.js +1 -1
- package/packages/block-platform/project-type-starters.js +13 -16
- package/packages/block-platform/revenue-review-project-types.js +14 -11
- package/packages/block-platform/revenue-review-starters.js +13 -0
- package/packages/block-platform/script-handoff.js +1 -1
- package/packages/block-platform/step-groups.js +3 -1
- package/packages/block-platform/type-preview.js +16 -3
- package/packages/block-platform/type-testing.js +3 -2
- package/packages/block-sdk/builder-contract.js +19 -4
- package/packages/block-sdk/contracts.js +3 -1
- package/packages/block-sdk/index.d.ts +3 -3
- package/packages/block-sdk/media-import-formats.js +2 -0
- package/packages/block-sdk/platform-checks.js +1 -1
- package/packages/block-sdk/view-design.js +92 -17
- package/packages/block-sdk/workspace-navigation.js +6 -2
- package/packages/design-system/block-kit.css +28 -6
- package/packages/design-system/block-kit.html +14 -4
- package/packages/design-system/theme.css +6 -5
|
@@ -13,6 +13,15 @@ export function projectTypeImportEligibility(type,{verifiedHostedPackages}={}){
|
|
|
13
13
|
|
|
14
14
|
// A trusted workspace cannot be exercised without a project. Its explicit
|
|
15
15
|
// deferral is allowed for import, but must never be displayed as a passed run.
|
|
16
|
+
// So is a module Step the server left to the browser sandbox: only a preview
|
|
17
|
+
// asked to defer modules (deferModules) reports 'browser-module', after the
|
|
18
|
+
// caller checked each module package statically and its files are stored.
|
|
16
19
|
export function admissionChecksPassed(results,{verifiedHostedPackages}={}){
|
|
17
|
-
return results.every(row=>row.passed===true || row.status==='disabled' || row.status==='deferred' &&
|
|
20
|
+
return results.every(row=>row.passed===true || row.status==='disabled' || row.status==='deferred' && ['trusted-workspace','browser-module'].includes(row.deferral) || verifiedHostedDeferral(row,verifiedHostedPackages));
|
|
21
|
+
}
|
|
22
|
+
// A Type whose workflow is incomplete only because the server left its module Steps to the
|
|
23
|
+
// browser sandbox: the report passed and every unexecuted row is a browser-module deferral.
|
|
24
|
+
export function onlyModuleStepsDeferred(report){
|
|
25
|
+
const unexecuted=(report?.results||[]).filter(row=>row.passed!==true&&row.status!=='disabled');
|
|
26
|
+
return report?.passed===true&&unexecuted.length>0&&unexecuted.every(row=>row.status==='deferred'&&row.deferral==='browser-module');
|
|
18
27
|
}
|
|
@@ -352,7 +352,8 @@ export const NATIVE_HOST_INVENTORY=[
|
|
|
352
352
|
"id": "sm.panel-strip",
|
|
353
353
|
"versions": [
|
|
354
354
|
"1.0.0",
|
|
355
|
-
"1.1.0"
|
|
355
|
+
"1.1.0",
|
|
356
|
+
"1.2.0"
|
|
356
357
|
],
|
|
357
358
|
"phase": "shotMap",
|
|
358
359
|
"kind": "image-panels",
|
|
@@ -381,7 +382,19 @@ export const NATIVE_HOST_INVENTORY=[
|
|
|
381
382
|
"priority": 10
|
|
382
383
|
}
|
|
383
384
|
],
|
|
384
|
-
"host": {}
|
|
385
|
+
"host": {},
|
|
386
|
+
"releaseHosts": {
|
|
387
|
+
"1.2.0": {
|
|
388
|
+
"canvasNode": "workspaceHostNode",
|
|
389
|
+
"ports": {
|
|
390
|
+
"narrationAudio": "in:narrationAudio",
|
|
391
|
+
"narrationTiming": "in:narrationTiming",
|
|
392
|
+
"narrationScript": "in:narrationScript",
|
|
393
|
+
"panels": "out:panels",
|
|
394
|
+
"images": "out:images"
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
385
398
|
},
|
|
386
399
|
{
|
|
387
400
|
"schemaVersion": 1,
|
|
@@ -609,11 +622,23 @@ export const NATIVE_HOST_INVENTORY=[
|
|
|
609
622
|
"id": "sm.voiceover",
|
|
610
623
|
"versions": [
|
|
611
624
|
"1.0.0",
|
|
612
|
-
"1.1.0"
|
|
625
|
+
"1.1.0",
|
|
626
|
+
"1.2.0"
|
|
613
627
|
],
|
|
614
628
|
"phase": "voiceover",
|
|
615
629
|
"kind": "voiceover",
|
|
616
|
-
"host": {}
|
|
630
|
+
"host": {},
|
|
631
|
+
"releaseHosts": {
|
|
632
|
+
"1.2.0": {
|
|
633
|
+
"canvasNode": "workspaceHostNode",
|
|
634
|
+
"ports": {
|
|
635
|
+
"input": "in:input",
|
|
636
|
+
"output": "out:output",
|
|
637
|
+
"timing": "out:timing",
|
|
638
|
+
"spokenScript": "out:spokenScript"
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
617
642
|
},
|
|
618
643
|
{
|
|
619
644
|
"schemaVersion": 1,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export function defaultNodeData(type, request = {}, sceneNumber = 1, {llmModel, voiceId} = {}) {
|
|
2
2
|
if (type === 'productionWorkspaceNode')return {title:request.name || 'Production workspace',prompt:'',messages:[],versions:[],status:'pending'};
|
|
3
|
+
if (type === 'workspaceHostNode')return {title:request.name || 'Workspace',workspaceOutputs:{}};
|
|
3
4
|
if (type === 'bibleNode') return { title: request.name || 'Manual Blueprint Node', description: request.description || 'Manual node.' };
|
|
4
5
|
if (type === 'textPromptNode') {
|
|
5
6
|
return {
|
|
@@ -22,7 +22,7 @@ const sceneFields=['title','name','sceneNumber','location','locationId','timeOfD
|
|
|
22
22
|
const shotFields=['shotId','sceneId','sceneNumber','shotNumber','title','name','description','shotDescription','voiceoverText','text','dialogue','hasDialogue','characterSpeaking','characterId','characterIds','charactersPresent','location','locationId','startTime','endTime','startMs','durationMs','estimatedScreenTime','shotSize','shotType','cameraSize','cameraMotion','fulfillment','vsmId','panelId','type'];
|
|
23
23
|
// URL-only histories (Panels and imported editor media) have no persisted version
|
|
24
24
|
// IDs. A content-derived identifier survives history trimming and reordering.
|
|
25
|
-
function urlVersion(url) {
|
|
25
|
+
export function urlVersion(url) {
|
|
26
26
|
let a=2166136261,b=5381;
|
|
27
27
|
for(let i=0;i<url.length;i++){a=Math.imul(a^url.charCodeAt(i),16777619);b=Math.imul(b,33)^url.charCodeAt(i);}
|
|
28
28
|
return `url-${(a>>>0).toString(16)}${(b>>>0).toString(16)}-${url.length}`;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {PRODUCT_AD_VARIANTS_PROJECT_TYPE} from './product-ad-variants-project-type.js';
|
|
1
|
+
import {REVENUE_REVIEW_STARTERS} from './revenue-review-starters.js';
|
|
3
2
|
import {CURRENT_BUILTIN_TYPES,WORKSPACE_BLOCKS} from './catalog.js';
|
|
4
|
-
import {PROPERTY_LISTING_REEL_PROJECT_TYPE,PRODUCT_PROMO_REEL_PROJECT_TYPE} from './first-revenue-project-types.js';
|
|
5
3
|
|
|
6
4
|
// The motion starters (Motion Graphics, Product Launch Video, Screenshot to Motion, Product Photo Ad) carry the
|
|
7
5
|
// Motion Designer packages, 4.2 MB of source each. They live in motion-starters.js so this module, imported at
|
|
@@ -9,6 +7,10 @@ import {PROPERTY_LISTING_REEL_PROJECT_TYPE,PRODUCT_PROMO_REEL_PROJECT_TYPE} from
|
|
|
9
7
|
// before Home showed). newProjectTypeFromStarter takes them as its third argument; loadProjectTypeStarter loads
|
|
10
8
|
// them when one is chosen; project-type-starters-full.js is the sync form for Node.
|
|
11
9
|
export const MOTION_STARTER_IDS=Object.freeze(['motion-graphics','saas-launch','screenshot-motion','product-photo-ad']);
|
|
10
|
+
// The fixture Types (fixture-starters.js: the revenue-review Types, Product Ad Variants, the photo reels) embed exact
|
|
11
|
+
// Block releases too; they resolve the same way, by id, for the acceptance drivers.
|
|
12
|
+
export const FIXTURE_STARTER_IDS=Object.freeze([...REVENUE_REVIEW_STARTERS.map(item=>item.id),'product-ad-variants','property-listing-reel','product-promo-reel']);
|
|
13
|
+
export const ON_DEMAND_STARTER_IDS=Object.freeze([...MOTION_STARTER_IDS,...FIXTURE_STARTER_IDS]);
|
|
12
14
|
|
|
13
15
|
// These are editable starting configurations of the existing production Blocks,
|
|
14
16
|
// not separate generators or new marketplace identities.
|
|
@@ -50,19 +52,14 @@ export const REVENUE_LOOP_CANDIDATES=Object.freeze([
|
|
|
50
52
|
decisionRule:'Approve the representative result and verify payment and export before acquisition. Test one offer within the agreed budget. Gift and music purchases need an explicit decision on one-project versus recurring payment.'};
|
|
51
53
|
}));
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
// `onDemand` holds the loaded definitions of motion and fixture starters by id (MOTION_STARTERS, FIXTURE_STARTERS).
|
|
56
|
+
export function newProjectTypeFromStarter(starter,id,onDemand=null){
|
|
54
57
|
const selected=PROJECT_TYPE_STARTERS.find(item=>item.id===starter);
|
|
55
|
-
if(
|
|
56
|
-
const definition=
|
|
57
|
-
if(!definition)throw Error(`The ${selected
|
|
58
|
+
if(ON_DEMAND_STARTER_IDS.includes(starter)){
|
|
59
|
+
const definition=onDemand?.[starter];
|
|
60
|
+
if(!definition)throw Error(`The ${selected?.name||starter} starter loads its Blocks first: use loadProjectTypeStarter.`);
|
|
58
61
|
return {...structuredClone(definition),id};
|
|
59
62
|
}
|
|
60
|
-
const reviewed=REVENUE_REVIEW_PROJECT_TYPES.find(type=>type.id==='stillmade.'+starter);
|
|
61
|
-
if(reviewed)return {...structuredClone(reviewed),id};
|
|
62
|
-
// Fixture Types (no longer on the Create page) still resolve by id for the acceptance drivers.
|
|
63
|
-
if(starter==='product-ad-variants')return {...structuredClone(PRODUCT_AD_VARIANTS_PROJECT_TYPE),id};
|
|
64
|
-
if(starter==='property-listing-reel')return {...structuredClone(PROPERTY_LISTING_REEL_PROJECT_TYPE),id};
|
|
65
|
-
if(starter==='product-promo-reel')return {...structuredClone(PRODUCT_PROMO_REEL_PROJECT_TYPE),id};
|
|
66
63
|
if(!selected){
|
|
67
64
|
const base=structuredClone(CURRENT_BUILTIN_TYPES.find(type=>type.legacyMode==='guided')||CURRENT_BUILTIN_TYPES[0]);
|
|
68
65
|
return {...base,id,name:'Untitled Project Type',version:'0.1.0',description:'A production workflow you can customize.'};
|
|
@@ -80,7 +77,7 @@ export function newProjectTypeFromStarter(starter,id,motion=null){
|
|
|
80
77
|
|
|
81
78
|
// Resolve any starter, loading the motion starters' packages first when one of them is chosen.
|
|
82
79
|
export async function loadProjectTypeStarter(starter,id){
|
|
83
|
-
if(
|
|
84
|
-
|
|
85
|
-
return newProjectTypeFromStarter(starter,id
|
|
80
|
+
if(MOTION_STARTER_IDS.includes(starter))return newProjectTypeFromStarter(starter,id,(await import('./motion-starters.js')).MOTION_STARTERS);
|
|
81
|
+
if(FIXTURE_STARTER_IDS.includes(starter))return newProjectTypeFromStarter(starter,id,(await import('./fixture-starters.js')).FIXTURE_STARTERS);
|
|
82
|
+
return newProjectTypeFromStarter(starter,id);
|
|
86
83
|
}
|
|
@@ -6,6 +6,7 @@ import shortForm from '../../blocks/short-form-finish/versions/1.0.3/package.js'
|
|
|
6
6
|
import occasion from '../../blocks/occasion-story/versions/1.0.2/package.js';
|
|
7
7
|
import release from '../../blocks/release-visuals/versions/1.0.2/package.js';
|
|
8
8
|
import editor from '../../blocks/native/sm.editor/versions/2.2.9/package.js';
|
|
9
|
+
import {REVENUE_REVIEW_STARTERS} from './revenue-review-starters.js';
|
|
9
10
|
|
|
10
11
|
// Production workspaces accept original media through their declared SDK ports.
|
|
11
12
|
// No special shell branches, hidden provider spending or fixture media in config.
|
|
@@ -20,14 +21,16 @@ function type(key,name,description,pkg,config,brief){return Object.freeze({
|
|
|
20
21
|
const shortBrief='Upload one original video and supply reviewed SRT subtitles with source timestamps. Choose complete source phrases, enter only approved titles and names, then review the original speech, captions, framing and finished export. Founder assistance may prepare the transcript; automatic transcription and automatic best-moment selection are not included in this Block. Never invent a testimonial or change the speaker’s meaning.';
|
|
21
22
|
const giftBrief='Use already-collected, authorized photos and video messages in story order. Enter the opening and closing dedication, exact names or memories, and optional rights-cleared music. Review all trims and listen to each message. This workflow does not collect submissions or send invitations. Do not invent a family relationship or customer story.';
|
|
22
23
|
const musicBrief='Upload one original cover image and one rights-cleared song. Set the excerpt timing and exact artist/release copy. For lyric excerpts, enter approved lyrics with timings relative to the selected excerpt. Review the full sound and timing before export. No music generation, automatic lyric alignment, or streaming-platform certification is included.';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
]
|
|
33
|
-
|
|
24
|
+
// Each Type's Block, Step config and brief, by the starter id in revenue-review-starters.js (names and descriptions live there).
|
|
25
|
+
const DEFINITIONS={
|
|
26
|
+
'captioned-business-clip':[shortForm,{purpose:'talking-head',aspect:'9:16',captionStyle:'Clean',fit:'contain'},shortBrief],
|
|
27
|
+
'customer-story-clip':[shortForm,{purpose:'testimonial',aspect:'9:16',captionStyle:'Clean',fit:'contain'},shortBrief],
|
|
28
|
+
'product-demonstration-clip':[shortForm,{purpose:'product-demo',aspect:'9:16',captionStyle:'Clean',fit:'contain'},shortBrief],
|
|
29
|
+
'birthday-story':[occasion,{occasion:'birthday',aspect:'16:9'},giftBrief],
|
|
30
|
+
'anniversary-story':[occasion,{occasion:'anniversary',aspect:'16:9'},giftBrief],
|
|
31
|
+
'farewell-story':[occasion,{occasion:'farewell',aspect:'16:9'},giftBrief],
|
|
32
|
+
'single-release-teaser':[release,{purpose:'release-teaser',aspect:'9:16',duration:20},musicBrief],
|
|
33
|
+
'lyric-excerpt':[release,{purpose:'lyric-excerpt',aspect:'9:16',duration:20},musicBrief],
|
|
34
|
+
};
|
|
35
|
+
export const REVENUE_REVIEW_PROJECT_TYPES=Object.freeze(REVENUE_REVIEW_STARTERS.map(({id,name,description})=>type(id,name,description,...DEFINITIONS[id])));
|
|
36
|
+
export {REVENUE_REVIEW_STARTERS};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The eight revenue-review Types by id, name and description: what the Create page's experiments list and the
|
|
2
|
+
// starter resolver need without the Types' embedded Block packages (revenue-review-project-types.js builds the
|
|
3
|
+
// full Types from this list and the exact releases).
|
|
4
|
+
export const REVENUE_REVIEW_STARTERS=Object.freeze([
|
|
5
|
+
{id:'captioned-business-clip',name:'Captioned Business Clip',description:'Turn a selected talking-head recording and reviewed subtitles into a captioned clip with original speech and approved branding.'},
|
|
6
|
+
{id:'customer-story-clip',name:'Customer Story Clip',description:'Edit a real, authorized customer interview into a captioned excerpt with its original words and your approved call to action.'},
|
|
7
|
+
{id:'product-demonstration-clip',name:'Product Demonstration Clip',description:'Finish a real product demonstration recording with selected source cuts, approved copy and optional supplied captions.'},
|
|
8
|
+
{id:'birthday-story',name:'Birthday Story',description:'Make a birthday video from already-collected photos, video messages, dedications and optional music.'},
|
|
9
|
+
{id:'anniversary-story',name:'Anniversary Story',description:'Arrange real memories and messages into an anniversary video with your exact dedications and optional soundtrack.'},
|
|
10
|
+
{id:'farewell-story',name:'Farewell Story',description:'Create a retirement or farewell video from an ordered collection of photos and messages, preserving each contributor’s voice.'},
|
|
11
|
+
{id:'single-release-teaser',name:'Single Release Teaser',description:'Combine original cover artwork, a song excerpt and approved release details into an editable social teaser.'},
|
|
12
|
+
{id:'lyric-excerpt',name:'Lyric Excerpt',description:'Create a short lyric visual from original artwork, an uploaded song and manually reviewed phrase timings.'},
|
|
13
|
+
].map(Object.freeze));
|
|
@@ -3,7 +3,7 @@ import {isScriptWriterHandoff} from './script-writer.js';
|
|
|
3
3
|
import {packageForStage} from './stage-package.js';
|
|
4
4
|
|
|
5
5
|
const SDK_SCRIPT_RUNTIMES = new Set(['recipe', 'javascript', 'capability']);
|
|
6
|
-
const VOICEOVER_VERSIONS = new Set(['1.0.0', '1.1.0']);
|
|
6
|
+
const VOICEOVER_VERSIONS = new Set(['1.0.0', '1.1.0', '1.2.0']);
|
|
7
7
|
|
|
8
8
|
// This identifies an explicit document handoff, not a Canvas wire or permission
|
|
9
9
|
// to replace narration. The host must separately verify the pinned source,
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// Steps in `stages`. Each part stays a real Step, so its permissions, approvals,
|
|
4
4
|
// credits, memory and runs are unchanged; groups only change what people see.
|
|
5
5
|
import {assert,plain} from '../block-sdk/contracts.js';
|
|
6
|
-
|
|
6
|
+
// The Block name limit, mirrored from builder-contract.js (the rule lives there; scripts/step-groups-name-length.test.js
|
|
7
|
+
// pins the two): importing it pulled 36 KB of builder prompt text into the studio's startup graph for one number.
|
|
8
|
+
export const BLOCK_NAME_MAX_LENGTH=40;
|
|
7
9
|
|
|
8
10
|
export const MAX_GROUP_PARTS=12;
|
|
9
11
|
const GROUP_KEYS=['id','label','stages','settings','result'];
|
|
@@ -19,8 +19,9 @@ import {projectInputSampleValues} from './project-input-samples.js';
|
|
|
19
19
|
import {resolveSemanticInputs} from './semantic-inputs.js';
|
|
20
20
|
import {packageForStage} from './stage-package.js';
|
|
21
21
|
import {briefInputNames} from './brief-ports.js';
|
|
22
|
+
import {requiresDesktopExecution} from '../block-sdk/desktop-execution.js';
|
|
22
23
|
|
|
23
|
-
export function workflowPreviewChecks(preview){return preview.results.map(result=>({name:`Connected workflow: ${result.name}`,passed:['completed','skipped'].includes(result.status),status:result.status==='completed'?'passed':result.status==='skipped'?'skipped':result.status==='disabled'?'disabled':['blocked','needs-project','needs-context','needs-hosted-run','needs-sample'].includes(result.status)?'deferred':'failed',...(result.deferral ? {deferral:result.deferral} : {}),...(result.hostedPackageDigests ? {hostedPackageDigests:[...result.hostedPackageDigests]} : {}),...(result.code ? {code:result.code} : {}),message:result.message || (result.status==='skipped' ? 'Condition skipped; input passed through' : result.status==='completed' ? 'Validated connected sample input and output' : result.status)}));}
|
|
24
|
+
export function workflowPreviewChecks(preview){return preview.results.map(result=>({name:`Connected workflow: ${result.name}`,passed:['completed','skipped'].includes(result.status),status:result.status==='completed'?'passed':result.status==='skipped'?'skipped':result.status==='disabled'?'disabled':['blocked','deferred','needs-project','needs-context','needs-hosted-run','needs-sample'].includes(result.status)?'deferred':'failed',...(result.deferral ? {deferral:result.deferral} : {}),...(result.hostedPackageDigests ? {hostedPackageDigests:[...result.hostedPackageDigests]} : {}),...(result.code ? {code:result.code} : {}),message:result.message || (result.status==='skipped' ? 'Condition skipped; input passed through' : result.status==='completed' ? 'Validated connected sample input and output' : result.status)}));}
|
|
24
25
|
|
|
25
26
|
// A brief with no text (nothing, an empty string or a document with empty text).
|
|
26
27
|
const blankBrief=value=>value===undefined||value===null||(typeof value==='string'?!value.trim():plain(value)&&typeof value.text==='string'&&!value.text.trim());
|
|
@@ -28,7 +29,12 @@ const briefLabel=(manifest,name)=>{const label=manifest.ui?.find(item=>item?.por
|
|
|
28
29
|
|
|
29
30
|
// A side-effect-free rehearsal using exact pinned packages. Hosted workspaces
|
|
30
31
|
// are marked as requiring a real project; no provider calls are simulated.
|
|
31
|
-
|
|
32
|
+
// deferModules (server review and approval, and the builder and import checks that
|
|
33
|
+
// gate them): StillMade has no server sandbox for module Blocks (runtime.js), so
|
|
34
|
+
// their Steps, and local Steps waiting only on them, are reported deferred
|
|
35
|
+
// ('browser-module') instead of failing; never as completed.
|
|
36
|
+
const MODULE_DEFERRED='Not executed on the server: module Blocks run in the browser sandbox';
|
|
37
|
+
export async function previewProjectType(type,{run=runPackageAsync,inputs={},context={},signal,onResult=()=>{},timeoutMs=15000,verifiedHostedPackages,interactive=false,verifyHostedResult,resolveInput,deferModules=false}={}) {
|
|
32
38
|
const validation=validateProjectType(type,BUILTIN_BLOCKS);assert(validation.valid,'PROJECT_TYPE',validation.errors.map(error=>error.message).join('. '));
|
|
33
39
|
const connections=projectTypeConnectionChecks(type,BUILTIN_BLOCKS);assert(connections.passed,'UNRESOLVED_REQUIREMENT',connections.results.filter(result=>!result.passed).map(result=>result.message).join(' '));
|
|
34
40
|
const eligibility=projectTypeImportEligibility(type,{verifiedHostedPackages});assert(eligibility.supported,eligibility.code,eligibility.message);
|
|
@@ -59,11 +65,18 @@ export async function previewProjectType(type,{run=runPackageAsync,inputs={},con
|
|
|
59
65
|
else if(incoming.some(edge=>!completed.get(edge.from.stage)?.outputs || !Object.hasOwn(completed.get(edge.from.stage).outputs,edge.from.port))){
|
|
60
66
|
const missing=incoming.filter(edge=>!completed.get(edge.from.stage)?.outputs || !Object.hasOwn(completed.get(edge.from.stage).outputs,edge.from.port));
|
|
61
67
|
const sources=missing.map(edge=>completed.get(edge.from.stage));
|
|
68
|
+
// Only a Step that itself runs in the browser sandbox inherits the deferral. A hosted
|
|
69
|
+
// (capability, ComfyUI) or desktop Step after a module Step stays blocked: its review
|
|
70
|
+
// and connected-run evidence are never waived because its input comes from a module.
|
|
71
|
+
if(deferModules&&['recipe','javascript','module'].includes(m.runtime)&&!requiresDesktopExecution(pkg)&&sources.every(source=>source?.deferral==='browser-module'))result={...result,status:'deferred',deferral:'browser-module',message:MODULE_DEFERRED};
|
|
72
|
+
else{
|
|
62
73
|
const hosted=sources.every(source=>source?.deferral==='trusted-workspace');
|
|
63
74
|
const reviewed=sources.every(source=>source?.deferral==='trusted-workspace'||verifiedHostedDeferral({...source,passed:false,status:'deferred'},verifiedHostedPackages));
|
|
64
75
|
const hostedPackageDigests=[...new Set(sources.flatMap(source=>source.hostedPackageDigests||[]))];
|
|
65
76
|
result=reviewed&&missingContext?{...result,status:'needs-context',message:'Provide the requested project context to rehearse this step'}:{...result,status:'blocked',...(hosted?{deferral:'trusted-workspace'}:reviewed&&hostedPackageDigests.length?{deferral:'reviewed-capability-dependency',hostedPackageDigests}:{}),message:hosted?'Not executed: its upstream StillMade workspace must run in a project first':reviewed?'Not executed: waiting for an explicitly confirmed connected hosted result. Package reviews do not supply workflow outputs':'Not executed: waiting for an upstream result'};
|
|
77
|
+
}
|
|
66
78
|
}
|
|
79
|
+
else if(deferModules&&m.runtime==='module')result={...result,status:'deferred',deferral:'browser-module',message:MODULE_DEFERRED};
|
|
67
80
|
else if(missingContext)result={...result,status:'needs-context',message:'Provide the requested project context to rehearse this step'};
|
|
68
81
|
else try {
|
|
69
82
|
// Fixture values seed unconnected sample inputs. Live edge values always win;
|
|
@@ -118,7 +131,7 @@ export async function previewProjectType(type,{run=runPackageAsync,inputs={},con
|
|
|
118
131
|
}catch(error){if(controller.signal.aborted || signal?.aborted || Date.now()>=deadline)throw error;result={...result,status:'failed',code:error.code || 'EXECUTION',message:error.message};}
|
|
119
132
|
completed.set(step.id,result);results.push(result);await onResult(structuredClone(result));check();
|
|
120
133
|
}
|
|
121
|
-
const failed=results.some(result=>result.status==='failed'),incomplete=results.some(result=>['blocked','needs-project','needs-context','needs-hosted-run','needs-sample'].includes(result.status));
|
|
134
|
+
const failed=results.some(result=>result.status==='failed'),incomplete=results.some(result=>['blocked','deferred','needs-project','needs-context','needs-hosted-run','needs-sample'].includes(result.status));
|
|
122
135
|
return {digest:await digest(pinned),passed:!failed,complete:!failed && !incomplete,status:failed?'failed':incomplete?'deferred':'completed',results};
|
|
123
136
|
}finally{media.dispose();clearTimeout(timer);signal?.removeEventListener('abort',cancel);controller.abort();}
|
|
124
137
|
}
|
|
@@ -6,7 +6,8 @@ import { testPackage } from '../block-sdk/testing.js';
|
|
|
6
6
|
import {previewProjectType,workflowPreviewChecks} from './type-preview.js';
|
|
7
7
|
import {projectTypeImportEligibility,admissionChecksPassed} from './import-eligibility.js';
|
|
8
8
|
import {verifiedHostedPackageReview} from './hosted-type-reviews.js';
|
|
9
|
-
|
|
9
|
+
// deferModules: the server's checks, where module Blocks cannot run, and the browser checks that gate them (type-preview.js).
|
|
10
|
+
export async function testProjectType(type, test = testPackage, {run,signal,verifiedHostedPackages,inputs,context,deferModules=false} = {}) {
|
|
10
11
|
type=structuredClone(type);
|
|
11
12
|
const deadline = Date.now() + 15000;
|
|
12
13
|
const validation = validateProjectType(type, BUILTIN_BLOCKS);
|
|
@@ -35,7 +36,7 @@ export async function testProjectType(type, test = testPackage, {run,signal,veri
|
|
|
35
36
|
}
|
|
36
37
|
let workflowComplete=false;
|
|
37
38
|
if(results.every(result=>result.passed))try{
|
|
38
|
-
const preview=await previewProjectType(type,{...(run ? {run} : {}),signal,verifiedHostedPackages,inputs,context,timeoutMs:Math.max(0,deadline-Date.now())});
|
|
39
|
+
const preview=await previewProjectType(type,{...(run ? {run} : {}),signal,verifiedHostedPackages,inputs,context,deferModules,timeoutMs:Math.max(0,deadline-Date.now())});
|
|
39
40
|
results.push(...workflowPreviewChecks(preview));workflowComplete=preview.complete;
|
|
40
41
|
}catch(error){results.push({name:'Connected workflow preview',passed:false,code:error.code || 'EXECUTION',message:error.message});}
|
|
41
42
|
const passed=admissionChecksPassed(results,{verifiedHostedPackages});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import {HOSTED_OPERATION_NAMES} from './contracts.js';
|
|
10
10
|
|
|
11
11
|
export const BUILDER_CONTRACT_ID='stillmade.block-builder';
|
|
12
|
-
export const BUILDER_CONTRACT_VERSION='1.
|
|
12
|
+
export const BUILDER_CONTRACT_VERSION='1.16.0';
|
|
13
13
|
export const BUILDER_CONTRACT_STAMP=`${BUILDER_CONTRACT_ID}@${BUILDER_CONTRACT_VERSION}`;
|
|
14
14
|
export const BUILDER_CONTRACT_DOCS_PATH='/docs/build/block-builder-contract';
|
|
15
15
|
// Block names appear in step bars, cards and pickers; new releases keep them short.
|
|
@@ -68,9 +68,24 @@ const SECTIONS=Object.freeze([
|
|
|
68
68
|
rule('naming','block',
|
|
69
69
|
`Name the Block in ${BLOCK_NAME_MAX_LENGTH} characters or fewer, in plain words.`,
|
|
70
70
|
`Give the Block a short, plain name of ${BLOCK_NAME_MAX_LENGTH} characters or fewer that says what it does ("Captioned Clip", not "Captioned Business Clip Generator With Review"). The name appears in step bars, Project Type cards and pickers, so put the detail in the description instead. New releases with a longer name do not pass readiness.`),
|
|
71
|
+
// November 2, 2026 (owner): a sermon Block's video input read "Video"; it should read "Drop your sermon video here".
|
|
72
|
+
rule('input-words','block',
|
|
73
|
+
'Name each input in the job\'s words (`sermonVideo`, not `video`): StillMade words its drop area from the name.',
|
|
74
|
+
'Name each input for what the person brings, in the job\'s words: `sermonVideo` for a sermon clip Block, `listingPhotos` for a listing, `lessonSlides` for a lesson podcast. StillMade titles the input from that name ("Sermon video") and words its drop area from it ("Drop your sermon video here", the formats, "Choose a video"), in StillMade and the SDK preview, so never draw your own upload box for an input. Keep a bare type name (`video`, `file`) only when any file of that kind does the job. The `description` says what makes a good one.'),
|
|
75
|
+
// November 2, 2026 (owner): every screen speaks the job, as a product made only for it would, and stays quiet. The same
|
|
76
|
+
// day (owner, of four tiles each saying "Only on the device it was added from"): "if it's a notification make it a
|
|
77
|
+
// notification": state is one StillMade notification or nothing, never a sentence on the page or one per item.
|
|
78
|
+
rule('job-words','block',
|
|
79
|
+
'Speak the job unless the person or creator asks otherwise: `manifest.action` names the button ("Plan the tour"; AI pictures, video or sound start with Generate, never "Make it"), one solid `sm-primary`, progress in the job\'s units, one line per problem, no notes on how the Block works or its state (one StillMade notification at most, never one per item), `date` inputs for days and times, plain input names so "Use a reference" fills them, posting as its own connector Step. Clear every Wording note `gate` lists.',
|
|
80
|
+
'These are StillMade\'s defaults: unless the person or the creator asked for something else, every screen speaks the job in its own words and adds nothing else. `manifest.action` names the Step\'s button, {label:"Plan the tour", running:"Planning the tour…"}, and StillMade adds the price ("Plan the tour · 1 credit"). When the Block makes a picture, video or sound with AI the label starts with Generate and says what ("Generate tour", "Generate voiceover"); one that cuts or renders the person\'s own media says Make ("Make 6 clips"); never "Make it", "Run", "Submit", "Process" or a bare "Generate". The main button stands out: one solid `sm-primary` in the kit\'s ink, never restyled to the background; everything else is `sm-quiet` or an `sm-icon`, and close is an X icon. Report progress in the job\'s units with `stillmade.progress(fraction, label)` ("Moving through the kitchen, 4 of 9 rooms"), never "Processing". Head the result in the job\'s words ("9 rooms", "Your tour · 44 s"), never "Output". Say a problem in one line with its fix beside it ("No bathroom photo. Guests look for one." and Add photo). Stay quiet: no welcome text, tips under fields, tours or banners, and at most one line of guidance on a screen, only where the next move is not obvious; anything more goes in the input\'s `description`, which StillMade shows behind an (i). Say nothing about how the Block works (where it runs, saving, syncing, recovery, accounts, credits) or where a file is kept: StillMade tells the person what they need as one short notification, or nothing; never a sentence on the page, and never the same line under each item. Every drop area offers "Use a reference", which fills the Step from the person\'s saved photos, videos and notes: media inputs fill by kind, so take `video` or `asset` where a clip works as well as a photo, and text inputs fill by plain names and labels (`productName`, `website`, `brandColors`, `fonts`, `voice`, and `details` or `notes` for the reference\'s note); never build your own reference picker. Days and times are picked, never typed: use the `date` type, which StillMade shows as a calendar, with a list of times when the input is about a time (`postAt`, `startsAt`); a custom view uses `<input type="date">` or `type="datetime-local"`. Posting is a connector Step of its own, and StillMade words its button from the app and action ("Post to Instagram"). Before handing the Block back, clear every Wording note that `gate` and readiness list, unless the person asked for those words.'),
|
|
81
|
+
// November 2, 2026 (owner): StillMade centers and zooms a screen; an app that lays itself out (a CAD tool) opts out with
|
|
82
|
+
// data-stillmade-layout="own" and keeps its own layout and Ctrl/Cmd +/- keys. The same day (owner: "implement A absolutely
|
|
83
|
+
// universally, like in app, platform, sdk, just everywhere"): every <video controls> gets StillMade's player and result
|
|
84
|
+
// pictures its full-screen button; a video without controls stays the Block's own. Also that day (owner: "there is no
|
|
85
|
+
// separation and I cannot tell what is going on"): a pick is the kit's ink fill, and media that cannot show is a quiet tile.
|
|
71
86
|
rule('ui','block',
|
|
72
|
-
'Prefer `manifest.ui`; custom views use the Block kit (`sm-*`) unless a look is named. Pictures over words: preview each option before choosing; few words, fold the rest; buttons carry `data-stillmade-action`; the start button names the action (never "Run") and shows no price (StillMade adds it). A job fits one screen unscrolled and a 320px phone; an app (editor, configurator, CAD tool) works like that app: full canvas, panels, tools, undo, never a form.',
|
|
73
|
-
'If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, picks as pictures in `sm-choices`, one `sm-primary` naming the action (
|
|
87
|
+
'Prefer `manifest.ui`; custom views use the Block kit (`sm-*`) unless a look is named. Pictures over words: preview each option before choosing; few words, fold the rest; buttons carry `data-stillmade-action`; the start button names the action (never "Run" or "Make it") and shows no price (StillMade adds it). A job fits one screen unscrolled and a 320px phone; an app (editor, configurator, CAD tool) works like that app: full canvas, panels, tools, undo, never a form. An app puts `data-stillmade-layout="own"` on its root: StillMade never centers or zooms it. `<video controls>` gets StillMade\'s player; leave out `controls` when the Block drives playback. A pick keeps the kit\'s ink fill; media that cannot show is a wordless `sm-media-off` tile.',
|
|
88
|
+
'If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, picks as pictures in `sm-choices`, the picked one in the kit\'s ink fill (never restyled to an outline, a tint or the page\'s background) so it reads at a glance, one solid `sm-primary` naming the action in the job\'s words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade\'s run button adds it), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons, (i) on unclear controls. These compact rules are for a job or an ingredient. Size the view to the Block\'s tier (block-tier.js): an app, a workspace a person works in for a while (video editor, 3D configurator, CAD or drawing tool), looks and works like that kind of app instead of a compact page: its canvas or stage fills the view, with the panels, toolbars, inspector or timeline such an app has, direct manipulation (select, drag, zoom), keyboard shortcuts and undo; it keeps the host appearance and Block kit controls, its panels may scroll, and on a phone its tools fold into sheets while the canvas stays usable. Never shrink an app into a form of fields and one button. StillMade centers a standard view narrower than its Step and zooms a view to fill its space; an app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. StillMade draws one player for every `<video controls>` (play, timeline, time, sound and full screen, shown while the pointer is on the video, with keys and touch) and a full-screen button on result pictures in `sm-stage` or `sm-gallery` and on any element marked `data-stillmade-expand` (the same value makes one set of pictures, `off` removes it); `data-stillmade-player="native"` keeps the browser\'s controls, and a `<video>` without `controls` is the Block\'s own and is never touched. A picture or clip that cannot show here (kept on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape with the `image-off` glyph and the reason in its label, never words in its place. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Mark each button `data-stillmade-action="what it does"` and label each field: Chat presses and fills them. Expose the capability, not the implementation: sensible defaults, previews, progress, clear errors, visible outputs. Show no raw JSON, internal IDs, filesystem details, provider terms or developer settings unless the Block is a developer tool. When a person chooses among options they need to hear or see (voices, music, fonts, looks, templates, images), let them preview each option before choosing: a sample or thumbnail from the Block\'s own files or the host\'s free samples, never a paid generation. Skip previews only when there is nothing to preview. Choices StillMade owns (voices, models in the run review) already show samples. Follow the host appearance (Light, Dark and White) and one elastic layout: stacked on a 320px phone with no sideways scroll (result first, action pinned), and from 860px the controls left (≤380px), the result filling the right. Short settings sit side by side, each label beside its control (`sm-field`, `sm-settings`; three or four short options as one `data-segmented` row); many items are one line each (`sm-list`) that open in place; input files are small tiles (`sm-gallery data-size="small"`); on a phone chip rows scroll sideways and long lists show three. Compacting moves controls, never deletes them.'),
|
|
74
89
|
rule('multiplayer','block',
|
|
75
90
|
'Custom views share edits: `StillMade.onShared`, publish every editable field (`data-stillmade-share`, `bindShared`, `updateShared`), respect `canEdit`, never re-run on remote updates.',
|
|
76
91
|
'Projects are multiplayer. A custom view subscribes through `StillMade.onShared(callback)` and publishes every editable field: give static controls a unique `id` and `data-stillmade-share="field"` (the host owns their synchronization), use `StillMade.bindShared(field, elementId)` for controls created dynamically, and `StillMade.updateShared(values, before, options)` for structured values; private variables and DOM values are not shared. Mark a truly device-only control (playback, hover, device selection) with `data-stillmade-local`. Observe shared outputs as well as state, honor `canEdit` in every mutating callback (read-only participants cannot write), never run, click or call a paid integration in response to a remote update, and preserve conflicting drafts. Shared background work uses `sharedWork` with the `work.shared` permission.'),
|
|
@@ -143,7 +158,7 @@ export const BUILDER_CONTRACT=Object.freeze({
|
|
|
143
158
|
gapClasses:GAP_CLASSES,
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
const SECTION_TITLES=Object.freeze({'runtime-choice':'Choose the runtime','grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-tier':'Three sizes of Block','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
|
|
161
|
+
const SECTION_TITLES=Object.freeze({'runtime-choice':'Choose the runtime','grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-tier':'Three sizes of Block','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','input-words':'Name inputs in the job\'s words','job-words':'Speak the job','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
|
|
147
162
|
const inScope=(scope,wanted)=>wanted==='all'||scope==='both'||scope===wanted;
|
|
148
163
|
|
|
149
164
|
/** Render the contract for a model prompt ('compact'), a guide ('full') or an SDK folder ('agents-md'). */
|
|
@@ -128,7 +128,7 @@ export function validateManifest(manifest, { builtIn = false } = {}) {
|
|
|
128
128
|
const check = (ok, path, message) => { if (!ok) errors.push({ path, message }); };
|
|
129
129
|
try { safeDocument(manifest); } catch (e) { return { valid: false, errors: [{ path: '$', message: e.message }] }; }
|
|
130
130
|
if (!plain(manifest)) return { valid: false, errors: [{ path: '$', message: 'Manifest must be an object' }] };
|
|
131
|
-
const allowed = ['schemaVersion', 'id', 'name', 'version', 'sdkVersion', 'description', 'kind', 'inputs', 'outputs', 'permissions', 'runtime', 'jobs', 'ui', 'uiLayout', 'entry', 'license', 'provenance', 'summary', 'category', 'platforms', 'state', 'storage', 'semantics', 'desktopTools', 'operations', 'sharedWork', 'triggers', 'features'];
|
|
131
|
+
const allowed = ['schemaVersion', 'id', 'name', 'version', 'sdkVersion', 'description', 'kind', 'inputs', 'outputs', 'permissions', 'runtime', 'jobs', 'ui', 'uiLayout', 'action', 'entry', 'license', 'provenance', 'summary', 'category', 'platforms', 'state', 'storage', 'semantics', 'desktopTools', 'operations', 'sharedWork', 'triggers', 'features'];
|
|
132
132
|
for (const key of Object.keys(manifest)) check(allowed.includes(key), key, 'Unknown manifest field');
|
|
133
133
|
for (const message of featureMapErrors(manifest)) check(false, 'features', message);
|
|
134
134
|
if(manifest.sharedWork!==undefined){try{validateSharedWorkContract(manifest.sharedWork);}catch(error){check(false,'sharedWork',error.message);}check(manifest.permissions?.project?.includes('work.shared'),'sharedWork','Declare the work.shared permission');}
|
|
@@ -158,6 +158,8 @@ export function validateManifest(manifest, { builtIn = false } = {}) {
|
|
|
158
158
|
check(typeof manifest.name === 'string' && manifest.name.trim().length > 0 && manifest.name.length <= 120, 'name', 'Name must be 1–120 characters');
|
|
159
159
|
check(typeof manifest.description === 'string' && manifest.description.trim().length > 0 && manifest.description.length <= 1000, 'description', 'Description must be 1–1000 characters');
|
|
160
160
|
check(KINDS.includes(manifest.kind), 'kind', 'Unsupported block kind');
|
|
161
|
+
// The Step's button in the job's words ("Plan the tour"), November 2, 2026; StillMade adds the price.
|
|
162
|
+
if(manifest.action!==undefined){const action=manifest.action,text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max&&!/[\r\n]/.test(value);check(plain(action)&&Object.keys(action).every(key=>['label','running'].includes(key))&&text(action.label,40)&&(action.running===undefined||text(action.running,48)),'action','Name the button as {label:"Plan the tour", running:"Planning the tour…"}: a label up to 40 characters, and optional running words up to 48');}
|
|
161
163
|
check(['recipe', 'javascript', 'module', 'comfyui', 'capability', ...(builtIn ? ['host'] : [])].includes(manifest.runtime), 'runtime', 'Installable runtimes are recipe, isolated JavaScript, sandboxed JavaScript modules with WebAssembly, the reviewed ComfyUI adapter, and approved host capabilities');
|
|
162
164
|
if(manifest.state!==undefined){
|
|
163
165
|
const state=manifest.state;
|
|
@@ -14,7 +14,7 @@ export interface BlockViewBridge { connectDocument(options:import('./document-st
|
|
|
14
14
|
export interface SdkCompatibility { status:'supported'|'unsupported'|'invalid'; declared:unknown; runtimeVersion:string; message:string; }
|
|
15
15
|
export function sdkCompatibility(declared:unknown,runtimeVersion?:string):SdkCompatibility;
|
|
16
16
|
export type Port = { type: string; imageMode?: 'pixels'|'reference'; role?: string; context?: keyof typeof CONTEXT_FIELDS; primary?: boolean; required?: boolean; default?: unknown; min?: number; max?: number; description?: string } & Partial<import('./semantic-contracts.js').SemanticContract>;
|
|
17
|
-
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record<string, Port>; outputs: Record<string, Port>; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record<string,unknown>;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; desktopTools?:DesktopToolDeclaration[]; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
|
|
17
|
+
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; action?: { label: string; running?: string }; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record<string, Port>; outputs: Record<string, Port>; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record<string,unknown>;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; desktopTools?:DesktopToolDeclaration[]; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
|
|
18
18
|
export interface BlockView { html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'; }
|
|
19
19
|
/** A frame view: any interface code (frameworks, SVG, canvas, drag) with assets named by SHA-256. */
|
|
20
20
|
export interface FrameView { runtime: 'frame'; html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'; files?: {schemaVersion: 1; files: Record<string, {sha256: string; bytes: number; mediaType: string}>}; }
|
|
@@ -193,7 +193,7 @@ export function validateComfyBackend(workflow: ComfyWorkflow, manifest: Manifest
|
|
|
193
193
|
export function extractComfyOutputs(workflow: ComfyWorkflow, manifest: Manifest, history: unknown, options: {promptId: string}): Record<string, {filename: string; subfolder: ''; type: 'temp'}>;
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
export interface PlatformCapture { frames?: { interface?: 'host'|'guest'; width?: number; height?: number; touch?: boolean; controls: number; issues: { code: string; message: string; severity?: 'warning'|'error' }[] }[]; interactionPassed?: boolean|null; embedded?:boolean; continuityPassed?:boolean; states?:Record<'empty'|'populated'|'loading'|'error'|'success'|'long-labels'|'enlarged-text',boolean>; error?: string; unavailable?: string; }
|
|
196
|
+
export interface PlatformCapture { frames?: { interface?: 'host'|'guest'; width?: number; height?: number; touch?: boolean; controls: number; issues: { code: string; message: string; severity?: 'warning'|'error' }[] }[]; interactionPassed?: boolean|null; embedded?:boolean; continuityPassed?:boolean; continuityChanges?:string[]; states?:Record<'empty'|'populated'|'loading'|'error'|'success'|'long-labels'|'enlarged-text',boolean>; error?: string; unavailable?: string; }
|
|
197
197
|
export interface PlatformProbeOptions { content: BlockPackage; platform: 'phone'|'browser'|'desktop'; viewport: {width:number;height:number;touch:boolean;label?:'compact'|'phone'|'tablet'|'phone-landscape'|'desktop'}; theme:'light'|'dark'|'white'; }
|
|
198
198
|
export type PlatformProbe = (options: PlatformProbeOptions) => PlatformCapture|Promise<PlatformCapture>;
|
|
199
199
|
export interface PlatformTargetCheck { supported:boolean|null; reason:string; checks:{viewport:{width:number;height:number;touch:boolean};theme:string;passed:boolean|null;message:string;findings?:{code:string;message:string}[]}[]; }
|
|
@@ -414,7 +414,7 @@ export * from './native-host-providers.js';
|
|
|
414
414
|
|
|
415
415
|
export {blockRemixPolicy,assertRemixReady,REMIX_AUTHORING_REQUIREMENT} from './remix-policy.js';
|
|
416
416
|
export interface SdkCompatibility {status:'supported'|'unsupported'|'invalid';declared:unknown;runtimeVersion:string;message:string}
|
|
417
|
-
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record<string, Port>; outputs: Record<string, Port>; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record<string,unknown>;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
|
|
417
|
+
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; action?: { label: string; running?: string }; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record<string, Port>; outputs: Record<string, Port>; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record<string,unknown>;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
|
|
418
418
|
export interface BlockPackage { portable?: PortableMetadata; manifest: Manifest; view?: BlockView|FrameView; code?: string; module?: ModuleDescriptor; comfyui?: ComfyWorkflow; capability?: ConnectedCapability|TextGenerationCapability|SpeechCapability|TranscriptionCapability|ImageGenerationCapability|VideoGenerationCapability|ImageDescriptionCapability|TimelineProposalCapability; recipe?: { schemaVersion: 1; steps: { id: string; op: string; args: Record<string, unknown> }[]; outputs: Record<string, unknown> }; tests: { name: string; input: Record<string, unknown>; expected?: Record<string, unknown>; state?: BlockState; expectedState?: BlockState; expectations?: Record<string, ConnectedResultExpectation|ComfyImageExpectation|CapabilityTextExpectation|CapabilityAudioExpectation|CapabilityTranscriptExpectation|CapabilityImageExpectation|CapabilityVideoExpectation|CapabilityTimelineExpectation> }[]; }
|
|
419
419
|
export type CapabilityInvocation = TextGenerationInvocation|SpeechInvocation|TranscriptionInvocation|ImageGenerationInvocation|VideoGenerationInvocation|ImageDescriptionInvocation|TimelineProposalInvocation;
|
|
420
420
|
export interface CapabilityResult { outputs: Record<string, unknown>; metadata?: Record<string, unknown>; mediaReceipts?: Record<string, SpeechMediaReceipt|GeneratedAudioMediaReceipt|GeneratedImageMediaReceipt|GeneratedVideoMediaReceipt>; }
|
|
@@ -16,6 +16,8 @@ const MEDIA_KINDS = Object.freeze(['image','video','audio']);
|
|
|
16
16
|
const LABELS = Object.freeze({png:'PNG',jpg:'JPEG',webp:'WebP',gif:'GIF',mp4:'MP4',mov:'MOV',webm:'WebM',mp3:'MP3',wav:'WAV',m4a:'M4A',aac:'AAC',ogg:'OGG',pdf:'PDF',txt:'TXT',csv:'CSV',md:'Markdown',json:'JSON',zip:'ZIP'});
|
|
17
17
|
const names = kinds => Object.entries(IMPORT_FORMATS).filter(([extension,item])=>kinds.includes(item.kind)&&LABELS[extension]).map(([extension])=>LABELS[extension]);
|
|
18
18
|
const list = items => items.length>1?`${items.slice(0,-1).join(', ')}, or ${items.at(-1)}`:items[0];
|
|
19
|
+
/** The formats an upload of these kinds takes, as one phrase ("MP4, MOV, or WebM"). */
|
|
20
|
+
export const importFormatList = kinds => list([...new Set(names(kinds))]);
|
|
19
21
|
/**
|
|
20
22
|
* Check a chosen file's name, type and size. `kinds` lists what this upload
|
|
21
23
|
* accepts: media by default, or ['file'] / ['image','video','audio','file'].
|
|
@@ -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
|
|
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.`};
|