stillmade-sdk 0.1.3 → 0.1.5
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/.claude/settings.json +15 -0
- package/.cursor/hooks.json +11 -0
- package/.gitignore +2 -0
- package/.stillmade/sdk-baseline.json +65 -0
- package/AGENTS.md +29 -7
- package/AGENT_CONTROL.md +2 -1
- package/BROWSER_APPLICATIONS.md +128 -3
- package/GITHUB_APPLICATIONS.md +184 -1
- package/NATIVE_APPLICATION_ADAPTATION.md +565 -0
- package/NATIVE_MCP.md +3 -2
- package/PASTE_TO_LLM.md +29 -10
- package/PORTABLE.md +5 -4
- package/REMIX.md +16 -10
- package/SDK.md +511 -46
- package/UI_AESTHETIC.md +37 -6
- package/api-index.json +90 -0
- package/blocks/connected-app/versions/{1.2.0 → 1.5.0}/task-selection.js +5 -3
- package/blocks/connected-app/versions/{1.4.0 → 1.5.0}/view.js +4 -2
- package/blocks/native/sm.ai-decide/versions/1.0.0/package.js +149 -0
- package/blocks/native/sm.ai-write/versions/1.0.0/package.js +147 -0
- package/blocks/native/sm.connect-app/versions/1.0.0/package.js +55 -0
- package/blocks/native/sm.contact-fields/versions/1.0.0/package.js +197 -0
- package/blocks/native/sm.continue-if/versions/1.0.0/package.js +168 -0
- package/blocks/native/sm.generate-image/versions/2.1.0/package.js +101 -0
- package/blocks/native/sm.meta-creative/versions/1.0.0/package.js +206 -0
- package/blocks/native/sm.new-items/versions/1.0.0/package.js +191 -0
- package/blocks/native/sm.voiceover/versions/2.1.5/package.js +179 -0
- package/blocks/native/sm.voiceover/versions/2.1.6/package.js +179 -0
- package/blocks/native/sm.when/versions/1.0.0/package.js +129 -0
- package/blocks/native/sm.write-summary/versions/1.0.0/package.js +133 -0
- package/blocks/occasion-story/package.js +1 -1
- package/blocks/occasion-story/versions/1.0.2/package.js +1682 -0
- package/blocks/photo-reel-composer/versions/1.0.1/package.js +3026 -0
- package/blocks/release-visuals/package.js +1 -1
- package/blocks/release-visuals/versions/1.0.2/package.js +1500 -0
- package/blocks/short-form-finish/package.js +1 -1
- package/blocks/short-form-finish/versions/1.0.3/package.js +1026 -0
- package/examples/original-photo-editor.stillmade-block +0 -0
- package/examples/portable-regexp.stillmade-block +0 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +16 -1
- package/packages/block-cli/format.js +2 -0
- package/packages/block-cli/ready-gate.js +164 -0
- package/packages/block-platform/block-readiness.js +3 -2
- package/packages/block-platform/canvas.js +1 -1
- package/packages/block-platform/catalog.js +6 -4
- package/packages/block-platform/chat-requirements.js +8 -3
- package/packages/block-platform/collaboration.js +5 -5
- package/packages/block-platform/first-revenue-project-types.js +2 -2
- package/packages/block-platform/flow-inspection.js +2 -2
- package/packages/block-platform/native-application-validation-context.js +20 -0
- package/packages/block-platform/plain-descriptions.js +67 -0
- package/packages/block-platform/product-ad-variants-project-type.js +6 -3
- package/packages/block-platform/project-context.js +4 -4
- package/packages/block-platform/project-type-composer.js +21 -12
- package/packages/block-platform/project-type-draft.js +2 -2
- package/packages/block-platform/project-type-recommendations.js +1 -1
- package/packages/block-platform/project-types.js +9 -4
- package/packages/block-platform/revenue-review-project-types.js +6 -5
- package/packages/block-platform/segment-runner.js +35 -11
- package/packages/block-platform/step-groups.js +78 -0
- package/packages/block-platform/steps.js +12 -8
- package/packages/block-platform/type-workspace.js +4 -4
- package/packages/block-platform/workflow-loop.js +4 -0
- package/packages/block-platform/workspace-input-readset.js +18 -4
- package/packages/block-sdk/admission.js +5 -17
- package/packages/block-sdk/agent-inputs.js +2 -0
- package/packages/block-sdk/agent-inspection.js +4 -2
- package/packages/block-sdk/browser-application-artifacts.js +1 -1
- package/packages/block-sdk/browser-application-execution.d.ts +2 -2
- package/packages/block-sdk/browser-application-execution.js +30 -12
- package/packages/block-sdk/browser-application-guest.d.ts +2 -0
- package/packages/block-sdk/browser-application-guest.js +16 -9
- package/packages/block-sdk/browser-application.d.ts +12 -4
- package/packages/block-sdk/browser-application.js +27 -13
- package/packages/block-sdk/builder-contract.js +25 -17
- package/packages/block-sdk/capabilities.js +38 -19
- package/packages/block-sdk/connected-app.js +26 -6
- package/packages/block-sdk/connection-adapters.js +19 -2
- package/packages/block-sdk/contracts-core.js +8 -0
- package/packages/block-sdk/contracts.js +9 -5
- package/packages/block-sdk/document-drafts.js +7 -1
- package/packages/block-sdk/document-store-binding.js +2 -1
- package/packages/block-sdk/file-bundle.js +4 -1
- package/packages/block-sdk/first-party-packages.js +18 -2
- package/packages/block-sdk/generation-catalog.js +3 -2
- package/packages/block-sdk/index.d.ts +20 -1
- package/packages/block-sdk/index.js +1 -0
- package/packages/block-sdk/native-application-package.d.ts +22 -0
- package/packages/block-sdk/native-application-package.js +77 -0
- package/packages/block-sdk/native_adapter.py +76 -0
- package/packages/block-sdk/package.json +1 -0
- package/packages/block-sdk/pipeline-runtime-host.d.ts +2 -0
- package/packages/block-sdk/project-flow-contract.js +2 -0
- package/packages/block-sdk/remix-policy.js +3 -1
- package/packages/block-sdk/resolver-admission.js +17 -0
- package/packages/block-sdk/runtime.js +4 -1
- package/packages/block-sdk/schema-fields.js +34 -2
- package/packages/block-sdk/semantic-relationships.js +14 -0
- package/packages/block-sdk/shared-controls.js +3 -1
- package/packages/block-sdk/shared-view.js +6 -1
- package/packages/block-sdk/source-rights.js +6 -5
- package/packages/block-sdk/testing.js +3 -0
- package/packages/design-system/theme.css +12 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {plainBlockDescription} from './plain-descriptions.js';
|
|
1
2
|
import {assert,plain,safeDocument,ROOT_CHAT_ID,stableStringify,valueMatches} from '../block-sdk/contracts.js';
|
|
2
3
|
import {ADAPTERS,validateConnectionAdapter} from '../block-sdk/connection-adapters.js';
|
|
3
4
|
import {STANDARD_SEMANTIC_RELATIONSHIPS} from '../block-sdk/semantic-contracts.js';
|
|
@@ -13,6 +14,7 @@ import {validateCondition} from './conditions.js';
|
|
|
13
14
|
import {projectTypeEventCandidates} from './project-type-events.js';
|
|
14
15
|
import {projectTypeRuleSequence} from './project-type-rules.js';
|
|
15
16
|
import {packageForStage} from './stage-package.js';
|
|
17
|
+
import {normalizeGroups,joinGroupAt,groupOf} from './step-groups.js';
|
|
16
18
|
|
|
17
19
|
const manifestOf=pkg=>pkg?.manifest || pkg;
|
|
18
20
|
const identity=manifest=>`${manifest.id}@${manifest.version}`;
|
|
@@ -286,7 +288,7 @@ export function auditProjectTypeConnections(type,catalog=[],{contextValues=[],re
|
|
|
286
288
|
catch(error){
|
|
287
289
|
if(!['SEMANTIC','SCHEMA','LIMIT','CYCLE','CONNECTION'].includes(error.code))throw error;
|
|
288
290
|
const reason=String(error.message),message=error.code==='CYCLE'?'These Blocks depend on each other. Change one source to break the loop.':error.code==='CONNECTION'?reason:'These Blocks disagree about the information they use. Choose compatible versions or fix their definitions.';
|
|
289
|
-
const failed=(type.stages || []).map(stage=>{const manifest=manifestFor(type,stage,available),paused=stage.enabled===false;return {id:stage.id,label:stage.label || manifest?.name || stage.blockId,description:manifest
|
|
291
|
+
const failed=(type.stages || []).map(stage=>{const manifest=manifestFor(type,stage,available),paused=stage.enabled===false;return {id:stage.id,label:stage.label || manifest?.name || stage.blockId,description:plainBlockDescription(manifest),state:paused?'ready':'broken',...(paused?{paused:true}:{contractIssue:{message,reason}}),requirements:[]};});
|
|
290
292
|
const failures=failed.filter(stage=>!stage.paused).map(stage=>({stageId:stage.id,state:'broken',code:error.code,message:`${stage.label}: ${message}`,reason}));
|
|
291
293
|
return {stages:failed,issues:failures,connections:[],valid:false,ready:false,counts:{blocks:failures.length,requirements:0,missing:failures.length,questions:0},requirementsInspected:false};
|
|
292
294
|
}
|
|
@@ -302,14 +304,15 @@ export function auditProjectTypeConnections(type,catalog=[],{contextValues=[],re
|
|
|
302
304
|
}
|
|
303
305
|
for(const stage of ordered) {
|
|
304
306
|
const manifest=manifestFor(type,stage,available);
|
|
305
|
-
const row={id:stage.id,label:stage.label || manifest?.name || stage.blockId,description:manifest
|
|
307
|
+
const row={id:stage.id,label:stage.label || manifest?.name || stage.blockId,description:plainBlockDescription(manifest),state:'ready',requirements:[]};
|
|
306
308
|
row.connectionIssues=issues.filter(issue=>issue.connectionId && issue.stageId===stage.id);
|
|
307
309
|
stages.push(row);
|
|
308
310
|
if(stage.enabled===false){row.paused=true;if(row.connectionIssues.length)row.state='broken';continue;}
|
|
309
311
|
if(!manifest){row.state='broken';issues.push({stageId:stage.id,message:`${row.label} is unavailable. Choose a Block.`});continue;}
|
|
310
312
|
const stageValues=[...values,...(eventCandidates.get(stage.id) || [])];
|
|
311
313
|
for(const [key,port] of Object.entries(manifest.inputs || {})) {
|
|
312
|
-
|
|
314
|
+
// An input a native host never reads (the original Editor's) cannot be asked for.
|
|
315
|
+
const requirement=normalized(key,executable(stage,manifest,key)?port:{...port,required:false}),permissions=manifest.permissions?.project || [];
|
|
313
316
|
const requirementResolver=resolver || createUniversalResolver({semantics,adapters:builtinSemanticAdapters(requirement,{permissions,semantics}),requestFromUser:pendingAnswer});
|
|
314
317
|
const edge=declared.find(item=>item.to?.stage===stage.id && item.to.port===key);
|
|
315
318
|
let decision;
|
|
@@ -519,7 +522,7 @@ export function chooseProjectTypeConverterRoute(type,{stageId,key,sourceStageId,
|
|
|
519
522
|
return applyConverterPath(type,stageId,key,candidateId,{stageId:sourceStageId,port:sourceOutputKey},path,catalog);
|
|
520
523
|
}
|
|
521
524
|
|
|
522
|
-
export function chooseProjectTypeSource(type,stageId,key,candidateId,catalog=[],{semantics,contextValues=[]}={}) {
|
|
525
|
+
export function chooseProjectTypeSource(type,stageId,key,candidateId,catalog=[],{semantics,contextValues=[],experimentalBrowser,experimentalNative}={}) {
|
|
523
526
|
assert(typeof candidateId==='string' && candidateId.length<=2000,'CONNECTION','Choose one current source.');
|
|
524
527
|
semantics ||= semanticRegistryForManifests((type.stages || []).filter(stage=>stage.enabled!==false).map(stage=>manifestFor(type,stage,[...catalog,...(type.packages || [])])));
|
|
525
528
|
const converterSearch=candidateId.startsWith('["block-conversion')?{stageId,key}:undefined;
|
|
@@ -538,7 +541,7 @@ export function chooseProjectTypeSource(type,stageId,key,candidateId,catalog=[],
|
|
|
538
541
|
const match=validateConnectionAdapter(producer.outputs[source.port],consumer.inputs[key],{permissions:consumer.permissions?.project || [],semantics,...(candidate.adapter?{adapter:{id:candidate.adapter.id,version:Number(candidate.adapter.version)}}:{})});
|
|
539
542
|
const removed=(type.connections||[]).filter(edge=>edge.to.stage===stageId&&edge.to.port===key);
|
|
540
543
|
const next=detachUnusedConverterBranches(type,{...type,stages:type.stages.map(stage=>stage.id===stageId?withoutInputSetting(stage,key):stage),connections:[...(type.connections || []).filter(edge=>edge.to.stage!==stageId || edge.to.port!==key),{from:{stage:source.stageId,port:source.port},to:{stage:stageId,port:key},...(match.adapter?{adapter:match.adapter}:{})}]},removed);
|
|
541
|
-
const validation=validateProjectTypeDraft(next,catalog);
|
|
544
|
+
const validation=validateProjectTypeDraft(next,catalog,{experimentalBrowser,experimentalNative});
|
|
542
545
|
assert(validation.valid,'PROJECT_TYPE',validation.errors.map(error=>error.message).join(' '));
|
|
543
546
|
return next;
|
|
544
547
|
}
|
|
@@ -613,17 +616,17 @@ export function setProjectTypeLoop(type,loopId,loop,catalog=[]) {
|
|
|
613
616
|
return next;
|
|
614
617
|
}
|
|
615
618
|
|
|
616
|
-
export function removeProjectTypeConnection(type,connectionId,catalog=[]) {
|
|
619
|
+
export function removeProjectTypeConnection(type,connectionId,catalog=[],options={}) {
|
|
617
620
|
assert(typeof connectionId==='string' && connectionId.length<=2000,'CONNECTION','Choose an inspected connection.');
|
|
618
621
|
const removed=(type.connections||[]).filter(edge=>stableStringify(edge)===connectionId);
|
|
619
622
|
const connections=(type.connections || []).filter(edge=>stableStringify(edge)!==connectionId);
|
|
620
623
|
assert(connections.length<(type.connections || []).length,'CONNECTION','This connection changed. Inspect the current flow again.');
|
|
621
|
-
const next=detachUnusedConverterBranches(type,{...type,connections},removed),validation=validateProjectTypeDraft(next,catalog);
|
|
624
|
+
const next=detachUnusedConverterBranches(type,{...type,connections},removed),validation=validateProjectTypeDraft(next,catalog,options);
|
|
622
625
|
assert(validation.valid,'PROJECT_TYPE',validation.errors.map(error=>error.message).join(' '));
|
|
623
626
|
return next;
|
|
624
627
|
}
|
|
625
628
|
|
|
626
|
-
export function editProjectTypeDraft(type,action,catalog=[]) {
|
|
629
|
+
export function editProjectTypeDraft(type,action,catalog=[],options={}) {
|
|
627
630
|
const available=[...catalog,...(type.packages || [])];
|
|
628
631
|
let next={...type,stages:[...type.stages],connections:[...(type.connections || [])]};
|
|
629
632
|
const index=type.stages.findIndex(stage=>stage.id===action.stageId);
|
|
@@ -666,6 +669,7 @@ export function editProjectTypeDraft(type,action,catalog=[]) {
|
|
|
666
669
|
} else {
|
|
667
670
|
assert(Number.isInteger(action.index) && action.index>=0 && action.index<=next.stages.length,'STEP','Choose a place in the flow.');
|
|
668
671
|
next.stages.splice(action.index,0,replacement);
|
|
672
|
+
next=joinGroupAt(next,replacement.id,action.index);
|
|
669
673
|
}
|
|
670
674
|
} else if(action.kind==='set-enabled') {
|
|
671
675
|
assert(typeof action.enabled==='boolean','STEP','Choose whether this Block is enabled.');
|
|
@@ -676,6 +680,7 @@ export function editProjectTypeDraft(type,action,catalog=[]) {
|
|
|
676
680
|
assert(manifest,'MISSING_BLOCK','The pinned Block package is missing.');
|
|
677
681
|
const copy={...structuredClone(stage),id:action.id || `step-${crypto.randomUUID().slice(0,12)}`,label:`${stage.label || manifest.name} copy`.slice(0,120)};
|
|
678
682
|
next.stages.splice(index+1,0,copy);
|
|
683
|
+
next=joinGroupAt(next,copy.id,index+1);
|
|
679
684
|
next.connections.push(...(type.connections || []).filter(edge=>edge.to.stage===stage.id).map(edge=>({...structuredClone(edge),to:{...edge.to,stage:copy.id}})));
|
|
680
685
|
} else if(action.kind==='remove') {
|
|
681
686
|
next.stages.splice(index,1);
|
|
@@ -683,12 +688,16 @@ export function editProjectTypeDraft(type,action,catalog=[]) {
|
|
|
683
688
|
if(next.rules)next.rules=next.rules.filter(rule=>rule.stepId!==stage.id && rule.endStepId!==stage.id);
|
|
684
689
|
if(next.loops)next.loops=next.loops.filter(loop=>loop.startStepId!==stage.id && loop.endStepId!==stage.id && loop.definition.until?.stage!==stage.id && !loop.definition.feedback.some(edge=>edge.from.stage===stage.id || edge.to.stage===stage.id));
|
|
685
690
|
} else if(action.kind==='move') {
|
|
686
|
-
|
|
687
|
-
|
|
691
|
+
// action.job moves every part of the Step group the Block belongs to.
|
|
692
|
+
const parts=action.job&&groupOf(type,stage.id)?.stages||[stage.id];
|
|
693
|
+
const moving=next.stages.filter(item=>parts.includes(item.id)),rest=next.stages.filter(item=>!parts.includes(item.id));
|
|
694
|
+
assert(Number.isInteger(action.index) && action.index>=0 && action.index<=rest.length,'STEP','Choose a place in the flow.');
|
|
695
|
+
rest.splice(action.index,0,...moving);next.stages=rest;
|
|
688
696
|
} else assert(false,'STEP','Choose a supported edit.');
|
|
697
|
+
next=normalizeGroups(next);
|
|
689
698
|
const validationCatalog=catalog.filter(item=>!next.packages?.some(pkg=>identity(pkg.manifest)===identity(manifestOf(item))));
|
|
690
|
-
next=connectProjectType(next,validationCatalog);
|
|
691
|
-
const validation=validateProjectTypeDraft(next,validationCatalog);
|
|
699
|
+
next=normalizeGroups(connectProjectType(next,validationCatalog,options));
|
|
700
|
+
const validation=validateProjectTypeDraft(next,validationCatalog,options);
|
|
692
701
|
assert(validation.valid,'PROJECT_TYPE',validation.errors.map(error=>error.message).join(' '));
|
|
693
702
|
return next;
|
|
694
703
|
}
|
|
@@ -4,11 +4,11 @@ import {validateProjectType} from './project-types.js';
|
|
|
4
4
|
import {validateProjectTypeRules} from './project-type-rules.js';
|
|
5
5
|
import {validateProjectTypeLoops} from './project-type-loops.js';
|
|
6
6
|
|
|
7
|
-
export function validateProjectTypeDraft(type,catalog=[]) {
|
|
7
|
+
export function validateProjectTypeDraft(type,catalog=[],{experimentalBrowser,experimentalNative}={}) {
|
|
8
8
|
try {
|
|
9
9
|
safeDocument(type);assert(plain(type),'PROJECT_TYPE','Expected a Project Type draft.');
|
|
10
10
|
const structuralType={...type,connections:[]};delete structuralType.rules;delete structuralType.loops;
|
|
11
|
-
const structure=validateProjectType(structuralType,catalog,{allowEmpty:true,semantics:createSemanticRegistry()});
|
|
11
|
+
const structure=validateProjectType(structuralType,catalog,{allowEmpty:true,semantics:createSemanticRegistry(),experimentalBrowser,experimentalNative});
|
|
12
12
|
if(!structure.valid)return structure;
|
|
13
13
|
assert(type.connections===undefined || Array.isArray(type.connections) && type.connections.length<=500,'CONNECTION','Expected at most 500 connections.');
|
|
14
14
|
for(const edge of type.connections || []) {
|
|
@@ -324,5 +324,5 @@ export function recommendProjectTypeCombinations({goal,requirements,handoffRequi
|
|
|
324
324
|
});
|
|
325
325
|
};
|
|
326
326
|
const missingHandoffProducers=handoffs.filter(handoff=>!available.some(candidate=>resultSlotsForManifest(candidate.manifest).some(slot=>!slot.viaAction&&semanticCompatibility(slot.contract,handoff.from,semantics).compatible))).map(handoff=>({...handoff.from,key:`${handoff.key}_producer`,required:true,description:`Block output needed for ${handoff.description}`}));
|
|
327
|
-
return {goal,requirements:wanted,sourceMaterials:supplied,resultPins,handoffRequirements:handoffs,executionDevices,combinations:results.map(result=>({...result,transformationPlan:transformationPlan(result),blocks:result.keys.map((key,index)=>{const candidate=catalog.get(key);return {key,identity:listingIdentity({kind:'block',id:candidate.release?.entityId||(candidate.origin==='built-in'?candidate.manifest.id:null),version:candidate.manifest.version,digest:candidate.release?.digest||candidate.sourceDigest}),id:candidate.manifest.id,version:candidate.manifest.version,name:candidate.manifest.name,description:candidate.manifest.description,reasons:explainBlock(candidate,index,result),origin:candidate.origin || 'catalog',...(candidate.release?{release:candidate.release}:{}),...(candidate.sourceDigest?{sourceDigest:candidate.sourceDigest}:{})};})})),missingCapabilities:[...wanted.filter(requirement=>!available.some(candidate=>covers(candidate.manifest,requirement))),...missingHandoffProducers],search:{evaluations,truncated:evaluations>=maxEvaluations,maxSteps},executionVerified:false};
|
|
327
|
+
return {goal,requirements:wanted,sourceMaterials:supplied,resultPins,handoffRequirements:handoffs,executionDevices,combinations:results.map(result=>({...result,transformationPlan:transformationPlan(result),blocks:result.keys.map((key,index)=>{const candidate=catalog.get(key);return {key,identity:listingIdentity({kind:'block',id:candidate.release?.entityId||(candidate.origin==='built-in'?candidate.manifest.id:null),version:candidate.manifest.version,digest:candidate.release?.digest||candidate.sourceDigest}),id:candidate.manifest.id,version:candidate.manifest.version,name:candidate.manifest.name,description:candidate.manifest.description,reasons:explainBlock(candidate,index,result),origin:candidate.origin || 'catalog',...(candidate.manifest.permissions?.capabilities?.includes('connection.execute')?{connected:true}:{}),...(candidate.release?{release:candidate.release}:{}),...(candidate.sourceDigest?{sourceDigest:candidate.sourceDigest}:{})};})})),missingCapabilities:[...wanted.filter(requirement=>!available.some(candidate=>covers(candidate.manifest,requirement))),...missingHandoffProducers],search:{evaluations,truncated:evaluations>=maxEvaluations,maxSteps},executionVerified:false};
|
|
328
328
|
}
|
|
@@ -13,7 +13,9 @@ import { validatePackage } from '../block-sdk/testing.js';
|
|
|
13
13
|
import {semanticRegistryForManifests} from '../block-sdk/package-semantics.js';
|
|
14
14
|
import {isBrowserApplication,experimentalBrowserSource} from './experimental-browser-context.js';
|
|
15
15
|
import {packageForStage} from './stage-package.js';
|
|
16
|
-
|
|
16
|
+
import {validateStepGroups} from './step-groups.js';
|
|
17
|
+
import {isNativeApplicationPackage,nativeApplicationValidationSource} from './native-application-validation-context.js';
|
|
18
|
+
export function validateProjectType(type, catalog, {allowEmpty=false,semantics,experimentalBrowser,experimentalNative}={}) {
|
|
17
19
|
const errors = [];
|
|
18
20
|
try {
|
|
19
21
|
safeDocument(type);
|
|
@@ -24,7 +26,7 @@ export function validateProjectType(type, catalog, {allowEmpty=false,semantics,e
|
|
|
24
26
|
validateProjectOutcome(type.outcome);
|
|
25
27
|
assert(type.acceptanceCriteria===undefined||Array.isArray(type.acceptanceCriteria)&&type.acceptanceCriteria.length<=24&&new Set(type.acceptanceCriteria.map(item=>item.key)).size===type.acceptanceCriteria.length&&type.acceptanceCriteria.every(item=>plain(item)&&Object.keys(item).length===3&&typeof item.key==='string'&&/^[a-z][a-z0-9_]{0,63}$/.test(item.key)&&typeof item.description==='string'&&item.description.trim().length>=3&&item.description.length<=240&&['artifact-inspection','workflow-receipt','human-review'].includes(item.verification)),'PROJECT_TYPE','Result checks must be distinct, bounded and unverified until a real result is inspected.');
|
|
26
28
|
assert(plain(type) && type.schemaVersion === 1 && ID.test(type.id || '') && VERSION.test(type.version || ''), 'PROJECT_TYPE', 'Invalid project type identity');
|
|
27
|
-
assert(Object.keys(type).every(k => ['schemaVersion', 'id', 'version', 'name', 'description', 'stages', 'connections', 'legacyMode', 'summary', 'packages', 'provenance','license','defaults','onboarding','outcome','rules','loops','projectInputs','startingChat','acceptanceCriteria','goalSourceMaterials','goalSourceInputs'].includes(k)), 'ROOT_PROTECTED', 'Project type contains unsupported settings');
|
|
29
|
+
assert(Object.keys(type).every(k => ['schemaVersion', 'id', 'version', 'name', 'description', 'stages', 'connections', 'legacyMode', 'summary', 'packages', 'provenance','license','defaults','onboarding','outcome','rules','loops','projectInputs','startingChat','acceptanceCriteria','goalSourceMaterials','goalSourceInputs','groups'].includes(k)), 'ROOT_PROTECTED', 'Project type contains unsupported settings');
|
|
28
30
|
assert(type.startingChat === undefined || type.startingChat === 'native', 'ROOT_PROTECTED', 'Starting Chat is provided by StillMade.');
|
|
29
31
|
allowEmpty = allowEmpty || type.startingChat === 'native';
|
|
30
32
|
assert(type.license === undefined || typeof type.license === 'string' && type.license.length > 0 && type.license.length <= 200,'LICENSE','Use a bounded license identifier');
|
|
@@ -32,7 +34,8 @@ export function validateProjectType(type, catalog, {allowEmpty=false,semantics,e
|
|
|
32
34
|
const available = [...catalog], identities = new Set(catalog.map(b => `${(b.manifest || b).id}@${(b.manifest || b).version}`));
|
|
33
35
|
const embedded = new Set();
|
|
34
36
|
for (const pkg of type.packages || []) {
|
|
35
|
-
if(
|
|
37
|
+
if(isNativeApplicationPackage(pkg))nativeApplicationValidationSource(experimentalNative,pkg);
|
|
38
|
+
else if(isBrowserApplication(pkg))experimentalBrowserSource(experimentalBrowser,pkg);
|
|
36
39
|
else validatePackage(pkg);
|
|
37
40
|
const key = `${pkg.manifest.id}@${pkg.manifest.version}`;
|
|
38
41
|
const known = catalog.find(block => `${(block.manifest || block).id}@${(block.manifest || block).version}` === key);
|
|
@@ -54,7 +57,8 @@ export function validateProjectType(type, catalog, {allowEmpty=false,semantics,e
|
|
|
54
57
|
assert(block, 'MISSING_BLOCK', `Missing or ambiguous ${stage.blockId}@${stage.version}`);
|
|
55
58
|
const manifest=block.manifest || block;
|
|
56
59
|
if(['browser-application','native-application'].includes(manifest.runtime)){
|
|
57
|
-
|
|
60
|
+
if(isNativeApplicationPackage(block))nativeApplicationValidationSource(experimentalNative,block);
|
|
61
|
+
else experimentalBrowserSource(experimentalBrowser,block);
|
|
58
62
|
assert(!stage.failurePolicy&&!stage.condition,'BROWSER_APPLICATION_APPROVAL','Experimental application Steps require a direct operation without failure automation or bypass.');
|
|
59
63
|
assert(!type.loops?.length&&!type.rules?.length,'BROWSER_APPLICATION_APPROVAL','Experimental application workflows cannot declare automatic rules or loops.');
|
|
60
64
|
}
|
|
@@ -66,6 +70,7 @@ export function validateProjectType(type, catalog, {allowEmpty=false,semantics,e
|
|
|
66
70
|
stages.set(stage.id, manifest);
|
|
67
71
|
}
|
|
68
72
|
assert(allowEmpty&&type.stages.length===0||type.stages.some(stage=>stage.enabled !== false),'STEP','Keep at least one enabled step');
|
|
73
|
+
validateStepGroups(type,stages);
|
|
69
74
|
validateProjectTypeRules(type,available);
|
|
70
75
|
validateProjectTypeLoops(type);
|
|
71
76
|
semantics ||= semanticRegistryForManifests([...stages.values()]);
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// Published Type releases embed exact Block releases. Never import a Block's
|
|
2
2
|
// floating current package here: a newer Block release would silently rewrite
|
|
3
|
-
// these immutable Type versions. Adopt a newer Block in a new Type version
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
// these immutable Type versions. Adopt a newer Block in a new Type version
|
|
4
|
+
// (1.0.3 adopted the releases that confirm media rights once at sign-up).
|
|
5
|
+
import shortForm from '../../blocks/short-form-finish/versions/1.0.3/package.js';
|
|
6
|
+
import occasion from '../../blocks/occasion-story/versions/1.0.2/package.js';
|
|
7
|
+
import release from '../../blocks/release-visuals/versions/1.0.2/package.js';
|
|
7
8
|
import editor from '../../blocks/native/sm.editor/versions/2.2.9/package.js';
|
|
8
9
|
|
|
9
10
|
// Production workspaces accept original media through their declared SDK ports.
|
|
10
11
|
// No special shell branches, hidden provider spending or fixture media in config.
|
|
11
12
|
function type(key,name,description,pkg,config,brief){return Object.freeze({
|
|
12
|
-
schemaVersion:1,id:'stillmade.'+key,version:'1.0.
|
|
13
|
+
schemaVersion:1,id:'stillmade.'+key,version:'1.0.3',name,description,license:'MIT',startingChat:'native',
|
|
13
14
|
summary:{text:description,origin:'human',generation:'authored',status:'ready'},
|
|
14
15
|
defaults:{aspectRatio:config.aspect,brief},onboarding:[],packages:[pkg,editor].map(p=>structuredClone(p)),
|
|
15
16
|
stages:[{id:'make',blockId:pkg.manifest.id,version:pkg.manifest.version,label:name,config},{id:'finish',blockId:editor.manifest.id,version:editor.manifest.version,label:'Editor',config:{commit:true,renderFps:30,renderResolution:1080,renderQuality:'high'}}],
|
|
@@ -23,14 +23,23 @@ import {pinCurrentSemanticSource,pinExplicitFlowSource,assertCurrentSemanticSour
|
|
|
23
23
|
import {workflowDependencyOrder} from './workflow-dependency-order.js';
|
|
24
24
|
import {USER_REQUEST_LIMIT,userRequestRequirement,chatRequestRequirement,checkedUserRequestAnswer} from '../block-sdk/interactive-requirements.js';
|
|
25
25
|
import {validateWorkflowApproval} from '../block-sdk/workflow-approvals.js';
|
|
26
|
+
import {groupOf} from './step-groups.js';
|
|
26
27
|
import {UNATTENDED_HOSTED_OPERATIONS} from './unattended.js';
|
|
27
28
|
export {UNATTENDED_HOSTED_OPERATIONS};
|
|
28
29
|
|
|
29
30
|
const runtimes=new Set(['recipe','javascript']);
|
|
30
31
|
const remoteRuntime=runtime=>['capability','comfyui'].includes(runtime);
|
|
31
32
|
// Remote Steps that may run in an unattended plan: granted connection actions
|
|
32
|
-
// and granted hosted operations.
|
|
33
|
-
|
|
33
|
+
// and granted hosted operations. AI text and image generation are the exception:
|
|
34
|
+
// they run on their own only when the schedule approved a credit cap for them
|
|
35
|
+
// (below); otherwise a scheduled run stops before them for the owner.
|
|
36
|
+
const scheduleApprovedOperation=pkg=>['text.generate','image.generate'].includes(pkg?.capability?.operation);
|
|
37
|
+
const unattendedConnection=pkg=>pkg?.manifest?.runtime==='capability'&&(pkg.capability?.operation==='connection.execute'||UNATTENDED_HOSTED_OPERATIONS.includes(pkg.capability?.operation)&&!scheduleApprovedOperation(pkg));
|
|
38
|
+
// Hosted text and image generation also join when the schedule approved a per-run
|
|
39
|
+
// credit cap for them (unattendedText / unattendedImage).
|
|
40
|
+
const textGeneration=pkg=>pkg?.manifest?.runtime==='capability'&&pkg.capability?.operation==='text.generate';
|
|
41
|
+
const imageGeneration=pkg=>pkg?.manifest?.runtime==='capability'&&pkg.capability?.operation==='image.generate';
|
|
42
|
+
const unattendedStep=(pkg,plan)=>unattendedConnection(pkg)||plan?.unattendedText===true&&textGeneration(pkg)||plan?.unattendedImage===true&&imageGeneration(pkg);
|
|
34
43
|
const hasInteractiveRequirements=pkg=>Object.values(pkg?.manifest.inputs || {}).some(port=>port.semantic && !port.context && (!port.sources || port.sources.includes('user') || port.sources.includes('chat')));
|
|
35
44
|
const edgeKey=edge=>stableStringify({source:edge.source,target:edge.target,sourceHandle:edge.sourceHandle,targetHandle:edge.targetHandle,adapter:edge.data?.stillmadeAdapter ?? null});
|
|
36
45
|
function frozen(value){if(value && typeof value==='object'){Object.freeze(value);for(const child of Object.values(value))frozen(child);}return value;}
|
|
@@ -44,10 +53,12 @@ function localDependencyOrder(placements,type) {
|
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/** Capture the production definition and current placement settings, never fixtures. */
|
|
47
|
-
export async function planSegment(projectType,canvas,startStepId,{interactive=false,background=false,unattended=false,endStepId,includeIndependent=false,reuseCurrentSources=false,batchUpdate=false,hostedUpdate=false,experimentalBrowser,applicationOperations={}}={}) {
|
|
56
|
+
export async function planSegment(projectType,canvas,startStepId,{interactive=false,background=false,unattended=false,unattendedText=false,unattendedImage=false,endStepId,includeIndependent=false,reuseCurrentSources=false,batchUpdate=false,hostedUpdate=false,experimentalBrowser,experimentalBrowserValidation,applicationOperations={}}={}) {
|
|
48
57
|
if(experimentalBrowser!==undefined)assertExperimentalBrowserExecutionAuthority(experimentalBrowser);
|
|
49
58
|
assert(!(interactive&&background),'SEGMENT_APPROVAL','Choose interactive or background execution');
|
|
50
59
|
assert(typeof unattended==='boolean'&&(!unattended||background&&!interactive&&!includeIndependent&&!reuseCurrentSources),'SEGMENT_APPROVAL','Unattended actions require the explicit background connection host.');
|
|
60
|
+
assert(typeof unattendedText==='boolean'&&(!unattendedText||unattended),'SEGMENT_APPROVAL','Unattended text generation requires an unattended background host.');
|
|
61
|
+
assert(typeof unattendedImage==='boolean'&&(!unattendedImage||unattended),'SEGMENT_APPROVAL','Unattended image generation requires an unattended background host.');
|
|
51
62
|
assert(typeof includeIndependent==='boolean' && (!includeIndependent || interactive && !background),'SEGMENT_APPROVAL','Independent Steps require an explicit interactive workflow run');
|
|
52
63
|
assert(typeof reuseCurrentSources==='boolean' && (!reuseCurrentSources || interactive && !background && !includeIndependent && endStepId===startStepId),'SEGMENT_APPROVAL','Reusing current sources requires an interactive single-Step update.');
|
|
53
64
|
assert(typeof batchUpdate==='boolean' && (!batchUpdate || reuseCurrentSources),'SEGMENT_APPROVAL','A batch update needs an explicit current-source update plan.');
|
|
@@ -56,22 +67,32 @@ export async function planSegment(projectType,canvas,startStepId,{interactive=fa
|
|
|
56
67
|
safeDocument(operations);assert(plain(operations),'BROWSER_APPLICATION_OPERATION','Select application operations by Step ID.');
|
|
57
68
|
safeDocument(type);assert(plain(graph) && Array.isArray(graph.nodes) && Array.isArray(graph.edges),'SEGMENT_CANVAS','A saved production Canvas is required');
|
|
58
69
|
if(graph.projectType)assert(stableStringify(graph.projectType)===stableStringify(type),'SEGMENT_CHANGED','The workflow and Canvas definitions differ');
|
|
59
|
-
const compiled=await compileTypeWorkspace(type,{experimentalBrowser}),active=type.stages.filter(step=>step.enabled!==false)
|
|
70
|
+
const compiled=await compileTypeWorkspace(type,{experimentalBrowser:experimentalBrowser ?? experimentalBrowserValidation}),active=type.stages.filter(step=>step.enabled!==false);
|
|
71
|
+
// One Step per job: with no explicit range, an interactive run from any part
|
|
72
|
+
// of a Step group runs that whole job, from its first enabled part to its last.
|
|
73
|
+
const job=interactive&&!includeIndependent&&endStepId===undefined?groupOf(type,startStepId):null,jobParts=job?active.filter(step=>job.stages.includes(step.id)).map(step=>step.id):[];
|
|
74
|
+
if(jobParts.length){startStepId=jobParts[0];endStepId=jobParts.at(-1);}
|
|
75
|
+
const start=active.findIndex(step=>step.id===startStepId);
|
|
60
76
|
assert(start>=0,'SEGMENT_STEP','Choose an enabled starting Step');
|
|
61
77
|
const end=endStepId===undefined?active.length-1:active.findIndex(step=>step.id===endStepId);
|
|
62
78
|
assert(end>=start,'SEGMENT_STEP','The execution range must end at an enabled Step at or after its start');
|
|
63
79
|
const steps=[],selected=new Set();let nextStageId=null,stopReason='complete';
|
|
64
|
-
|
|
80
|
+
let placements=active.slice(start,end+1);
|
|
81
|
+
// A source-only context can describe upstream applications, never run them.
|
|
82
|
+
if(experimentalBrowser===undefined){const boundary=placements.findIndex(step=>isBrowserApplication(packageForStage(type,step)));if(boundary>=0){nextStageId=placements[boundary].id;stopReason='runtime-approval';placements=placements.slice(0,boundary);}}
|
|
65
83
|
const applicationPlacements=placements.filter(step=>isBrowserApplication(packageForStage(type,step)));
|
|
66
84
|
assert(!applicationPlacements.length||interactive&&!background&&!includeIndependent&&!reuseCurrentSources&&!batchUpdate&&!hostedUpdate,'BROWSER_APPLICATION_APPROVAL','Application operations require an interactive connected run without batch, independent or source-reuse modes.');
|
|
67
85
|
assert(Object.keys(operations).every(id=>applicationPlacements.some(step=>step.id===id)),'BROWSER_APPLICATION_OPERATION','Operations must belong to selected application Steps.');
|
|
68
86
|
const executionOrder=includeIndependent?localDependencyOrder(placements,type):placements;
|
|
87
|
+
// A When Block starts an automation: every Step after it runs, in order, even
|
|
88
|
+
// one that only uses its own settings (such as an app task listing new leads).
|
|
89
|
+
const automationRoot=packageForStage(type,placements[0])?.manifest.id==='sm.when';
|
|
69
90
|
for(const placement of executionOrder) {
|
|
70
91
|
const pkg=packageForStage(type,placement);
|
|
71
92
|
const applicationOperation=isBrowserApplication(pkg)?experimentalBrowserOperation(experimentalBrowser,pkg,operations[placement.id]):undefined;
|
|
72
93
|
if(requiresDesktopExecution(pkg)&&(!interactive||background||includeIndependent)){nextStageId=placement.id;stopReason='runtime-approval';break;}
|
|
73
94
|
if(pkg?.manifest.state&&!interactive&&!background){nextStageId=placement.id;stopReason='runtime-approval';break;}
|
|
74
|
-
if(!pkg || !(runtimes.has(pkg.manifest.runtime)||unattended&&unattendedConnection(pkg)||isBrowserApplication(pkg)&&interactive||interactive&&!includeIndependent&&(!reuseCurrentSources||hostedUpdate&&pkg.manifest.runtime==='capability')&&remoteRuntime(pkg.manifest.runtime))){nextStageId=placement.id;stopReason=pkg?'runtime-approval':'host-workspace';break;}
|
|
95
|
+
if(!pkg || !(runtimes.has(pkg.manifest.runtime)||unattended&&(unattendedConnection(pkg)||unattendedText&&textGeneration(pkg)||unattendedImage&&imageGeneration(pkg))||isBrowserApplication(pkg)&&interactive||interactive&&!includeIndependent&&(!reuseCurrentSources||hostedUpdate&&pkg.manifest.runtime==='capability')&&remoteRuntime(pkg.manifest.runtime))){nextStageId=placement.id;stopReason=pkg?'runtime-approval':'host-workspace';break;}
|
|
75
96
|
const incoming=await Promise.all((type.connections || []).filter(edge=>edge.to.stage===placement.id).map(async edge=>{
|
|
76
97
|
// External converted values arrive raw. Pin the producing contract so the
|
|
77
98
|
// executor can check its type/role without trusting a caller's descriptor.
|
|
@@ -91,7 +112,7 @@ export async function planSegment(projectType,canvas,startStepId,{interactive=fa
|
|
|
91
112
|
const outputContract=pin?{...currentOutputContract,...pin.outputContract}:currentOutputContract;
|
|
92
113
|
return {...edge,outputContract:structuredClone(outputContract),...(savedSource?{savedSource}:{})};
|
|
93
114
|
}));
|
|
94
|
-
if(!includeIndependent && steps.length && !incoming.some(edge=>selected.has(edge.from.stage))){nextStageId=placement.id;stopReason='unconnected-step';break;}
|
|
115
|
+
if(!includeIndependent && steps.length && !automationRoot && !incoming.some(edge=>selected.has(edge.from.stage))){nextStageId=placement.id;stopReason='unconnected-step';break;}
|
|
95
116
|
assert(incoming.every(edge=>reuseCurrentSources?!!edge.savedSource || active.findIndex(step=>step.id===edge.from.stage)<active.indexOf(placement):includeIndependent?selected.has(edge.from.stage) || active.findIndex(step=>step.id===edge.from.stage)<start:active.findIndex(step=>step.id===edge.from.stage)<active.indexOf(placement)),'SEGMENT_ORDER',includeIndependent?'A required source is outside this local run. Start from that source or review the workflow boundary.':'A production Step depends on a later Step. Reorder the workflow first');
|
|
96
117
|
const candidates=graph.nodes.filter(node=>node.id===stageNodeId(placement)),node=candidates[0];
|
|
97
118
|
assert(candidates.length===1 && node.type==='programmableBlockNode','SEGMENT_PIN','The compiled Step is missing or duplicated');
|
|
@@ -110,7 +131,7 @@ export async function planSegment(projectType,canvas,startStepId,{interactive=fa
|
|
|
110
131
|
assert(steps.length,'SEGMENT_RUNTIME','Choose a recipe or JavaScript Step to run a connected segment');
|
|
111
132
|
if(endStepId!==undefined && stopReason==='complete' && active[end+1]){nextStageId=active[end+1].id;stopReason='selection-boundary';}
|
|
112
133
|
const semanticManifests=active.map(stage=>packageForStage(type,stage,BUILTIN_BLOCKS)?.manifest).filter(manifest=>manifest?.semantics?.length).map(manifest=>({id:manifest.id,version:manifest.version,semantics:structuredClone(manifest.semantics)}));
|
|
113
|
-
const plan={schemaVersion:1,workflowDigest:compiled.digest,startStepId,steps,nextStageId,stopReason,...(includeIndependent?{includeIndependent:true}:{}),...(unattended?{unattended:true}:{}),...(background&&steps.some(step=>step.package.manifest.state)?{background:true}:{}),...(interactive&&(includeIndependent||steps.some(step=>isBrowserApplication(step.package)||requiresDesktopExecution(step.package)||remoteRuntime(step.package.manifest.runtime)||step.package.manifest.state||hasInteractiveRequirements(step.package)||hasInteractiveRequirements(step.fallbackPackage)))?{interactive:true}:{})};
|
|
134
|
+
const plan={schemaVersion:1,workflowDigest:compiled.digest,startStepId,steps,nextStageId,stopReason,...(includeIndependent?{includeIndependent:true}:{}),...(unattended?{unattended:true}:{}),...(unattendedText?{unattendedText:true}:{}),...(unattendedImage?{unattendedImage:true}:{}),...(background&&steps.some(step=>step.package.manifest.state)?{background:true}:{}),...(interactive&&(includeIndependent||steps.some(step=>isBrowserApplication(step.package)||requiresDesktopExecution(step.package)||remoteRuntime(step.package.manifest.runtime)||step.package.manifest.state||hasInteractiveRequirements(step.package)||hasInteractiveRequirements(step.fallbackPackage)))?{interactive:true}:{})};
|
|
114
135
|
if(semanticManifests.length)plan.semanticManifests=semanticManifests;
|
|
115
136
|
if(type.projectInputs?.length){plan.projectInputs=structuredClone(type.projectInputs);plan.projectTypeIdentity={id:type.id,version:type.version};}
|
|
116
137
|
if(reuseCurrentSources){plan.reuseCurrentSources=true;plan.interactive=true;}
|
|
@@ -181,7 +202,7 @@ async function runSegmentInternal(suppliedPlan,{runId=crypto.randomUUID(),source
|
|
|
181
202
|
assert(!plan.includeIndependent || plan.includeIndependent===true && interactive && plan.interactive && !background && !hosted,'SEGMENT_APPROVAL','Independent Steps require an interactive local workflow run');
|
|
182
203
|
assert(!rebind || typeof verifyHostedResult==='function' || !plan.steps.slice(0,prior.length).some(step=>remoteRuntime(step.package.manifest.runtime)),'SEGMENT_APPROVAL','Hosted receipts require a separate project ownership adapter');
|
|
183
204
|
const unattendedHost=plan.unattended===true&&background===true&&!interactive&&typeof authorizeHostedStep==='function'&&typeof verifyHostedResult==='function';
|
|
184
|
-
assert(!plan.unattended||unattendedHost&&plan.steps.filter(step=>remoteRuntime(step.package.manifest.runtime)).every(step=>
|
|
205
|
+
assert(!plan.unattended||unattendedHost&&plan.steps.filter(step=>remoteRuntime(step.package.manifest.runtime)).every(step=>unattendedStep(step.package,plan)&&!step.failurePolicy&&!step.fallbackPackage),'SEGMENT_APPROVAL','Unattended Steps require exact current grants and verified connection receipts; automatic remote retries are unavailable.');
|
|
185
206
|
assert(!hosted||(interactive===true&&plan.interactive===true&&typeof verifyHostedResult==='function')||unattendedHost,'SEGMENT_APPROVAL','Hosted Steps require an approved host and verified execution receipts');
|
|
186
207
|
safeDocument(memorySources);assert(plain(memorySources),'SEGMENT_STATE','Memory sources must be keyed by placement.');
|
|
187
208
|
safeDocument(memory);assert(plain(memory)&&Object.keys(memory).every(id=>plan.steps.some(step=>step.id===id&&step.package.manifest.state)),'SEGMENT_STATE','Memory must belong to a selected stateful placement');
|
|
@@ -202,7 +223,7 @@ async function runSegmentInternal(suppliedPlan,{runId=crypto.randomUUID(),source
|
|
|
202
223
|
await fence(step);
|
|
203
224
|
const pkg=step.package,m=pkg.manifest;
|
|
204
225
|
const startingState=m.state?validateBlockState(m,memory[step.id]??step.state):undefined,stateSource=memorySources[step.id]??step.stateSource;
|
|
205
|
-
assert((runtimes.has(m.runtime)||unattendedHost&&
|
|
226
|
+
assert((runtimes.has(m.runtime)||unattendedHost&&unattendedStep(pkg,plan)||interactive&&plan.interactive&&(remoteRuntime(m.runtime)||isBrowserApplication(pkg))) && await wait(digest(pkg))===step.packageDigest,'SEGMENT_PIN','The Step package pin changed');
|
|
206
227
|
if(step.failurePolicy?.action==='fallback_block'){validateFailurePolicy(pinnedFailurePolicy(step.failurePolicy),m,[step.fallbackPackage]);assert(await wait(digest(step.fallbackPackage))===step.fallbackPackageDigest,'SEGMENT_PIN','The fallback Block package pin changed');}
|
|
207
228
|
else assert(step.fallbackPackage===undefined&&step.fallbackPackageDigest===undefined,'SEGMENT_PIN','Unexpected fallback package');
|
|
208
229
|
const applicationOptions=isBrowserApplication(pkg)?{operation:step.applicationOperation,stepId:step.id,runId,planDigest:plan.planDigest,workflowDigest:plan.workflowDigest,signal:controller.signal,check:()=>fence(step)}:null;
|
|
@@ -219,6 +240,9 @@ async function runSegmentInternal(suppliedPlan,{runId=crypto.randomUUID(),source
|
|
|
219
240
|
if(selectedPrimary!==null)input[selectedPrimary]=structuredClone(origin);
|
|
220
241
|
let changedPrimaryRepresentation=false;const workspaceSources=new Map(),connectionSources={};
|
|
221
242
|
const context=await wait(resolveContext(structuredClone(step),structuredClone(origin)));await fence(step);
|
|
243
|
+
// An upstream gate that produced nothing for a required input means "this
|
|
244
|
+
// branch was not taken": the run ends here, successfully, without asking.
|
|
245
|
+
if(incoming.some(edge=>ids.has(edge.from.stage)&&edge.to.port!==selectedPrimary&&m.inputs[edge.to.port]?.required!==false&&completed.has(edge.from.stage)&&!Object.hasOwn(completed.get(edge.from.stage).outputs,edge.from.port))){result.stoppedAt={stepId:step.id,reason:'branch-not-taken'};break;}
|
|
222
246
|
for(const edge of incoming){
|
|
223
247
|
assert(edge.to.stage===step.id && Object.hasOwn(m.inputs,edge.to.port),'SEGMENT_INPUT','Connection targets an unavailable input');
|
|
224
248
|
let output,value,omitted=false;
|
|
@@ -398,7 +422,7 @@ async function runSegmentInternal(suppliedPlan,{runId=crypto.randomUUID(),source
|
|
|
398
422
|
const alternate=executionPackage!==pkg,executionStep=alternate?{...step,package:executionPackage,packageDigest:step.fallbackPackageDigest,label:`${step.label} / ${executionPackage.manifest.name}`}:step;
|
|
399
423
|
await wait(onStartStep({runId,stepId:step.id,packageDigest:executionStep.packageDigest,placementDigest:step.packageDigest,attempt,inputDigest,capturedInputs:captureRunInputIdentities({...bound,...runtimeAnswers,...runtimeChatValues},activeSourceMap(),m.inputs),inputsComplete:executionPackage.manifest.runtime!=='javascript'}));await fence(step);
|
|
400
424
|
const dispatchInputDigest=inputDigest;
|
|
401
|
-
if(unattendedHost&&
|
|
425
|
+
if(unattendedHost&&unattendedStep(executionPackage,plan))await wait(authorizeHostedStep(structuredClone(executionPackage),structuredClone(executionInput),{step:structuredClone(executionStep),runId,inputDigest,signal:controller.signal,check:()=>fence(step)}));
|
|
402
426
|
const execution=isBrowserApplication(executionPackage)?await executeExperimentalBrowser(experimentalBrowser,executionPackage,executionInput,applicationOptions):await execute(structuredClone(executionPackage),structuredClone({...executionInput,...runtimeAnswers,...runtimeChatValues}),{...(recoverLastValid?{recoverLastValid}:{}),...(requestRuntimeAnswer && executionPackage.manifest.runtime==='javascript'?{requestFromUser:question=>requestRuntimeAnswer(question,attempt,executionStep.label,executionStep.packageDigest)}:{}),...(requestRuntimeChat && executionPackage.manifest.runtime==='javascript'?{requestFromChat:question=>requestRuntimeChat(question,attempt,executionStep)}:{}),runId,inputDigest,...(materialInputDigest?{workspaceMaterializedInput:structuredClone(bound)}:{}),workspaceSources:structuredClone([...workspaceSources.values()]),semanticSources:structuredClone(semantic.provenance),step:structuredClone(executionStep),...(executionPackage.manifest.state?{state:structuredClone(startingState),stateSource:structuredClone(stateSource)}:{}),signal:controller.signal,check:()=>fence(step)});
|
|
403
427
|
if(requiresDesktopExecution(executionPackage)){
|
|
404
428
|
assert(!execution.skipped,'DESKTOP_RECEIPT','The native executor must return a completed native action; only the host may apply a reviewed skip or fallback.');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// One Step a person sees is one job, and a job can be made of several parts
|
|
2
|
+
// (contract rule block-scope). A group names the parts of one job: consecutive
|
|
3
|
+
// Steps in `stages`. Each part stays a real Step, so its permissions, approvals,
|
|
4
|
+
// credits, memory and runs are unchanged; groups only change what people see.
|
|
5
|
+
import {assert,plain} from '../block-sdk/contracts.js';
|
|
6
|
+
import {BLOCK_NAME_MAX_LENGTH} from '../block-sdk/builder-contract.js';
|
|
7
|
+
|
|
8
|
+
export const MAX_GROUP_PARTS=12;
|
|
9
|
+
const GROUP_KEYS=['id','label','stages','settings','result'];
|
|
10
|
+
|
|
11
|
+
/** Checks `type.groups` against the stages and their manifests (Map stageId → manifest). */
|
|
12
|
+
export function validateStepGroups(type,manifests){
|
|
13
|
+
if(type.groups===undefined)return;
|
|
14
|
+
assert(Array.isArray(type.groups)&&type.groups.length<=50,'STEP_GROUP','Expected at most 50 Step groups');
|
|
15
|
+
const order=new Map(type.stages.map((stage,index)=>[stage.id,index])),used=new Set(),ids=new Set();
|
|
16
|
+
for(const group of type.groups){
|
|
17
|
+
assert(plain(group)&&Object.keys(group).every(key=>GROUP_KEYS.includes(key)),'STEP_GROUP','A Step group has an id, a name, its parts, settings and a result');
|
|
18
|
+
assert(/^[a-z][a-z0-9-]*$/.test(group.id||'')&&!ids.has(group.id)&&!order.has(group.id),'STEP_GROUP','Each Step group needs its own ID');ids.add(group.id);
|
|
19
|
+
assert(typeof group.label==='string'&&group.label.trim()&&group.label.length<=BLOCK_NAME_MAX_LENGTH,'STEP_GROUP',`Name each Step in ${BLOCK_NAME_MAX_LENGTH} characters or fewer`);
|
|
20
|
+
assert(Array.isArray(group.stages)&&group.stages.length>=2&&group.stages.length<=MAX_GROUP_PARTS,'STEP_GROUP',`A Step groups 2 to ${MAX_GROUP_PARTS} parts`);
|
|
21
|
+
const indexes=group.stages.map(id=>order.get(id));
|
|
22
|
+
assert(indexes.every(Number.isInteger)&&group.stages.every(id=>!used.has(id)),'STEP_GROUP','A part belongs to one Step and must exist');
|
|
23
|
+
assert(indexes.every((index,at)=>!at||index===indexes[at-1]+1),'STEP_GROUP','The parts of a Step must be next to each other, in order');
|
|
24
|
+
for(const id of group.stages)used.add(id);
|
|
25
|
+
// A When part starts its job; it is never in the middle of one.
|
|
26
|
+
assert(group.stages.slice(1).every(id=>manifests.get(id)?.id!=='sm.when'),'STEP_GROUP','When starts its Step');
|
|
27
|
+
assert(group.settings===undefined||Array.isArray(group.settings)&&group.settings.length<=16&&group.settings.every(item=>plain(item)&&group.stages.includes(item.stage)&&Object.hasOwn(manifests.get(item.stage)?.inputs||{},item.input)&&typeof item.label==='string'&&item.label.trim()&&item.label.length<=60),'STEP_GROUP','Step settings must name an input of one of its parts');
|
|
28
|
+
assert(group.result===undefined||plain(group.result)&&group.stages.includes(group.result.stage)&&Object.hasOwn(manifests.get(group.result.stage)?.outputs||{},group.result.port),'STEP_GROUP','A Step result must be an output of one of its parts');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keeps groups consistent after an edit: missing parts leave, a part moved away leaves (the longest run stays), and a group under two parts is dissolved. */
|
|
33
|
+
export function normalizeGroups(type){
|
|
34
|
+
if(!Array.isArray(type.groups))return type;
|
|
35
|
+
const order=new Map(type.stages.map((stage,index)=>[stage.id,index]));
|
|
36
|
+
const groups=type.groups.flatMap(group=>{
|
|
37
|
+
const present=group.stages.filter(id=>order.has(id)).sort((a,b)=>order.get(a)-order.get(b)),runs=[];
|
|
38
|
+
for(const id of present){if(runs.length&&order.get(id)===order.get(runs.at(-1).at(-1))+1)runs.at(-1).push(id);else runs.push([id]);}
|
|
39
|
+
const stages=runs.reduce((best,run)=>run.length>best.length?run:best,[]);
|
|
40
|
+
if(stages.length<2)return [];
|
|
41
|
+
const settings=(group.settings||[]).filter(item=>stages.includes(item.stage)),result=group.result&&stages.includes(group.result.stage)?group.result:undefined;
|
|
42
|
+
return [{id:group.id,label:group.label,stages,...(settings.length?{settings}:{}),...(result?{result}:{})}];
|
|
43
|
+
});
|
|
44
|
+
if(!groups.length){const {groups:_dropped,...rest}=type;return rest;}
|
|
45
|
+
return {...type,groups};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The group a Step belongs to, if any. */
|
|
49
|
+
export const groupOf=(type,stageId)=>(type.groups||[]).find(group=>group.stages.includes(stageId))||null;
|
|
50
|
+
|
|
51
|
+
/** What people see in order: one item per job, a group or a single Step. */
|
|
52
|
+
export function stepItems(type){
|
|
53
|
+
const items=[],seen=new Set();
|
|
54
|
+
for(const stage of type.stages){
|
|
55
|
+
if(seen.has(stage.id))continue;
|
|
56
|
+
const group=groupOf(type,stage.id);
|
|
57
|
+
if(group){for(const id of group.stages)seen.add(id);items.push({kind:'group',id:group.id,label:group.label,stages:[...group.stages],group});}
|
|
58
|
+
else{seen.add(stage.id);items.push({kind:'stage',id:stage.id,label:stage.label,stages:[stage.id]});}
|
|
59
|
+
}
|
|
60
|
+
return items;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A Step inserted strictly between two parts of a job becomes one of its parts. */
|
|
64
|
+
export function joinGroupAt(type,stageId,index){
|
|
65
|
+
if(!Array.isArray(type.groups))return type;
|
|
66
|
+
const before=type.stages[index-1]?.id,after=type.stages[index+1]?.id;
|
|
67
|
+
return {...type,groups:type.groups.map(group=>before&&after&&group.stages.includes(before)&&group.stages.includes(after)?{...group,stages:[...group.stages.slice(0,group.stages.indexOf(after)),stageId,...group.stages.slice(group.stages.indexOf(after))]}:group)};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Group consecutive Steps into one job. */
|
|
71
|
+
export function groupSteps(type,stageIds,label,{id=`job-${crypto.randomUUID().slice(0,8)}`}={}){
|
|
72
|
+
const groups=(type.groups||[]).filter(group=>!group.stages.some(stage=>stageIds.includes(stage)));
|
|
73
|
+
return {...type,groups:[...groups,{id,label:String(label||'').trim().slice(0,BLOCK_NAME_MAX_LENGTH),stages:[...stageIds]}]};
|
|
74
|
+
}
|
|
75
|
+
/** Show a job's parts as separate Steps again. */
|
|
76
|
+
export function ungroupStep(type,groupId){return normalizeGroups({...type,groups:(type.groups||[]).filter(group=>group.id!==groupId)});}
|
|
77
|
+
/** Rename a job. */
|
|
78
|
+
export function renameGroup(type,groupId,label){return {...type,groups:(type.groups||[]).map(group=>group.id===groupId?{...group,label:String(label||'').trim().slice(0,BLOCK_NAME_MAX_LENGTH)}:group)};}
|
|
@@ -4,6 +4,7 @@ import {validateProjectType} from './project-types.js';
|
|
|
4
4
|
import {validateCondition} from './conditions.js';
|
|
5
5
|
import {HOST_PORTS} from './type-workspace.js';
|
|
6
6
|
import {packageForStage} from './stage-package.js';
|
|
7
|
+
import {normalizeGroups,joinGroupAt} from './step-groups.js';
|
|
7
8
|
|
|
8
9
|
// `stages` is the existing on-disk field for Step instances. Keep it stable so
|
|
9
10
|
// releases, digests, project links and instance IDs do not need migration.
|
|
@@ -41,7 +42,9 @@ function connection(type,from,to,catalog) {
|
|
|
41
42
|
validateConnectionAdapter(out,input,{permissions:b.permissions?.project || [],...(match.adapter?{adapter:match.adapter}:{})});
|
|
42
43
|
return {from:{stage:from.id,port:out.name},to:{stage:to.id,port:input.name},...(match.adapter?{adapter:match.adapter}:{})};
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
// Every edit keeps Step groups consistent: missing parts leave a group, and a
|
|
46
|
+
// group that is no longer one run of parts is shown as separate Steps again.
|
|
47
|
+
function validated(type,catalog,options) {type=normalizeGroups(type);const result=validateProjectType(type,catalog,options);assert(result.valid,'STEP',result.errors.map(error=>error.message).join('. '));return type;}
|
|
45
48
|
function pinPackage(type,pkg,catalog) {
|
|
46
49
|
const manifest=pkg?.manifest;assert(manifest,'MANIFEST','Choose a Block package');
|
|
47
50
|
const packages=[...(type.packages || [])],matches=[...catalog,...packages].filter(candidate=>candidate?.manifest?.id===manifest.id&&candidate.manifest.version===manifest.version),existing=matches.find(candidate=>stableStringify(candidate)===stableStringify(pkg));
|
|
@@ -52,10 +55,10 @@ function pinPackage(type,pkg,catalog) {
|
|
|
52
55
|
}
|
|
53
56
|
return packages;
|
|
54
57
|
}
|
|
55
|
-
function insertConfigured(type,pkg,index,catalog,step) {
|
|
58
|
+
function insertConfigured(type,pkg,index,catalog,step,options) {
|
|
56
59
|
assert(Number.isInteger(index)&&index>=0&&index<=type.stages.length,'STEP','Choose a position in the workflow');
|
|
57
60
|
const packages=pinPackage(type,pkg,catalog),nextType={...type,packages},stages=[...type.stages.slice(0,index),step,...type.stages.slice(index)];
|
|
58
|
-
if(step.enabled===false)return validated({...nextType,stages},catalog);
|
|
61
|
+
if(step.enabled===false)return validated({...nextType,stages},catalog,options);
|
|
59
62
|
const previous=type.stages.slice(0,index).findLast(item=>item.enabled!==false),next=type.stages.slice(index).find(item=>item.enabled!==false);
|
|
60
63
|
const incoming=previous?connection(nextType,previous,step,catalog):null,outgoing=next?connection(nextType,step,next,catalog):null;
|
|
61
64
|
const oldOutput=previous&&automaticPort(manifestFor(nextType,previous,catalog),'outputs'),oldInput=next&&automaticPort(manifestFor(nextType,next,catalog),'inputs');
|
|
@@ -65,16 +68,17 @@ function insertConfigured(type,pkg,index,catalog,step) {
|
|
|
65
68
|
for(const edge of [incoming,outgoing].filter(Boolean)) {
|
|
66
69
|
assert(!connections.some(existing=>existing.to.stage===edge.to.stage&&existing.to.port===edge.to.port),'STEP_INCOMPATIBLE','Another Block already supplies this input. Choose a compatible position in its connected workflow.');connections.push(edge);
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
// A Step placed between two parts of a job becomes one of its parts.
|
|
72
|
+
return validated(joinGroupAt({...type,packages,stages,connections},step.id,index),catalog,options);
|
|
69
73
|
}
|
|
70
|
-
export function insertStep(type,pkg,index,catalog=[],{id}={}) {
|
|
74
|
+
export function insertStep(type,pkg,index,catalog=[],{id,experimentalBrowser,experimentalNative}={}) {
|
|
71
75
|
const manifest=pkg?.manifest;assert(manifest,'MANIFEST','Choose a Block package');
|
|
72
76
|
assert(manifest.id!==ROOT_CHAT_ID,'ROOT_PROTECTED','Starting chat is provided by the host and cannot be inserted as another Step');
|
|
73
|
-
return insertConfigured(type,pkg,index,catalog,{id:id||`step-${crypto.randomUUID().slice(0,12)}`,blockId:manifest.id,version:manifest.version,label:manifest.name});
|
|
77
|
+
return insertConfigured(type,pkg,index,catalog,{id:id||`step-${crypto.randomUUID().slice(0,12)}`,blockId:manifest.id,version:manifest.version,label:manifest.name},{experimentalBrowser,experimentalNative});
|
|
74
78
|
}
|
|
75
|
-
export function insertionLocations(type,pkg,catalog=[]) {
|
|
79
|
+
export function insertionLocations(type,pkg,catalog=[],options={}) {
|
|
76
80
|
return Array.from({length:type.stages.length+1},(_,index)=>{
|
|
77
|
-
try{insertStep(type,pkg,index,catalog,{id:`insertion-preview-${index}`});return {index,compatible:true};}
|
|
81
|
+
try{insertStep(type,pkg,index,catalog,{...options,id:`insertion-preview-${index}`});return {index,compatible:true};}
|
|
78
82
|
catch(error){return {index,compatible:false,reason:error.message};}
|
|
79
83
|
});
|
|
80
84
|
}
|
|
@@ -19,8 +19,8 @@ export function stageHost(stage) {
|
|
|
19
19
|
return {phase:declaration?.phase||'blockStep',nodeId:host.canvasNode||!declaration?stageNodeId(stage):null,exportVideo:!!host.exportVideo,...(host.exportKind?{exportKind:host.exportKind}:{}),...(host.inspectClip?{inspectClip:true}:{}),...(host.editorTab?{editorTab:host.editorTab}:{})};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function typeWorkspaceSupport(type,{experimentalBrowser}={}) {
|
|
23
|
-
const validation = validateProjectType(type, BUILTIN_BLOCKS,{experimentalBrowser});
|
|
22
|
+
export function typeWorkspaceSupport(type,{experimentalBrowser,experimentalNative}={}) {
|
|
23
|
+
const validation = validateProjectType(type, BUILTIN_BLOCKS,{experimentalBrowser,experimentalNative});
|
|
24
24
|
if (!validation.valid) return { supported: false, reasons: validation.errors.map(e => e.message) };
|
|
25
25
|
const recipes = new Set([...(type.packages || []),...BUILTIN_BLOCKS.filter(pkg=>pkg.manifest.runtime!=='host')].map(pkg => `${pkg.manifest.id}@${pkg.manifest.version}`));
|
|
26
26
|
const reasons = [];
|
|
@@ -51,14 +51,14 @@ export function projectAnchorTarget(project, anchor) {
|
|
|
51
51
|
}
|
|
52
52
|
// All executable source travels with the project. Installing a newer release
|
|
53
53
|
// cannot change these nodes or silently replace a project's workflow definition.
|
|
54
|
-
export async function compileTypeWorkspace(type,{experimentalBrowser}={}) {
|
|
54
|
+
export async function compileTypeWorkspace(type,{experimentalBrowser,experimentalNative}={}) {
|
|
55
55
|
const definition = structuredClone(type);
|
|
56
56
|
// Embed exact SDK-owned built-ins so saved projects carry their implementation.
|
|
57
57
|
for (const stage of definition.stages || []) {
|
|
58
58
|
const builtin = BUILTIN_BLOCKS.find(pkg => pkg.manifest.id === stage.blockId && pkg.manifest.version === stage.version && pkg.manifest.runtime !== 'host');
|
|
59
59
|
if (builtin && !definition.packages?.some(pkg => pkg.manifest.id === stage.blockId && pkg.manifest.version === stage.version)) (definition.packages ||= []).push(structuredClone(builtin));
|
|
60
60
|
}
|
|
61
|
-
const support = typeWorkspaceSupport(definition,{experimentalBrowser});
|
|
61
|
+
const support = typeWorkspaceSupport(definition,{experimentalBrowser,experimentalNative});
|
|
62
62
|
assert(support.supported, 'UNSUPPORTED_TYPE', support.reasons.join('\n'));
|
|
63
63
|
const nodes = [];
|
|
64
64
|
for (const stage of definition.stages.filter(stage=>stage.enabled !== false)) {
|
|
@@ -4,6 +4,10 @@ import {assertInputSource} from '../block-sdk/input-source-policy.js';
|
|
|
4
4
|
import {semanticRegistryForManifests} from '../block-sdk/package-semantics.js';
|
|
5
5
|
import {validateDataPredicate,conditionMatches} from './conditions.js';
|
|
6
6
|
|
|
7
|
+
// Repeat workflow is tucked away in the product for now (owner decision,
|
|
8
|
+
// October 9, 2026): the loop panel is hidden on Steps. The loop runtime, routes
|
|
9
|
+
// and tests stay in place; flip this to true to show the panel again.
|
|
10
|
+
export const WORKFLOW_LOOPS_ENABLED=false;
|
|
7
11
|
export const MAX_WORKFLOW_ITERATIONS=100;
|
|
8
12
|
|
|
9
13
|
const endpoint=value=>plain(value) && Object.keys(value).length===2 && ['stage','port'].every(key=>typeof value[key]==='string' && value[key].length>0 && value[key].length<=200);
|