stillmade-sdk 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.cursor/hooks.json +11 -0
  3. package/.gitignore +2 -0
  4. package/.stillmade/sdk-baseline.json +65 -0
  5. package/AGENTS.md +29 -7
  6. package/AGENT_CONTROL.md +2 -1
  7. package/BROWSER_APPLICATIONS.md +128 -3
  8. package/GITHUB_APPLICATIONS.md +184 -1
  9. package/NATIVE_APPLICATION_ADAPTATION.md +565 -0
  10. package/PASTE_TO_LLM.md +29 -10
  11. package/PORTABLE.md +5 -4
  12. package/REMIX.md +16 -10
  13. package/SDK.md +511 -46
  14. package/UI_AESTHETIC.md +37 -6
  15. package/api-index.json +90 -0
  16. package/blocks/connected-app/versions/{1.2.0 → 1.5.0}/task-selection.js +5 -3
  17. package/blocks/connected-app/versions/{1.4.0 → 1.5.0}/view.js +4 -2
  18. package/blocks/native/sm.ai-decide/versions/1.0.0/package.js +149 -0
  19. package/blocks/native/sm.ai-write/versions/1.0.0/package.js +147 -0
  20. package/blocks/native/sm.connect-app/versions/1.0.0/package.js +55 -0
  21. package/blocks/native/sm.contact-fields/versions/1.0.0/package.js +197 -0
  22. package/blocks/native/sm.continue-if/versions/1.0.0/package.js +168 -0
  23. package/blocks/native/sm.generate-image/versions/2.1.0/package.js +101 -0
  24. package/blocks/native/sm.meta-creative/versions/1.0.0/package.js +206 -0
  25. package/blocks/native/sm.new-items/versions/1.0.0/package.js +191 -0
  26. package/blocks/native/sm.voiceover/versions/2.1.5/package.js +179 -0
  27. package/blocks/native/sm.voiceover/versions/2.1.6/package.js +179 -0
  28. package/blocks/native/sm.when/versions/1.0.0/package.js +129 -0
  29. package/blocks/native/sm.write-summary/versions/1.0.0/package.js +133 -0
  30. package/blocks/occasion-story/package.js +1 -1
  31. package/blocks/occasion-story/versions/1.0.2/package.js +1682 -0
  32. package/blocks/photo-reel-composer/versions/1.0.1/package.js +3026 -0
  33. package/blocks/release-visuals/package.js +1 -1
  34. package/blocks/release-visuals/versions/1.0.2/package.js +1500 -0
  35. package/blocks/short-form-finish/package.js +1 -1
  36. package/blocks/short-form-finish/versions/1.0.3/package.js +1026 -0
  37. package/package.json +1 -1
  38. package/packages/block-cli/cli.js +16 -1
  39. package/packages/block-cli/format.js +2 -0
  40. package/packages/block-cli/ready-gate.js +164 -0
  41. package/packages/block-platform/block-readiness.js +3 -2
  42. package/packages/block-platform/canvas.js +1 -1
  43. package/packages/block-platform/catalog.js +6 -4
  44. package/packages/block-platform/chat-requirements.js +8 -3
  45. package/packages/block-platform/collaboration.js +5 -5
  46. package/packages/block-platform/first-revenue-project-types.js +2 -2
  47. package/packages/block-platform/flow-inspection.js +2 -2
  48. package/packages/block-platform/native-application-validation-context.js +20 -0
  49. package/packages/block-platform/plain-descriptions.js +67 -0
  50. package/packages/block-platform/product-ad-variants-project-type.js +6 -3
  51. package/packages/block-platform/project-context.js +4 -4
  52. package/packages/block-platform/project-type-composer.js +21 -12
  53. package/packages/block-platform/project-type-draft.js +2 -2
  54. package/packages/block-platform/project-type-recommendations.js +1 -1
  55. package/packages/block-platform/project-types.js +9 -4
  56. package/packages/block-platform/revenue-review-project-types.js +6 -5
  57. package/packages/block-platform/segment-runner.js +35 -11
  58. package/packages/block-platform/step-groups.js +78 -0
  59. package/packages/block-platform/steps.js +12 -8
  60. package/packages/block-platform/type-workspace.js +4 -4
  61. package/packages/block-platform/workflow-loop.js +4 -0
  62. package/packages/block-platform/workspace-input-readset.js +18 -4
  63. package/packages/block-sdk/admission.js +5 -17
  64. package/packages/block-sdk/agent-inputs.js +2 -0
  65. package/packages/block-sdk/agent-inspection.js +4 -2
  66. package/packages/block-sdk/browser-application-artifacts.js +1 -1
  67. package/packages/block-sdk/browser-application-execution.d.ts +2 -2
  68. package/packages/block-sdk/browser-application-execution.js +30 -12
  69. package/packages/block-sdk/browser-application-guest.d.ts +2 -0
  70. package/packages/block-sdk/browser-application-guest.js +16 -9
  71. package/packages/block-sdk/browser-application.d.ts +12 -4
  72. package/packages/block-sdk/browser-application.js +27 -13
  73. package/packages/block-sdk/builder-contract.js +25 -17
  74. package/packages/block-sdk/capabilities.js +38 -19
  75. package/packages/block-sdk/connected-app.js +26 -6
  76. package/packages/block-sdk/connection-adapters.js +19 -2
  77. package/packages/block-sdk/contracts-core.js +8 -0
  78. package/packages/block-sdk/contracts.js +9 -5
  79. package/packages/block-sdk/document-drafts.js +7 -1
  80. package/packages/block-sdk/document-store-binding.js +2 -1
  81. package/packages/block-sdk/file-bundle.js +4 -1
  82. package/packages/block-sdk/first-party-packages.js +18 -2
  83. package/packages/block-sdk/generation-catalog.js +3 -2
  84. package/packages/block-sdk/index.d.ts +20 -1
  85. package/packages/block-sdk/index.js +1 -0
  86. package/packages/block-sdk/native-application-package.d.ts +22 -0
  87. package/packages/block-sdk/native-application-package.js +77 -0
  88. package/packages/block-sdk/native_adapter.py +76 -0
  89. package/packages/block-sdk/package.json +1 -0
  90. package/packages/block-sdk/pipeline-runtime-host.d.ts +2 -0
  91. package/packages/block-sdk/project-flow-contract.js +2 -0
  92. package/packages/block-sdk/remix-policy.js +3 -1
  93. package/packages/block-sdk/resolver-admission.js +17 -0
  94. package/packages/block-sdk/runtime.js +4 -1
  95. package/packages/block-sdk/schema-fields.js +34 -2
  96. package/packages/block-sdk/semantic-relationships.js +14 -0
  97. package/packages/block-sdk/shared-controls.js +3 -1
  98. package/packages/block-sdk/shared-view.js +6 -1
  99. package/packages/block-sdk/source-rights.js +6 -5
  100. package/packages/block-sdk/testing.js +3 -0
  101. package/packages/design-system/theme.css +12 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stillmade-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "StillMade Block SDK, CLI and examples",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "type": "module",
@@ -37,6 +37,7 @@ import { validateProjectType } from '../block-platform/project-types.js';
37
37
  import { testProjectType } from '../block-platform/type-testing.js';
38
38
  import {previewProjectType} from '../block-platform/type-preview.js';
39
39
  import {assessBlockReadiness} from '../block-platform/block-readiness.js';
40
+ import {defineBrowserApplicationSource} from '../block-sdk/browser-application-execution.js';
40
41
  import { BUILTIN_BLOCKS } from '../block-platform/catalog.js';
41
42
  import { RECIPE_TYPE_EXAMPLE, typeWorkspaceSupport } from '../block-platform/type-workspace.js';
42
43
  import {PLATFORM_CHECK_POLICY,PLATFORM_CHECK_TARGETS,PLATFORM_CHECK_THEMES} from '../block-sdk/platform-checks.js';
@@ -206,7 +207,15 @@ export async function main(rawArgs) {
206
207
  await mkdir(path,{recursive:true});
207
208
  const names=['stillmade.block.json',...Object.keys(files).filter(name=>name!=='stillmade.block.json')];
208
209
  for(const name of names){await mkdir(dirname(join(path,name)),{recursive:true});await writeFile(join(path,name),files[name],{flag:'wx'});}
209
- return { created: path, sdkVersion: '0.1.0', template:template.name, runtime:template.runtime, files:names };
210
+ // Coding agents read command output: restate the readiness rule where they will see it.
211
+ return { created: path, sdkVersion: '0.1.0', template:template.name, runtime:template.runtime, files:names, next: 'Build the Block, then run `stillmade-block gate .`. Do not call the Block ready until the gate passes; report the status it prints.' };
212
+ }
213
+ if (command === 'gate') {
214
+ // Every new or edited Block in a folder must pass readiness before anyone calls it ready.
215
+ const {runReadyGate,gateMessage}=await import('./ready-gate.js');
216
+ const verdict=await runReadyGate({root:path,main});
217
+ if(!verdict.passed)throw Object.assign(new Error(gateMessage(verdict)),{code:'READINESS_GATE_FAILED',report:{results:verdict.results}});
218
+ return {passed:true,checked:verdict.checked,results:verdict.results.map(({package:name,status,limitations})=>({package:name,status,limitations}))};
210
219
  }
211
220
  if(command==='run')return main(['preview',...args.slice(1),...(mockRequested?['--mock']:[])]);
212
221
  if(command==='dev')return dev(path,args.slice(2));
@@ -226,6 +235,12 @@ export async function main(rawArgs) {
226
235
  if(command==='ready'){
227
236
  // The same Block builder contract readiness check StillMade runs before a release.
228
237
  if(pkg.stages)throw new Error('ready checks one Block package; run it on each Block in the Project Type.');
238
+ if(pkg.manifest?.runtime==='browser-application'){
239
+ // An original browser application is judged only by StillMade's isolated
240
+ // review; locally its exact source contract is all that can be checked.
241
+ const source=await defineBrowserApplicationSource(pkg);
242
+ throw Object.assign(new Error(`${source.manifest.name} passed the local browser-application source contract. Browser applications are judged only by StillMade's isolated review: import this exact package in the StillMade desktop app and run its review. It is not ready until that review passes.`),{code:'APPLICATION_REVIEW_REQUIRED'});
243
+ }
229
244
  const readiness=await assessBlockReadiness(pkg);
230
245
  if(['FAILED','BLOCKED BY PLATFORM CAPABILITY'].includes(readiness.status))throw Object.assign(new Error(`Block readiness: ${readiness.status}`),{report:readiness});
231
246
  return readiness;
@@ -18,6 +18,8 @@ Build and check
18
18
  types [folder] Write stillmade-env.d.ts: TypeScript types for the ports and globals
19
19
  inspect [folder] Show the manifest, runtime and operations
20
20
  ready [folder] The readiness check StillMade runs before a release
21
+ gate [folder] Run ready on every new or edited Block in a folder; a Block is not
22
+ ready until this passes
21
23
 
22
24
  Share
23
25
  pack [folder] [output] Test, then write the file to import in StillMade (Create, then Import)
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ // Readiness gate for Blocks built outside StillMade. It runs StillMade's own
3
+ // readiness check on every Block package in a folder that differs from what
4
+ // the SDK shipped, so a coding agent (Claude Code, Cursor, Codex...) cannot
5
+ // call a Block ready while that check fails. Results are cached by content
6
+ // digest, so an unchanged Block is never re-run.
7
+ import {createHash} from 'node:crypto';
8
+ import {readFile,readdir,writeFile,mkdir,lstat} from 'node:fs/promises';
9
+ import {realpathSync} from 'node:fs';
10
+ import {join,relative,resolve,sep} from 'node:path';
11
+ import {fileURLToPath} from 'node:url';
12
+
13
+ export const READY_GATE_STATE='.stillmade';
14
+ // Shipped in the SDK download: Claude Code and Cursor run the gate whenever the
15
+ // agent finishes a turn and hand any failures back to it.
16
+ export const AGENT_READY_GATE_HOOKS=Object.freeze({
17
+ claude:{hooks:{Stop:[{hooks:[{type:'command',command:'node "$CLAUDE_PROJECT_DIR/packages/block-cli/ready-gate.js" --claude',timeout:600}]}]}},
18
+ cursor:{version:1,hooks:{stop:[{command:'node packages/block-cli/ready-gate.js --cursor',timeout:600}]}}
19
+ });
20
+ export const SDK_BASELINE_FILE='.stillmade/sdk-baseline.json';
21
+ const SKIP=new Set(['node_modules','.git','.stillmade','.claude','.cursor','packages','dist','build','coverage']);
22
+ const PASSING=new Set(['PRODUCTION READY','READY WITH DOCUMENTED LIMITATIONS','PROJECT TYPE TESTS PASSED']);
23
+ const LIMITS=Object.freeze({directories:4000,depth:8,packageBytes:24*1024*1024});
24
+
25
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
26
+ const posix=path=>path.split(sep).join('/');
27
+
28
+ /** Digest of one package: a single file, or every file under a package folder. */
29
+ export async function packageDigest(path){
30
+ const info=await lstat(path);
31
+ if(!info.isDirectory())return sha256(await readFile(path));
32
+ const hash=createHash('sha256'),files=[];let bytes=0;
33
+ const walk=async(folder,prefix='')=>{for(const entry of await readdir(folder,{withFileTypes:true})){
34
+ if(entry.isSymbolicLink())throw new Error(`Package symlinks are unavailable: ${prefix}${entry.name}`);
35
+ const full=join(folder,entry.name);
36
+ if(entry.isDirectory())await walk(full,prefix+entry.name+'/');
37
+ else if(entry.isFile())files.push([prefix+entry.name,full]);
38
+ }};
39
+ await walk(path);
40
+ files.sort(([a],[b])=>a<b?-1:a>b?1:0);
41
+ for(const [name,full] of files){const data=await readFile(full);bytes+=data.length;if(bytes>LIMITS.packageBytes)throw new Error('Package exceeds the readiness gate size limit');hash.update(name+'\0'+data.length+'\0');hash.update(data);}
42
+ return hash.digest('hex');
43
+ }
44
+
45
+ /** Block and Project Type packages under root: folders holding stillmade.block.json
46
+ * or portable.json, and single .stillmade.json / .stillmade-block files. */
47
+ export async function findPackages(root){
48
+ const found=[];let visited=0;
49
+ const walk=async(folder,depth)=>{
50
+ if(++visited>LIMITS.directories||depth>LIMITS.depth)return;
51
+ const entries=await readdir(folder,{withFileTypes:true});
52
+ if(entries.some(entry=>entry.isFile()&&['stillmade.block.json','portable.json'].includes(entry.name))){found.push(folder);return;}
53
+ for(const entry of entries){
54
+ const full=join(folder,entry.name);
55
+ if(entry.isDirectory()&&!SKIP.has(entry.name)&&!entry.name.startsWith('.'))await walk(full,depth+1);
56
+ else if(entry.isFile()&&/\.(stillmade\.json|stillmade-block)$/i.test(entry.name))found.push(full);
57
+ }
58
+ };
59
+ await walk(root,0);
60
+ return found.sort();
61
+ }
62
+
63
+ /** Baseline of the packages an SDK download ships, so only new or edited
64
+ * Blocks are judged. Keys are POSIX paths relative to the SDK root. */
65
+ export async function sdkBaseline(root){
66
+ const entries={};
67
+ for(const path of await findPackages(root))entries[posix(relative(root,path))]=await packageDigest(path);
68
+ return {version:1,packages:entries};
69
+ }
70
+
71
+ /** Baseline for an in-memory archive map, computed by the same rules on disk. */
72
+ export async function sdkBaselineForFiles(files){
73
+ const {mkdtemp,rm}=await import('node:fs/promises'),{tmpdir}=await import('node:os'),{dirname}=await import('node:path');
74
+ const folder=await mkdtemp(join(tmpdir(),'stillmade-sdk-baseline-'));
75
+ try{
76
+ for(const [name,bytes] of Object.entries(files)){if(name.endsWith('/'))continue;const target=join(folder,...name.split('/'));await mkdir(dirname(target),{recursive:true});await writeFile(target,bytes);}
77
+ return await sdkBaseline(folder);
78
+ }finally{await rm(folder,{recursive:true,force:true});}
79
+ }
80
+
81
+ async function readJson(path,fallback){try{return JSON.parse(await readFile(path,'utf8'));}catch{return fallback;}}
82
+ const summarize=readiness=>(readiness?.items||[]).filter(item=>['failed','blocked'].includes(item.state)).slice(0,6).map(item=>`${item.id} ${item.title}: ${String(item.reason||'').slice(0,300)}`);
83
+
84
+ async function judge(path,main){
85
+ try{
86
+ const readiness=await main(['ready',path]);
87
+ return {ok:PASSING.has(readiness.status),status:readiness.status,failures:summarize(readiness),limitations:(readiness.limitations||[]).length};
88
+ }catch(error){
89
+ if(/^ready checks one Block package/.test(error.message)){
90
+ try{await main(['test',path]);return {ok:true,status:'PROJECT TYPE TESTS PASSED',failures:[]};}
91
+ catch(typeError){return {ok:false,status:'FAILED',failures:[String(typeError.message).slice(0,300)]};}
92
+ }
93
+ if(error.code==='APPLICATION_REVIEW_REQUIRED')return {ok:false,status:'NEEDS STILLMADE REVIEW',failures:[String(error.message).slice(0,400)]};
94
+ if(error.report?.status)return {ok:false,status:error.report.status,failures:summarize(error.report)};
95
+ return {ok:false,status:'FAILED',failures:[String(error.message).slice(0,300)]};
96
+ }
97
+ }
98
+
99
+ /** Judge every changed package. Never throws for a failing Block; returns a verdict. */
100
+ export async function runReadyGate({root=process.cwd(),main}={}){
101
+ root=resolve(root);
102
+ main??=(await import('./cli.js')).main;
103
+ const baseline=(await readJson(join(root,SDK_BASELINE_FILE),{packages:{}})).packages||{};
104
+ const cachePath=join(root,READY_GATE_STATE,'ready-cache.json'),cache=await readJson(cachePath,{version:1,entries:{}});
105
+ const results=[];
106
+ for(const path of await findPackages(root)){
107
+ const name=posix(relative(root,path)),digest=await packageDigest(path);
108
+ if(baseline[name]===digest)continue;
109
+ const cached=cache.entries[name];
110
+ const verdict=cached?.digest===digest?cached:{digest,...await judge(path,main),checkedAt:new Date().toISOString()};
111
+ cache.entries[name]=verdict;
112
+ results.push({package:name,...verdict});
113
+ }
114
+ await mkdir(join(root,READY_GATE_STATE),{recursive:true});
115
+ await writeFile(cachePath,JSON.stringify(cache,null,1)+'\n');
116
+ const failing=results.filter(result=>!result.ok);
117
+ return {checked:results.length,passed:failing.length===0,results,failing,signature:sha256(JSON.stringify(failing.map(result=>[result.package,result.digest])))};
118
+ }
119
+
120
+ export function gateMessage(verdict){
121
+ const lines=verdict.failing.map(result=>`- ${result.package} (${result.status}):\n${result.failures.map(failure=>` ${failure}`).join('\n')||' readiness check failed'}`);
122
+ return `StillMade readiness gate: ${verdict.failing.length} Block${verdict.failing.length===1?'':'s'} you changed ${verdict.failing.length===1?'is':'are'} not ready.\n${lines.join('\n')}\nFix these and re-run \`node packages/block-cli/cli.js ready <folder>\`. Do not tell the user a Block is ready, done or working while this gate fails. If you cannot fix it, say plainly that the Block is NOT ready and list these failures.`;
123
+ }
124
+
125
+ /** Hook entry. `agent` is claude, cursor or plain. Returns {exitCode, stdout}. */
126
+ export async function readyGateHook({agent='plain',input={},root,main}={}){
127
+ root=resolve(root||(agent==='cursor'?process.env.CURSOR_PROJECT_DIR||input.workspace_roots?.[0]:process.env.CLAUDE_PROJECT_DIR||input.cwd)||process.cwd());
128
+ const verdict=await runReadyGate({root,main});
129
+ const statePath=join(root,READY_GATE_STATE,'gate-state.json'),state=await readJson(statePath,{});
130
+ if(verdict.passed){
131
+ await writeFile(statePath,'{}\n');
132
+ return {exitCode:0,stdout:agent==='plain'?JSON.stringify({passed:true,checked:verdict.checked,results:verdict.results.map(({package:name,status})=>({package:name,status}))},null,2)+'\n':agent==='cursor'?'{}\n':''};
133
+ }
134
+ // An agent that already saw this exact failure set and made no change may
135
+ // stop (it has been told to report the Block as not ready); any edit re-arms.
136
+ const repeated=state.signature===verdict.signature;
137
+ if(agent==='claude'){
138
+ if(input.stop_hook_active&&repeated)return {exitCode:0,stdout:''};
139
+ await writeFile(statePath,JSON.stringify({signature:verdict.signature})+'\n');
140
+ return {exitCode:0,stdout:JSON.stringify({decision:'block',reason:gateMessage(verdict)})+'\n'};
141
+ }
142
+ if(agent==='cursor'){
143
+ if(Number(input.loop_count)>0&&repeated)return {exitCode:0,stdout:'{}\n'};
144
+ await writeFile(statePath,JSON.stringify({signature:verdict.signature})+'\n');
145
+ return {exitCode:0,stdout:JSON.stringify({followup_message:gateMessage(verdict)})+'\n'};
146
+ }
147
+ return {exitCode:1,stdout:gateMessage(verdict)+'\n'};
148
+ }
149
+
150
+ if(process.argv[1]&&(()=>{try{return realpathSync(process.argv[1])===fileURLToPath(import.meta.url);}catch{return false;}})()){
151
+ const agent=process.argv.includes('--claude')?'claude':process.argv.includes('--cursor')?'cursor':'plain';
152
+ let raw='';
153
+ if(agent!=='plain'&&!process.stdin.isTTY){for await(const chunk of process.stdin)raw+=chunk;}
154
+ let input={};try{input=raw.trim()?JSON.parse(raw):{};}catch{}
155
+ const rootArg=process.argv.slice(2).find(arg=>!arg.startsWith('--'));
156
+ readyGateHook({agent,input,root:rootArg}).then(({exitCode,stdout})=>{process.stdout.write(stdout);process.exitCode=exitCode;})
157
+ .catch(error=>{
158
+ const message=`StillMade readiness gate could not run: ${error.message}. Run \`node packages/block-cli/cli.js ready <folder>\` yourself before telling the user a Block is ready.`;
159
+ if(agent==='claude'&&!input.stop_hook_active)process.stdout.write(JSON.stringify({decision:'block',reason:message})+'\n');
160
+ else if(agent==='cursor'&&!(Number(input.loop_count)>0))process.stdout.write(JSON.stringify({followup_message:message})+'\n');
161
+ else process.stderr.write(message+'\n');
162
+ process.exitCode=agent==='plain'?1:0;
163
+ });
164
+ }
@@ -5,7 +5,7 @@
5
5
  // Project Type) and maps everything onto the builder contract's Definition of
6
6
  // Done. Status is derived, never asserted: an item that did not run is
7
7
  // "not-verified", so the report cannot claim more than was checked.
8
- import {BUILDER_CONTRACT,READINESS_STATUSES,GAP_CLASSES} from '../block-sdk/builder-contract.js';
8
+ import {BUILDER_CONTRACT,READINESS_STATUSES,GAP_CLASSES,BLOCK_NAME_MAX_LENGTH} from '../block-sdk/builder-contract.js';
9
9
  import {compatible,digest,stableStringify,validateBlockOutputs} from '../block-sdk/contracts.js';
10
10
  import {validatePackage} from '../block-sdk/testing.js';
11
11
  import {admitLocalPackage} from '../block-sdk/local-admission.js';
@@ -23,7 +23,7 @@ const OFFLINE_RUNTIMES=new Set(['recipe','javascript']);
23
23
  // Conditions of the checking machine, not of the Block: they leave items unverified, never failed.
24
24
  const ENVIRONMENT=new Set(['BUSY','READINESS_BUDGET','TIMEOUT_BUDGET']);
25
25
  const environmental=entry=>ENVIRONMENT.has(entry?.code)||/runners are busy/i.test(entry?.message||'');
26
- const HOST_REFUSALS=Object.freeze({RUNTIME_UNAVAILABLE:'E',DESKTOP_EXECUTION_REQUIRED:'E',LOCAL_WORKER_REQUIRED:'E'});
26
+ const HOST_REFUSALS=Object.freeze({RUNTIME_UNAVAILABLE:'E',DESKTOP_EXECUTION_REQUIRED:'E',LOCAL_WORKER_REQUIRED:'E',NATIVE_APPLICATION_RUNTIME_UNAVAILABLE:'E'});
27
27
  const TITLES=Object.fromEntries(BUILDER_CONTRACT.definitionOfDone.map(item=>[item.id,item.title]));
28
28
  const message=error=>String(error?.message||error).slice(0,300);
29
29
  const offline=pkg=>OFFLINE_RUNTIMES.has(pkg?.manifest?.runtime)&&!requiresDesktopExecution(pkg);
@@ -98,6 +98,7 @@ export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES
98
98
  }
99
99
  try{validatePackage(pkg);verified('DOD-02','Manifest, ports and fixtures validate against the SDK schema.');}
100
100
  catch(error){failed('DOD-02',message(error));return result();}
101
+ if(pkg.manifest.name.trim().length>BLOCK_NAME_MAX_LENGTH)failed('DOD-02',`Shorten the Block name to ${BLOCK_NAME_MAX_LENGTH} characters or fewer. Put the detail in its description.`);
101
102
 
102
103
  const manifest=pkg.manifest,sourceDigest=await digest(pkg),hosted=!offline(pkg);
103
104
  let report=admission;
@@ -115,7 +115,7 @@ export function connectedBlockInputs(node, nodes, edges, {context={},raw=false,a
115
115
  }
116
116
  if(!pin&&allowOmissions && source.type==='programmableBlockNode' && !source.data.blockStale && output.required===false && node.data?.blockPackage?.manifest?.inputs[name]?.required===false && source.data.blockOutputs && !Object.hasOwn(source.data.blockOutputs,output.name) && /^[a-f0-9]{64}$/.test(source.data.blockDigest || '') && source.data.blockLastRun?.digest===source.data.blockDigest && Number.isFinite(Date.parse(source.data.blockLastRun.at))){omissions[name]=true;continue;}
117
117
  inputs[name] = pinnedOutput.value;
118
- if (pinnedOutput.value === undefined){errors.push(pin?`The pinned result for ${name} is unavailable. Choose a recorded result again.`:`Run the upstream block for ${name} first`);continue;}
118
+ if (pinnedOutput.value === undefined){errors.push(pin?`The pinned result for ${name} is unavailable. Choose a recorded result again.`:`Run ${source?.data?.label||source?.data?.blockPackage?.manifest?.name||'the Step before this one'} first.`);continue;}
119
119
  if(node.type==='programmableBlockNode' && !raw){
120
120
  try{inputs[name]=adaptConnectionValue(pinnedOutput,node.data.blockPackage.manifest.inputs[name],pinnedOutput.value,{context,semantics,permissions:node.data.blockPackage.manifest.permissions?.project || [],...(Object.hasOwn(edge.data || {},'stillmadeAdapter')?{adapter:edge.data.stillmadeAdapter}:{})});}
121
121
  catch(error){inputs[name]=undefined;errors.push(error.message);}
@@ -1,5 +1,5 @@
1
1
  import {REVENUE_REVIEW_PROJECT_TYPES} from './revenue-review-project-types.js';
2
- import {PRODUCT_AD_VARIANTS_PROJECT_TYPE,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE} from './product-ad-variants-project-type.js';
2
+ import {PRODUCT_AD_VARIANTS_PROJECT_TYPE,PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE} from './product-ad-variants-project-type.js';
3
3
  import {FIRST_PARTY_SDK_PACKAGES} from '../block-sdk/first-party-packages.js';
4
4
  import {FIRST_REVENUE_PROJECT_TYPES} from './first-revenue-project-types.js';
5
5
  import { SDK_VERSION, ROOT_CHAT_ID } from '../block-sdk/contracts.js';
@@ -86,7 +86,7 @@ HOST_WORKSPACE_BLOCKS.splice(HOST_WORKSPACE_BLOCKS.findIndex(block=>block.manife
86
86
  // Promote a portable workspace after its accepted production MVP and rendered
87
87
  // acceptance are complete; advanced host extensions can remain on old exact pins. Exact portable releases remain usable as normal
88
88
  // Steps before promotion; this map only controls the default production route.
89
- export const CURRENT_PORTABLE_WORKSPACE_VERSIONS=Object.freeze({'sm.chat':'2.0.3','sm.script':'2.1.4','sm.voiceover':'2.1.4','sm.shot-plan':'2.1.3','sm.blueprint':'2.1.9','sm.pipeline':'2.2.5','sm.video-assets':'2.1.4','sm.panel-strip':'2.3.7','sm.board-editor':'2.2.10','sm.animated-scene':'2.3.8','sm.editor':'2.2.11'});
89
+ export const CURRENT_PORTABLE_WORKSPACE_VERSIONS=Object.freeze({'sm.chat':'2.0.3','sm.script':'2.1.4','sm.voiceover':'2.1.6','sm.shot-plan':'2.1.3','sm.blueprint':'2.1.9','sm.pipeline':'2.2.5','sm.video-assets':'2.1.4','sm.panel-strip':'2.3.7','sm.board-editor':'2.2.10','sm.animated-scene':'2.3.8','sm.editor':'2.2.11'});
90
90
  export const WORKSPACE_BLOCKS=HOST_WORKSPACE_BLOCKS.map(block=>{
91
91
  const version=CURRENT_PORTABLE_WORKSPACE_VERSIONS[block.manifest.id];
92
92
  if(!version)return block;
@@ -122,7 +122,9 @@ const GENERATION_HOST_RELEASES=Object.freeze(['sm.generate-image','sm.generate-v
122
122
  const semverParts=version=>String(version).split(/[.-]/).slice(0,3).map(value=>Number(value)||0);
123
123
  const newer=(left,right)=>{const a=semverParts(left),b=semverParts(right);for(let i=0;i<3;i++){if(a[i]!==b[i])return a[i]>b[i];}return false;};
124
124
  const currentPortableById=new Map();
125
- for(const block of SDK_BUILTIN_BLOCKS){const prior=currentPortableById.get(block.manifest.id);if(!prior||newer(block.manifest.version,prior.manifest.version))currentPortableById.set(block.manifest.id,block);}
125
+ // An unset app connector is a project placeholder, not something to find or install.
126
+ export const UNDISCOVERABLE_SDK_BLOCKS=new Set(['sm.connect-app']);
127
+ for(const block of SDK_BUILTIN_BLOCKS){if(UNDISCOVERABLE_SDK_BLOCKS.has(block.manifest.id))continue;const prior=currentPortableById.get(block.manifest.id);if(!prior||newer(block.manifest.version,prior.manifest.version))currentPortableById.set(block.manifest.id,block);}
126
128
  export const DISCOVERABLE_SDK_BUILTIN_BLOCKS = Object.freeze([...currentPortableById.values()]);
127
129
  export const DISCOVERABLE_BLOCKS = [STARTING_CHAT_BLOCK,...WORKSPACE_BLOCKS,...DISCOVERABLE_SDK_BUILTIN_BLOCKS.filter(block=>!WORKSPACE_BLOCKS.some(current=>current.manifest.id===block.manifest.id&&current.manifest.version===block.manifest.version))];
128
130
  export const BUILTIN_BLOCKS = [STARTING_CHAT_BLOCK,...WORKSPACE_BLOCKS,...HISTORICAL_WORKSPACE_BLOCKS,DESCRIBE_MEDIA_HOST_1_1,YOUTUBE_HOST_1_1,...GENERATION_HOST_RELEASES,...SDK_BUILTIN_BLOCKS.filter(block=>!WORKSPACE_BLOCKS.some(current=>current.manifest.id===block.manifest.id&&current.manifest.version===block.manifest.version)),...LEGACY_BLOCKS];
@@ -154,7 +156,7 @@ const SHARED_WORKSPACE_TYPES = currentBuiltinTypes('1.12.0','2.2.3',SCRIPT_WRITE
154
156
  const PRIOR_CURRENT_TYPES = currentBuiltinTypes('1.13.0','2.2.3',SCRIPT_WRITER_BLOCK.manifest.version,{'blueprint':'2.1.8','panel-strip':'2.3.6','board-editor':'2.2.9','animated-scene':'2.3.7','editor':'2.2.8'});
155
157
  const PREVIOUS_PORTABLE_TYPES=currentBuiltinTypes('1.14.0','2.2.4',SCRIPT_WRITER_BLOCK.manifest.version,{'blueprint':'2.1.8','panel-strip':'2.3.6','board-editor':'2.2.9','animated-scene':'2.3.7','editor':'2.2.8'});
156
158
  export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.15.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'2.2.9'}),...REVENUE_REVIEW_PROJECT_TYPES,PRODUCT_AD_VARIANTS_PROJECT_TYPE,...FIRST_REVENUE_PROJECT_TYPES];
157
- const HISTORICAL_CURRENT_TYPES=[PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE,...PREVIOUS_PORTABLE_TYPES,...PRIOR_CURRENT_TYPES,...SHARED_WORKSPACE_TYPES,...SHARED_CANVAS_TYPES,...SHARED_BLUEPRINT_TYPES,...PRIOR_SHARED_TYPES,...MEDIA_LIBRARY_TYPES,...EDITOR_AUDIO_TYPES,...PLAYABLE_EDITOR_TYPES,...ADVANCED_PORTABLE_TYPES,...PORTABLE_MVP_TYPES,...PRE_PROMOTION_CURRENT_TYPES,...PREVIOUS_BUILTIN_TYPES.filter(type=>flows[type.legacyMode].includes('shot-plan')),...currentBuiltinTypes('1.1.0','1.1.0','1.1.0',PRIOR_HOST_WORKSPACE_VERSIONS)];
159
+ const HISTORICAL_CURRENT_TYPES=[PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE,...PREVIOUS_PORTABLE_TYPES,...PRIOR_CURRENT_TYPES,...SHARED_WORKSPACE_TYPES,...SHARED_CANVAS_TYPES,...SHARED_BLUEPRINT_TYPES,...PRIOR_SHARED_TYPES,...MEDIA_LIBRARY_TYPES,...EDITOR_AUDIO_TYPES,...PLAYABLE_EDITOR_TYPES,...ADVANCED_PORTABLE_TYPES,...PORTABLE_MVP_TYPES,...PRE_PROMOTION_CURRENT_TYPES,...PREVIOUS_BUILTIN_TYPES.filter(type=>flows[type.legacyMode].includes('shot-plan')),...currentBuiltinTypes('1.1.0','1.1.0','1.1.0',PRIOR_HOST_WORKSPACE_VERSIONS)];
158
160
  export const BUILTIN_TYPES = [...CURRENT_BUILTIN_TYPES,...HISTORICAL_CURRENT_TYPES,...LEGACY_TYPES];
159
161
  export const workspaceBlock = (id,version)=>DISCOVERABLE_BLOCKS.find(block=>block.manifest.id===id && (!version || block.manifest.version===version));
160
162
  export const discoverableBuiltin = item=>!!workspaceBlock((item.manifest || item).id,(item.manifest || item).version);
@@ -1,6 +1,10 @@
1
1
  import {auditProjectTypeConnections} from './project-type-composer.js';
2
2
 
3
- export function projectTypeRequirementsForChat(type,catalog=[],{currentStage,contextValues=[]}={}) {
3
+ // `stepInputs` holds what each Step already has typed in ({stageId:{key:value}}):
4
+ // an input a person supplies that is already there reads as provided, not as
5
+ // still needing them.
6
+ const supplied=value=>value!==undefined&&value!==null&&(typeof value!=='string'||value.trim()!=='')&&(!Array.isArray(value)||value.length>0);
7
+ export function projectTypeRequirementsForChat(type,catalog=[],{currentStage,contextValues=[],stepInputs={}}={}) {
4
8
  if(!type)return undefined;
5
9
  const values=contextValues.map(value=>value.source?.kind==='block'?{...value,source:{...value.source,stageId:value.source.stepId || value.source.stageId}}:value);
6
10
  const audit=auditProjectTypeConnections(type,catalog,{contextValues:values}),enabled=audit.stages.filter(step=>!step.paused),steps=[];
@@ -13,8 +17,9 @@ export function projectTypeRequirementsForChat(type,catalog=[],{currentStage,con
13
17
  const issues=blocking.slice(0,10).map(issue=>({stageId:issue.stageId?String(issue.stageId).slice(0,160):null,message:String(issue.reason || issue.message).slice(0,240)}));
14
18
  let size=4500+JSON.stringify(issues).length,metadataSize=0,truncated=blocking.length>issues.length || blocking.some(issue=>String(issue.reason || issue.message).length>240 || String(issue.stageId || '').length>160);
15
19
  for(const step of relevant) {
16
- const row={id:String(step.id).slice(0,160),label:String(step.label).slice(0,160),upcoming:currentIndex<0 || enabled.indexOf(step)>=currentIndex,requirements:step.requirements.slice(0,12).map(decision=>({key:String(decision.key || decision.requirement.key).slice(0,160),description:String(decision.requirement.description || '').slice(0,200),semantic:String(decision.requirement.semantic || '').slice(0,160),type:decision.requirement.type,required:decision.requirement.required,state:decision.state,reason:String(decision.reason || '').slice(0,240),source:summarizeSource(decision.selected),candidates:candidatesFor(decision).slice(0,3).map(summarizeSource)}))};
17
- row.state=step.state;
20
+ const row={id:String(step.id).slice(0,160),label:String(step.label).slice(0,160),upcoming:currentIndex<0 || enabled.indexOf(step)>=currentIndex,requirements:step.requirements.slice(0,12).map(decision=>({key:String(decision.key || decision.requirement.key).slice(0,160),description:String(decision.requirement.description || '').slice(0,200),semantic:String(decision.requirement.semantic || '').slice(0,160),type:decision.requirement.type,required:decision.requirement.required,...(decision.state==='needs-user-input'&&supplied(stepInputs[step.id]?.[decision.key || decision.requirement.key])?{state:'provided',reason:'Set on this Step.'}:{state:decision.state,reason:String(decision.reason || '').slice(0,240)}),source:summarizeSource(decision.selected),candidates:candidatesFor(decision).slice(0,3).map(summarizeSource)}))};
21
+ // Once the inputs a person supplies are all there, the Step no longer waits on them.
22
+ row.state=step.state==='needs-user-input'&&!row.requirements.some(item=>item.state==='needs-user-input'&&item.required)?['chat','auto-connected'].find(state=>row.requirements.some(item=>item.state===state&&(item.required||state==='auto-connected')))||'ready':step.state;
18
23
  const issue=step.contractIssue?.message || step.connectionIssues?.[0]?.message;
19
24
  if(issue)row.issue=String(issue).slice(0,240);
20
25
  let length=JSON.stringify(row).length;
@@ -61,7 +61,7 @@ export function mergeLiveDocument(local, remote, scope = 'editor') {
61
61
  return reconcileShared(local, result);
62
62
  }
63
63
 
64
- export function validateLiveDocument(document, scope = 'editor', {experimentalBrowser} = {}) {
64
+ export function validateLiveDocument(document, scope = 'editor', {experimentalBrowser,experimentalNative} = {}) {
65
65
  safeDocument(document);
66
66
  assert(plain(document), 'DOCUMENT', 'Shared workspace must be an object');
67
67
  const collection = (items, name, maximum) => {
@@ -101,7 +101,7 @@ export function validateLiveDocument(document, scope = 'editor', {experimentalBr
101
101
  }
102
102
  }
103
103
  if (document.projectType !== undefined) {
104
- const result=validateProjectType(document.projectType,BUILTIN_BLOCKS,{experimentalBrowser});
104
+ const result=validateProjectType(document.projectType,BUILTIN_BLOCKS,{experimentalBrowser,experimentalNative});
105
105
  assert(result.valid,'DOCUMENT',`Invalid shared Project Type: ${result.errors.map(error=>error.message).join(' ')}`);
106
106
  }
107
107
  collection(document.nodes,'nodes',5000); collection(document.edges,'edges',10000);
@@ -146,13 +146,13 @@ export function validateLiveDocument(document, scope = 'editor', {experimentalBr
146
146
  // validates already hydrated exact source only; no serializable document flag
147
147
  // opens admission, reads artifacts, verifies retained records or grants access.
148
148
  // Existing server save routes do not supply it and remain closed to this runtime.
149
- export async function validateCanvasWorkflowPair(document,{previous,complete=false,experimentalBrowser}={}) {
150
- validateLiveDocument(document,'canvas',{experimentalBrowser});
149
+ export async function validateCanvasWorkflowPair(document,{previous,complete=false,experimentalBrowser,experimentalNative}={}) {
150
+ validateLiveDocument(document,'canvas',{experimentalBrowser,experimentalNative});
151
151
  if(document.projectType === undefined) {assert(!complete,'DOCUMENT','A workflow handoff requires its Project Type');return document;}
152
152
  const type=document.projectType;
153
153
  for(const [index,old] of (previous?.projectType?.packages||[]).entries())
154
154
  assert(stableStringify(type.packages?.[index])===stableStringify(old),'VERSION','A saved package changed or moved. Keep existing package slots unchanged and append any newly reviewed release.');
155
- const {canvas}=await compileTypeWorkspace(type,{experimentalBrowser}),expected=new Map(canvas.nodes.map(node=>[node.id,node])),actual=new Map(document.nodes.map(node=>[node.id,node]));
155
+ const {canvas}=await compileTypeWorkspace(type,{experimentalBrowser,experimentalNative}),expected=new Map(canvas.nodes.map(node=>[node.id,node])),actual=new Map(document.nodes.map(node=>[node.id,node]));
156
156
  for(const [id,node] of expected) {
157
157
  const current=actual.get(id);
158
158
  assert(current || !complete,'CONFLICT','The workflow is missing a required Block instance. Reload the project before applying it.');
@@ -1,5 +1,5 @@
1
1
  import mediaImport from '../../blocks/native/sm.import-media/versions/2.0.2/package.js';
2
- import photoReelComposer from '../../blocks/photo-reel-composer/versions/1.0.0/package.js';
2
+ import photoReelComposer from '../../blocks/photo-reel-composer/versions/1.0.1/package.js';
3
3
  import editor from '../../blocks/native/sm.editor/versions/2.2.9/package.js';
4
4
 
5
5
  // Each Type carries its exact executable dependencies. The shared host supplies
@@ -8,7 +8,7 @@ import editor from '../../blocks/native/sm.editor/versions/2.2.9/package.js';
8
8
  // than collecting onboarding answers that the Block cannot consume.
9
9
  function photoReelType({id,name,description,brief,purpose,importLabel}) {
10
10
  return Object.freeze({
11
- schemaVersion:1,id,version:'1.0.1',name,description,license:'MIT',startingChat:'native',
11
+ schemaVersion:1,id,version:'1.0.2',name,description,license:'MIT',startingChat:'native',
12
12
  summary:{text:description,origin:'human',generation:'authored',status:'ready'},
13
13
  defaults:{aspectRatio:'9:16',brief},
14
14
  onboarding:[],
@@ -244,7 +244,7 @@ export function inspectProjectFlow(type,canvas={nodes:[],edges:[]},{projectId=nu
244
244
  const appliedActivity=last?.manualSelection?{applied:true,at:last.at||null,runId:null,...(selectedManually?{selection:selectedManually}:{})}:last?{applied:true,at:last.at||null,runId:last.executionRunId||last.semanticRunId||last.segmentRunId||last.runId||null,skipped:last.skipped===true&&!last.failureRecovery,execution:executionEvidence(last,{conditional:!!stage.condition,outputs:node?.data?.blockOutputs||{}})}:null;
245
245
  const condition=stage.condition?{inputKey:stage.condition.input,inputLabel:manifest?.inputs?.[stage.condition.input]?.description||stage.condition.input,path:stage.condition.path.slice(0,8),operator:stage.condition.operator,outcome:last?(last.skipped===true&&!last.failureRecovery?'skipped':'ran'):'unknown'}:null;
246
246
  const latestFailure=(node?.data?.blockFailedAttempts||[]).slice(-1)[0],failureIsLatest=typeof latestFailure?.runId==='string'&&Number.isFinite(Date.parse(latestFailure.at))&&(!last?.at||latestFailure.at>last.at)&&!failureRecovered(latestFailure),failedActivity=failureIsLatest?{runId:latestFailure.runId.slice(0,160),at:latestFailure.at,code:failureCode(latestFailure),durationMs:Number.isSafeInteger(latestFailure.diagnostic?.durationMs)?Math.max(0,Math.min(86400000,latestFailure.diagnostic.durationMs)):null,...Object.fromEntries(['attempt','batchIndex','batchCount'].filter(key=>failedCounter(latestFailure[key])).map(key=>[key,latestFailure[key]]))}:null;
247
- return {id:row.id,nodeId:stageNodeId(stage),label:row.label,blockId:stage.blockId,version:stage.version,transformation:String(manifest?.description||'').slice(0,500),enabled:stage.enabled!==false,state:node?.data?.blockPrivacy?'restricted':active?'working':row.paused?'paused':node?.data?.blockStale||last&&last.digest!==node?.data?.blockDigest?'needs-updating':failureIsLatest?(failureCode(latestFailure)==='EXECUTION_FAILED'?'failed':'needs-recovery'):last?.skipped===true&&!last?.failureRecovery&&stage.condition?'skipped':last?'finished':row.state==='broken'||row.state==='needs-user-input'?'needs-choice':'waiting',...(condition?{condition}:{}),inputs:inputs.map(input=>({...input,valueType:manifest?.inputs?.[input.key]?.type||row.requirements.find(requirement=>requirement.key===input.key)?.requirement?.type||null})),outputs,unusedResults,unusedResultsRevision,unusedResultsCount,unusedResultsOffset,unusedResultsLimit,unusedResultsNextOffset,...(retainedPage?.candidateId?{inspectedRetainedResult}:{}),usedInputs,activity:active?{...(appliedActivity||{}),active,activeRuns,activeRunsOmitted:Math.max(0,(runsByPlacement.get(row.id)||[]).length-activeRuns.length)}:failedActivity?{...(appliedActivity||{}),failedAttempt:failedActivity}:appliedActivity,history:recentHistory,historyComplete,retainedLoopRuns:retainedWorkflowLoopSummaries(canvas,row.id)};
247
+ return {id:row.id,nodeId:stageNodeId(stage),label:row.label,blockId:stage.blockId,version:stage.version,...(manifest?.permissions?.capabilities?.includes('connection.execute')?{connector:true}:{}),transformation:String(manifest?.description||'').slice(0,500),enabled:stage.enabled!==false,state:node?.data?.blockPrivacy?'restricted':active?'working':row.paused?'paused':node?.data?.blockStale||last&&last.digest!==node?.data?.blockDigest?'needs-updating':failureIsLatest?(failureCode(latestFailure)==='EXECUTION_FAILED'?'failed':'needs-recovery'):last?.skipped===true&&!last?.failureRecovery&&stage.condition?'skipped':last?'finished':row.state==='broken'||row.state==='needs-user-input'?'needs-choice':'waiting',...(condition?{condition}:{}),inputs:inputs.map(input=>({...input,valueType:manifest?.inputs?.[input.key]?.type||row.requirements.find(requirement=>requirement.key===input.key)?.requirement?.type||null})),outputs,unusedResults,unusedResultsRevision,unusedResultsCount,unusedResultsOffset,unusedResultsLimit,unusedResultsNextOffset,...(retainedPage?.candidateId?{inspectedRetainedResult}:{}),usedInputs,activity:active?{...(appliedActivity||{}),active,activeRuns,activeRunsOmitted:Math.max(0,(runsByPlacement.get(row.id)||[]).length-activeRuns.length)}:failedActivity?{...(appliedActivity||{}),failedAttempt:failedActivity}:appliedActivity,history:recentHistory,historyComplete,retainedLoopRuns:retainedWorkflowLoopSummaries(canvas,row.id)};
248
248
  });
249
249
  if(retainedResultPage){const placement=placements.find(item=>item.id===retainedResultPage.placementId);if(!placement||placement.unusedResultsRevision!==retainedResultPage.revision||retainedResultPage.operation==='list'&&retainedResultPage.offset>placement.unusedResultsCount||retainedResultPage.candidateId&&!placement.inspectedRetainedResult)throw new Error('This retained result list or exact candidate changed. Inspect Result again.');}
250
250
  if(receiptInputPage){const entry=placements.find(item=>item.id===receiptInputPage.placementId)?.history?.find(item=>projectFlowRunReceiptIdentity(item)===receiptInputPage.receiptId);if(!entry||entry.inputsRevision!==receiptInputPage.revision||!entry.inputs||receiptInputPage.offset>entry.inputsCount)throw new Error('This retained receipt or its captured inputs changed. Inspect the run again.');}
@@ -271,7 +271,7 @@ export function inspectProjectFlow(type,canvas={nodes:[],edges:[]},{projectId=nu
271
271
  if(placement.condition)conditionalLabels.push(`Conditional Block · last recorded ${placement.condition.outcome==='unknown'?'not run':placement.condition.outcome}`);
272
272
  return {from:input.sourcePlacementId,outputKey:input.sourceOutputKey,to:placement.id,inputKey:input.key,state:input.state,materialized:input.materialized===true,sourceRunId:input.pinRunId||sourceOutput?.runId||null,adapter:input.adapter,...(conditionalLabels.length?{conditionalLabel:conditionalLabels.join(' · ')}:{})};
273
273
  }));
274
- return projectFlowSnapshot({contractVersion:PROJECT_FLOW_CONTRACT_VERSION,projectId,definition:{id:type.id,version:type.version},acceptanceCriteria:(type.acceptanceCriteria||[]).map(item=>({...item,verified:false})),goalSourceMaterials:validateGoalSourceMaterials(type),goalSourceInputs:currentGoalSourceInputs(type,[...BUILTIN_BLOCKS,...catalog]),placements,edges,issues:audit.issues.slice(0,50).map(issue=>({placementId:issue.stageId||null,inputKey:issue.key||null,message:issue.message||issue.reason||'Connection needs attention'})),partial:audit.requirementsInspected===false});
274
+ return projectFlowSnapshot({contractVersion:PROJECT_FLOW_CONTRACT_VERSION,projectId,definition:{id:type.id,version:type.version},...(type.groups?.length?{jobs:type.groups.map(group=>({id:group.id,label:group.label,parts:[...group.stages]}))}:{}),acceptanceCriteria:(type.acceptanceCriteria||[]).map(item=>({...item,verified:false})),goalSourceMaterials:validateGoalSourceMaterials(type),goalSourceInputs:currentGoalSourceInputs(type,[...BUILTIN_BLOCKS,...catalog]),placements,edges,issues:audit.issues.filter(issue=>!(issue.state==='needs-user-input'&&placements.find(placement=>placement.id===issue.stageId)?.inputs?.some(input=>input.key===issue.key&&input.state==='ready'))).slice(0,50).map(issue=>({placementId:issue.stageId||null,inputKey:issue.key||null,...(issue.state?{state:issue.state}:{}),message:issue.message||issue.reason||'Connection needs attention'})),partial:audit.requirementsInspected===false});
275
275
  }
276
276
 
277
277
  // A read-only explanation of the current handoff. Each hop is an inspected
@@ -0,0 +1,20 @@
1
+ import {assert,plain,safeDocument,stableStringify} from '../block-sdk/contracts.js';
2
+ import {defineNativeApplicationPackage} from '../block-sdk/native-application-package.js';
3
+
4
+ // Source structure only. This opaque capability grants no execution, source
5
+ // access, installation, result authentication, state commit or runtime setup.
6
+ const contexts=new WeakMap();
7
+ export const isNativeApplicationPackage=value=>value?.manifest?.runtime==='native-application'&&Object.hasOwn(value,'nativeApplication');
8
+ export function createNativeApplicationValidationContext(options){
9
+ assert(plain(options)&&Reflect.ownKeys(options).length===1&&Object.hasOwn(options,'sources')&&Object.hasOwn(Object.getOwnPropertyDescriptor(options,'sources'),'value'),'NATIVE_APPLICATION_CONTEXT','Supply only exact native sources for structure validation.');
10
+ safeDocument(options.sources);
11
+ assert(Array.isArray(options.sources)&&options.sources.length>0&&options.sources.length<=100,'NATIVE_APPLICATION_CONTEXT','Prevalidate 1–100 exact native application sources.');
12
+ const entries=new Map();
13
+ for(const value of options.sources){const source=defineNativeApplicationPackage(value),key=stableStringify(source);assert(!entries.has(key),'NATIVE_APPLICATION_CONTEXT','Duplicate exact native source.');entries.set(key,source);}
14
+ const context=Object.freeze(Object.create(null));contexts.set(context,entries);return context;
15
+ }
16
+ export function nativeApplicationValidationSource(context,value){
17
+ const entries=contexts.get(context);
18
+ assert(entries,'NATIVE_APPLICATION_RUNTIME_UNAVAILABLE','Native source structure requires an opaque trusted validation context; package JSON cannot grant access or admission.');
19
+ const source=entries.get(stableStringify(value));assert(source,'NATIVE_APPLICATION_PIN','Native source differs from this host-prevalidated package.');return source;
20
+ }
@@ -0,0 +1,67 @@
1
+ // What people (and Chat, which speaks to them) read about a StillMade Block.
2
+ // Published manifests keep their exact technical descriptions; releases are
3
+ // immutable, so the plain wording for first-party Blocks lives here, by Block id.
4
+ // A Block whose own description is already plain needs no entry.
5
+ export const PLAIN_BLOCK_DESCRIPTIONS=Object.freeze({
6
+ 'sm.script':'Write and edit a script with AI. Set a length, add notes, and go back to earlier versions.',
7
+ 'sm.shot-plan':'Turn a script into a list of timed shots, then edit what each shot shows and how long it lasts.',
8
+ 'sm.blueprint':'Plan your video’s scenes, characters and look in one place, and lock the parts you want to keep.',
9
+ 'sm.pipeline':'Lay out your project on a canvas and connect its pieces, keeping your Steps and content.',
10
+ 'sm.video-assets':'Choose which images and clips go into your video, in order, from everything the project has made or imported.',
11
+ 'sm.editor':'The full video editor: text and titles, captions, transitions, music, effects and export are built in.',
12
+ 'sm.panel-strip':'Turn a script into pictures timed to the voice, then edit scenes, images and transitions.',
13
+ 'sm.board-editor':'Make text and pictures move on a whiteboard in time with a voice, then send it to the Editor.',
14
+ 'sm.animated-scene':'Build animated scenes from your images and characters, preview them, then send them to the Editor.',
15
+ 'stillmade.product-ad-variants':'Make three short product ads with different opening hooks from your own footage. Nothing is invented.',
16
+ 'sm.youtube':'Upload a finished video to your connected YouTube channel.',
17
+ 'sm.stock-search':'Search free-to-use stock photos and videos and pick one.',
18
+ 'sm.export-video':'Turn your timeline into a video file you can download and keep in the project.',
19
+ 'sm.export-audio':'Turn your timeline into an audio file you can download and keep in the project.',
20
+ 'sm.motion-graphics':'Add animated titles, text, charts and transitions to your timeline. You see the change before it applies.',
21
+ 'sm.score':'Put music, sound effects and voiceover from your project on the timeline. You see the change before it applies.',
22
+ 'stillmade.short-form-finish':'Turn your clips and subtitles into a captioned short video you can still edit.',
23
+ 'stillmade.occasion-story':'Arrange photos and video messages into a birthday, anniversary or farewell video you can still edit.',
24
+ 'stillmade.release-visuals':'Make a social teaser or lyric clip for a song from its cover art and recording.',
25
+ 'stillmade.photo-reel-composer':'Arrange property or product photos and your text into a 20–30 second reel. No AI images.',
26
+ 'sm.import-media':'Upload or choose your own photos, images, video and audio, and put them in order for the next Step.',
27
+ 'sm.chat':'Work out a script by chatting with AI.',
28
+ 'sm.rough-cut':'Put your chosen clips together into a first cut you can review.',
29
+ 'sm.final-edit':'Have AI suggest a final polish for a finished timeline. You review the changes before they apply.',
30
+ 'sm.edit-assistant':'Ask for a timeline change in your own words, such as trimming a section. You review it before it applies.',
31
+ 'sm.grade':'Change the colors of chosen clips with ready-made looks or simple sliders. Your originals stay as they are.',
32
+ 'sm.transition':'Pick transitions between clips, with their timing and sounds, then apply them in the Editor.',
33
+ 'sm.quality-audit':'Check your images and clips for quality problems before you export.',
34
+ 'sm.compare':'Compare two images with a slider and keep the one you prefer.',
35
+ 'sm.export-timeline':'Download your timeline to open in another editor, with or without its media.',
36
+ 'sm.captions':'Turn a transcript or text into captions with word highlighting, then add them to the timeline.',
37
+ 'sm.thumbnail':'Make a square thumbnail image from a short description.',
38
+ 'sm.write-summary':'Turn data or notes, such as an app’s results, into a short summary in plain words.',
39
+ 'sm.generate-video':'Make a short video clip from a description.',
40
+ 'sm.character':'Describe a character once so your images and videos keep them the same.',
41
+ 'sm.location':'Describe a place once so your images and videos keep it the same.',
42
+ 'sm.style':'Set a look, with colors, lighting and camera style, for your images to follow.',
43
+ 'sm.research':'Collect your notes and a source link into research you can reuse. It does not browse the web.',
44
+ 'sm.image-editor':'Adjust an image’s brightness and contrast.',
45
+ 'sm.describe-media':'Have AI describe what is in an image.',
46
+ 'sm.transcribe':'Turn speech in an audio file into text, with a time for each word.',
47
+ 'sm.shot-plan-assistant':'Have AI suggest a shot list from your script, keeping the shots you already have.',
48
+ 'sm.animated-scene-assistant':'Have AI suggest how an animated scene is laid out and how it moves.',
49
+ 'sm.blueprint-assistant':'Have AI suggest edits to your video plan.',
50
+ 'sm.whiteboard-assistant':'Have AI build whiteboard boards, timing and motion from what you ask for.',
51
+ });
52
+
53
+ // Words that describe how StillMade works inside rather than what a person gets.
54
+ export const INTERNAL_WORDS=/\b(exact|bounded|portable|capabilit(y|ies)|fulfillment|source-bound|canonical|typed|stored|production prompt|approved|retained|semantic|workspace|handoff|sandbox|dossier)\b/i;
55
+
56
+ /** The description to show for a Block: a first-party Block's plain wording, else its own. */
57
+ // An app task's own description is written for developers (field rules, other
58
+ // task ids); people see its first sentence only.
59
+ export function firstSentence(text,max=200){
60
+ const plain=String(text||'').replace(/\s+/g,' ').trim(),end=plain.search(/[.!?](\s|$)/);
61
+ const sentence=end>=0?plain.slice(0,end+1):plain;
62
+ return sentence.length>max?`${sentence.slice(0,max-1).trimEnd()}…`:sentence;
63
+ }
64
+ export function plainBlockDescription(manifest){
65
+ if(PLAIN_BLOCK_DESCRIPTIONS[manifest?.id])return PLAIN_BLOCK_DESCRIPTIONS[manifest.id];
66
+ return /^connected\./.test(String(manifest?.id||''))?firstSentence(manifest?.description):manifest?.description||'';
67
+ }
@@ -1,11 +1,12 @@
1
- import shortForm from '../../blocks/short-form-finish/versions/1.0.1/package.js';
1
+ import shortFormV101 from '../../blocks/short-form-finish/versions/1.0.1/package.js';
2
+ import shortFormV103 from '../../blocks/short-form-finish/versions/1.0.3/package.js';
2
3
  import variants from '../../blocks/product-ad-variants/versions/1.0.0/package.js';
3
4
  import editorV229 from '../../blocks/native/sm.editor/versions/2.2.9/package.js';
4
5
  import editorV2210 from '../../blocks/native/sm.editor/versions/2.2.10/package.js';
5
6
  import editorV2211 from '../../blocks/native/sm.editor/versions/2.2.11/package.js';
6
7
 
7
8
  const description='Make three distinct approved hook and real-footage opening variants from one supplied product demonstration, with a separate editable export for each.';
8
- const productAdVariantsType=(version,editor)=>Object.freeze({
9
+ const productAdVariantsType=(version,editor,shortForm=shortFormV101)=>Object.freeze({
9
10
  schemaVersion:1,id:'stillmade.product-ad-variants-workflow',version,name:'Product Footage Ad Variants',description,license:'MIT',startingChat:'native',
10
11
  summary:{text:description,origin:'human',generation:'authored',status:'ready'},
11
12
  defaults:{aspectRatio:'9:16',brief:'Upload one authorized real product demonstration. Prepare a complete 5–60 second portrait edit with full framing and exact approved copy. Optional captions must be reviewed against the original speech. Choose three different non-overlapping 1–3 second source moments and supply three distinct approved hooks. Each candidate labels that source preview, then retains the same complete demonstration. Review and export A, B and C separately. No generated UGC, invented claims, automatic winning-hook selection or ad launch is included.'},
@@ -23,5 +24,7 @@ const productAdVariantsType=(version,editor)=>Object.freeze({
23
24
  });
24
25
  export const PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE=productAdVariantsType('1.0.1',editorV229);
25
26
  export const PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE=productAdVariantsType('1.0.2',editorV2210);
26
- export const PRODUCT_AD_VARIANTS_PROJECT_TYPE=productAdVariantsType('1.0.3',editorV2211);
27
+ export const PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3=productAdVariantsType('1.0.3',editorV2211);
28
+ // 1.0.4: rights are confirmed once in the Content & IP Policy at sign-up, not per project.
29
+ export const PRODUCT_AD_VARIANTS_PROJECT_TYPE=productAdVariantsType('1.0.4',editorV2211,shortFormV103);
27
30
  export default PRODUCT_AD_VARIANTS_PROJECT_TYPE;
@@ -121,7 +121,7 @@ export function projectContextSnapshot(data={},options={}) {
121
121
  // generic native-media loop too. Excluding Block results must exclude the
122
122
  // whole programmable node, including its legacy versions and input claims.
123
123
  if(options.includeBlockOutputs===false&&node.type==='programmableBlockNode')continue;
124
- const d=object(node.data),label=d.title || d.name || d.label || node.id;
124
+ const d=object(node.data),label=d.title || d.name || d.label || d.blockPackage?.manifest?.name || node.id;
125
125
  if(node.type==='programmableBlockNode')for(const value of [...Object.values(object(d.blockInputs)),...(d.blockStale?[]:Object.values(object(d.blockOutputs)))])addFiles(value,d,label);
126
126
  const referenceField={characterReferenceNode:'characters',locationReferenceNode:'locations',styleNode:'styles'}[node.type];
127
127
  const role=referenceField ? {characters:'character_reference',locations:'location_reference',styles:'style_reference'}[referenceField] : undefined;
@@ -176,7 +176,7 @@ export function projectContextSnapshot(data={},options={}) {
176
176
  for(const [name,declared] of Object.entries(d.blockPackage?.manifest?.outputs || {})){
177
177
  if(!['image','video','audio','asset','image[]','video[]','audio[]','asset[]'].includes(declared.type))continue;
178
178
  const output=result.outputs?.[name];
179
- for(const ref of Array.isArray(output) ? output : output ? [output] : [])addMedia({...ref,source:'block',owner,role:declared.role,label:entry.step?.label || node.id,record:{createdAt:result.at || entry.at},active:false});
179
+ for(const ref of Array.isArray(output) ? output : output ? [output] : [])addMedia({...ref,source:'block',owner,role:declared.role,label:entry.step?.label || d.blockPackage?.manifest?.name || node.id,record:{createdAt:result.at || entry.at},active:false});
180
180
  }
181
181
  }
182
182
  }
@@ -275,7 +275,7 @@ export function projectContextSnapshot(data={},options={}) {
275
275
  for(const [name,declared] of Object.entries(d.blockPackage?.manifest?.outputs || {})){
276
276
  if(!['image','video','audio','asset','image[]','video[]','audio[]','asset[]'].includes(declared.type))continue;
277
277
  const output=row.outputs?.[name];
278
- for(const ref of Array.isArray(output)?output:output?[output]:[])addMedia({...ref,source:'block',owner,role:declared.role,label:d.title || d.label || node.id,record:{createdAt:row.at},active,generated:active});
278
+ for(const ref of Array.isArray(output)?output:output?[output]:[])addMedia({...ref,source:'block',owner,role:declared.role,label:d.title || d.label || d.blockPackage?.manifest?.name || node.id,record:{createdAt:row.at},active,generated:active});
279
279
  }
280
280
  }
281
281
  for(const row of [...list(d.blockBatchResults),...list(d.blockUnusedResults),...list(d.blockPinnedResults).map(result=>({...result,active:false}))]){
@@ -283,7 +283,7 @@ export function projectContextSnapshot(data={},options={}) {
283
283
  for(const [name,declared] of Object.entries(d.blockPackage?.manifest?.outputs||{})){
284
284
  if(!['image','video','audio','asset','image[]','video[]','audio[]','asset[]'].includes(declared.type))continue;
285
285
  const output=row.outputs?.[name];
286
- for(const ref of Array.isArray(output)?output:output?[output]:[])addMedia({...ref,source:'block',owner,role:declared.role,label:d.title||d.label||node.id,record:{createdAt:row.at},active:false});
286
+ for(const ref of Array.isArray(output)?output:output?[output]:[])addMedia({...ref,source:'block',owner,role:declared.role,label:d.title||d.label||d.blockPackage?.manifest?.name||node.id,record:{createdAt:row.at},active:false});
287
287
  }
288
288
  }
289
289
  }