stillmade-sdk 0.1.8 → 0.1.10

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 (54) hide show
  1. package/.stillmade/sdk-baseline.json +1 -1
  2. package/AGENTS.md +8 -2
  3. package/DESKTOP_CAPABILITIES.md +1 -1
  4. package/INTERFACE_DESIGN.md +167 -83
  5. package/MODULES.md +1 -1
  6. package/NATIVE_MCP.md +1 -1
  7. package/PASTE_TO_LLM.md +151 -12
  8. package/README.md +5 -2
  9. package/SDK.md +510 -115
  10. package/UI_AESTHETIC.md +64 -0
  11. package/blocks/native/sm.export-video/versions/2.1.0/package.js +580 -0
  12. package/blocks/native/sm.panel-strip/versions/2.4.0/package.js +1176 -0
  13. package/blocks/native/sm.panel-strip/versions/2.4.1/package.js +1316 -0
  14. package/blocks/native/sm.shot-plan/versions/2.2.0/package.js +800 -0
  15. package/blocks/native/sm.shot-plan/versions/2.2.1/package.js +920 -0
  16. package/examples/README.md +10 -0
  17. package/examples/custom-interface.stillmade.json +3 -3
  18. package/package.json +7 -1
  19. package/packages/block-cli/dev-host/assets/host.js +219 -26
  20. package/packages/block-cli/dev-host/assets/index.css +1 -1
  21. package/packages/block-cli/format.js +1 -1
  22. package/packages/block-cli/templates.js +53 -22
  23. package/packages/block-platform/block-readiness.js +5 -0
  24. package/packages/block-platform/brief-ports.js +29 -0
  25. package/packages/block-platform/catalog.js +32 -9
  26. package/packages/block-platform/current-semantic-source.js +7 -3
  27. package/packages/block-platform/flow-inspection.js +14 -4
  28. package/packages/block-platform/plain-descriptions.js +1 -1
  29. package/packages/block-platform/project-context.js +7 -0
  30. package/packages/block-platform/project-input-samples.js +3 -1
  31. package/packages/block-platform/project-inputs.js +11 -2
  32. package/packages/block-platform/project-start-input.js +46 -0
  33. package/packages/block-platform/segment-runner.js +4 -2
  34. package/packages/block-platform/step-groups.js +4 -0
  35. package/packages/block-platform/steps.js +55 -4
  36. package/packages/block-platform/type-preview.js +19 -3
  37. package/packages/block-platform/workspace-input-readset.js +6 -1
  38. package/packages/block-sdk/builder-contract.js +11 -5
  39. package/packages/block-sdk/collaboration-admission.js +4 -2
  40. package/packages/block-sdk/connection-adapters.js +35 -2
  41. package/packages/block-sdk/contracts.js +5 -2
  42. package/packages/block-sdk/examples.js +3 -3
  43. package/packages/block-sdk/first-party-identities.js +190 -0
  44. package/packages/block-sdk/first-party-packages.js +6 -1
  45. package/packages/block-sdk/index.d.ts +2 -0
  46. package/packages/block-sdk/index.js +1 -1
  47. package/packages/block-sdk/javascript.js +5 -1
  48. package/packages/block-sdk/platform-checks.js +13 -2
  49. package/packages/block-sdk/shared-agent-controls.js +5 -4
  50. package/packages/block-sdk/shared-controls.js +13 -9
  51. package/packages/block-sdk/view-design.js +104 -27
  52. package/packages/block-sdk/view.js +4 -2
  53. package/packages/design-system/block-kit.css +184 -0
  54. package/packages/design-system/block-kit.html +187 -0
@@ -1,6 +1,7 @@
1
1
  import {assert,valueMatches} from '../block-sdk/contracts.js';
2
2
  import {semanticCompatibility} from '../block-sdk/semantic-contracts.js';
3
3
  import {createSemanticContext} from './semantic-context.js';
4
+ import {isBriefPort,inputLabel} from './brief-ports.js';
4
5
 
5
6
  export function seedProjectStartContext(canvas,definition,brief,{now=Date.now()}={}) {
6
7
  if(typeof brief!=='string'||!brief.trim())return canvas;
@@ -32,3 +33,48 @@ export function seedProjectStartBrief(compiled,brief){
32
33
  if(previous!==undefined&&previous!=='')return compiled.canvas;
33
34
  return {...compiled.canvas,nodes:compiled.canvas.nodes.map(item=>item===node?{...node,data:{...node.data,blockInputs:{...node.data.blockInputs,[name]:brief}}}:item)};
34
35
  }
36
+
37
+ // A new project never starts with a brief its Project Type was saved with. A
38
+ // Step's brief is the facts about the thing this project makes, which come from
39
+ // this project's own Chat; a brief saved in a Type was written while the Type was
40
+ // being built (October 2026: a dealer script brief carried the builder's step
41
+ // names "Import media, Script Writer, Voiceover…" and a test car's mileage into
42
+ // every new project, and Chat never asked because the input looked answered).
43
+ // Only the project's Block inputs change: the definition stays the exact pinned
44
+ // Type, so release and automation digests still match.
45
+ // One brief rule (brief-ports.js): a brief input, or a custom Block's text input named or
46
+ // labelled "brief" (October 16, 2026: a custom script Block's text "brief" kept the builder's
47
+ // words because only brief-typed inputs were left to Chat).
48
+ export function leaveStepBriefsToChat(compiled){
49
+ let changed=false;
50
+ const nodes=compiled.canvas.nodes.map(node=>{
51
+ const manifest=node.type==='programmableBlockNode'?node.data?.blockPackage?.manifest:null,ports=manifest?.inputs,inputs=node.data?.blockInputs;
52
+ if(!ports||!inputs)return node;
53
+ const drop=Object.keys(inputs).filter(name=>isBriefPort(name,ports[name],{label:inputLabel(manifest,name)}));
54
+ if(!drop.length)return node;
55
+ changed=true;
56
+ const kept={...inputs};for(const name of drop)delete kept[name];
57
+ return {...node,data:{...node.data,blockInputs:kept}};
58
+ });
59
+ return changed?{...compiled,canvas:{...compiled.canvas,nodes}}:compiled;
60
+ }
61
+
62
+ // The same rule for a Project Type a Chat build places (use_project_type) or puts together
63
+ // (prepare_automation): its Steps' saved brief settings are left out, so the project's own
64
+ // Chat step gathers those facts (October 16, 2026: a Type built in the builder chat carried
65
+ // the builder's request in its script Step's brief). manifestOf(stage) gives each Step's
66
+ // Block; only, when given, limits it to those Step ids (Steps already in a project keep theirs).
67
+ export function leaveTypeBriefsToChat(type,manifestOf,{only=null}={}){
68
+ let changed=false;
69
+ const stages=(type?.stages||[]).map(stage=>{
70
+ if(only&&!only.has(stage.id)||!stage.config||typeof stage.config!=='object')return stage;
71
+ let manifest=null;try{manifest=manifestOf(stage);}catch{manifest=null;}
72
+ const drop=Object.keys(stage.config).filter(name=>isBriefPort(name,manifest?.inputs?.[name],{label:inputLabel(manifest,name)}));
73
+ if(!drop.length)return stage;
74
+ changed=true;
75
+ const config={...stage.config};for(const name of drop)delete config[name];
76
+ const {config:_config,...rest}=stage;
77
+ return Object.keys(config).length?{...rest,config}:rest;
78
+ });
79
+ return changed?{...type,stages}:type;
80
+ }
@@ -23,7 +23,7 @@ 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
+ import {groupOf,withoutGroups} from './step-groups.js';
27
27
  import {UNATTENDED_HOSTED_OPERATIONS} from './unattended.js';
28
28
  export {UNATTENDED_HOSTED_OPERATIONS};
29
29
 
@@ -66,7 +66,9 @@ export async function planSegment(projectType,canvas,startStepId,{interactive=fa
66
66
  const type=structuredClone(projectType),graph=structuredClone(canvas),operations=structuredClone(applicationOperations);
67
67
  safeDocument(operations);assert(plain(operations),'BROWSER_APPLICATION_OPERATION','Select application operations by Step ID.');
68
68
  safeDocument(type);assert(plain(graph) && Array.isArray(graph.nodes) && Array.isArray(graph.edges),'SEGMENT_CANVAS','A saved production Canvas is required');
69
- if(graph.projectType)assert(stableStringify(graph.projectType)===stableStringify(type),'SEGMENT_CHANGED','The workflow and Canvas definitions differ');
69
+ // Step groups only change what people see and which job one button runs, so a
70
+ // Canvas saved before its Step was condensed still matches (Block in block).
71
+ if(graph.projectType)assert(stableStringify(withoutGroups(graph.projectType))===stableStringify(withoutGroups(type)),'SEGMENT_CHANGED','The workflow and Canvas definitions differ');
70
72
  const compiled=await compileTypeWorkspace(type,{experimentalBrowser:experimentalBrowser ?? experimentalBrowserValidation}),active=type.stages.filter(step=>step.enabled!==false);
71
73
  // One Step per job: with no explicit range, an interactive run from any part
72
74
  // of a Step group runs that whole job, from its first enabled part to its last.
@@ -42,9 +42,13 @@ export function normalizeGroups(type){
42
42
  return [{id:group.id,label:group.label,stages,...(settings.length?{settings}:{}),...(result?{result}:{})}];
43
43
  });
44
44
  if(!groups.length){const {groups:_dropped,...rest}=type;return rest;}
45
+ // Unchanged groups keep the type's identity, so memo boundaries hold.
46
+ if(groups.length===type.groups.length&&groups.every((group,index)=>JSON.stringify(group)===JSON.stringify(type.groups[index])))return type;
45
47
  return {...type,groups};
46
48
  }
47
49
 
50
+ /** The type without its Step groups: the same workflow, whatever people see. */
51
+ export function withoutGroups(type){if(!type||!Object.hasOwn(type,'groups'))return type;const {groups:_groups,...rest}=type;return rest;}
48
52
  /** The group a Step belongs to, if any. */
49
53
  export const groupOf=(type,stageId)=>(type.groups||[]).find(group=>group.stages.includes(stageId))||null;
50
54
 
@@ -83,6 +83,46 @@ export function insertionLocations(type,pkg,catalog=[],options={}) {
83
83
  });
84
84
  }
85
85
 
86
+ // A connection along the executable primary chain (rebuilt around a gap), as
87
+ // opposed to an explicit secondary or context handoff.
88
+ function primaryEdge(type,from,to,edge,catalog) {
89
+ const output=automaticPort(manifestFor(type,from,catalog),'outputs'),input=automaticPort(manifestFor(type,to,catalog),'inputs');
90
+ return !!(output&&input&&edge.from.port===output.name&&edge.to.port===input.name);
91
+ }
92
+ const portWords=name=>String(name||'').replace(/([a-z0-9])([A-Z])/g,'$1 $2').replace(/[_-]+/g,' ').trim().toLowerCase()||'result';
93
+ const stepName=(type,step,catalog)=>String(step?.label||manifestFor(type,step,catalog)?.name||step?.id||'Step');
94
+ const joined=items=>items.length<3?items.join(' and '):`${items.slice(0,-1).join(', ')} and ${items.at(-1)}`;
95
+ // Whether an input still has a value without its connection: it is optional, or
96
+ // has a default it may use, a project document (context) or the Step's own setting.
97
+ function fallsBack(step,port,name) {
98
+ if(!port||port.required===false||port.context)return true;
99
+ if(Object.hasOwn(port,'default')&&(!Array.isArray(port.sources)||port.sources.includes('default')))return true;
100
+ return Object.hasOwn(step.config||{},name)||Object.hasOwn(step.inputSources||{},name);
101
+ }
102
+ // Names the Steps that read an explicit result of a Step that is leaving.
103
+ function stillUsed(type,edges,catalog) {
104
+ const byId=new Map(type.stages.map(step=>[step.id,step])),name=id=>`“${stepName(type,byId.get(id),catalog)}”`;
105
+ const uses=edges.map(edge=>`${name(edge.to.stage)} uses the ${portWords(edge.from.port)} of ${name(edge.from.stage)} for its ${portWords(edge.to.port)} input`);
106
+ return `${joined(uses)}. Replace ${joined([...new Set(edges.map(edge=>name(edge.from.stage)))])} with a compatible Block or remove ${joined([...new Set(edges.map(edge=>name(edge.to.stage)))])} first.`;
107
+ }
108
+ /**
109
+ * What deleting the Steps `ids` cuts: each explicit connection from one of them
110
+ * into an input of a kept, enabled Step (the primary chain closes around the
111
+ * gap instead). `optional` inputs are disconnected by the delete; one the kept
112
+ * Step needs refuses it.
113
+ * [{edge, from:{stage,label,output}, to:{stage,label,input}, optional}]
114
+ */
115
+ export function removalDependents(type,ids,catalog=[]) {
116
+ const gone=new Set(ids),byId=new Map((type?.stages||[]).map(step=>[step.id,step])),cut=[];
117
+ for(const edge of type?.connections||[]) {
118
+ const from=byId.get(edge.from?.stage),to=byId.get(edge.to?.stage);
119
+ if(!from||!to||!gone.has(from.id)||gone.has(to.id)||to.enabled===false||primaryEdge(type,from,to,edge,catalog))continue;
120
+ cut.push({edge,from:{stage:from.id,label:stepName(type,from,catalog),output:portWords(edge.from.port)},to:{stage:to.id,label:stepName(type,to,catalog),input:portWords(edge.to.port)},
121
+ optional:fallsBack(to,manifestFor(type,to,catalog)?.inputs?.[edge.to.port],edge.to.port)});
122
+ }
123
+ return cut;
124
+ }
125
+
86
126
  // Rebuild each existing executable primary chain, retaining independent chains
87
127
  // and explicit secondary/context handoffs. No new host port is invented, and an
88
128
  // active consumer's secondary source cannot silently disappear.
@@ -92,11 +132,11 @@ export function changeSteps(type,stages,catalog=[]) {
92
132
  for(const edge of type.connections||[]) {
93
133
  const from=byId.get(edge.from?.stage),to=byId.get(edge.to?.stage);
94
134
  assert(from&&to,'STEP_REFERENCE','A connection references a missing Step');
95
- const output=automaticPort(manifestFor(type,from,catalog),'outputs'),input=automaticPort(manifestFor(type,to,catalog),'inputs');
96
- if(output&&input&&edge.from.port===output.name&&edge.to.port===input.name){primary.push(edge);participants.add(from.id);participants.add(to.id);}else explicit.push(edge);
135
+ if(primaryEdge(type,from,to,edge,catalog)){primary.push(edge);participants.add(from.id);participants.add(to.id);}else explicit.push(edge);
97
136
  }
98
137
  const active=new Set(stages.filter(step=>step.enabled!==false).map(step=>step.id));
99
- for(const edge of explicit)assert(!active.has(edge.to.stage)||active.has(edge.from.stage),'STEP_REFERENCE','Another Step still uses this Block’s output. Replace it with a compatible Block or remove the dependent Step first.');
138
+ const used=explicit.filter(edge=>active.has(edge.to.stage)&&!active.has(edge.from.stage));
139
+ assert(!used.length,'STEP_REFERENCE',used.length?stillUsed(type,used,catalog):'');
100
140
  const connections=explicit.filter(edge=>active.has(edge.from.stage)&&active.has(edge.to.stage));
101
141
  const unseen=new Set(participants);
102
142
  while(unseen.size){
@@ -113,7 +153,18 @@ export function changeSteps(type,stages,catalog=[]) {
113
153
  }
114
154
  return validated({...type,stages,connections},catalog);
115
155
  }
116
- export function removeStep(type,id,catalog=[]) {assert(type.stages.some(step=>step.id===id),'STEP','Step not found');return changeSteps({...type,...(type.rules?{rules:type.rules.filter(rule=>rule.stepId!==id&&rule.endStepId!==id)}:{})},type.stages.filter(step=>step.id!==id),catalog);}
156
+ export function removeStep(type,id,catalog=[]) {return removeSteps(type,[id],catalog);}
157
+ // A Step people see may be several Blocks (step-jobs.js condensedType); they
158
+ // leave together in one change, so a connection between them never blocks it.
159
+ // A kept Step's optional input fed by a leaving Step is disconnected (October
160
+ // 17, 2026: Export video 2.1.0's optional media and narration refused deleting
161
+ // Import media and Voiceover); an input it needs refuses the delete, by name.
162
+ export function removeSteps(type,ids,catalog=[]) {
163
+ const gone=new Set(ids);
164
+ assert(gone.size>0&&[...gone].every(id=>type.stages.some(step=>step.id===id)),'STEP','Step not found');
165
+ const disconnected=new Set(removalDependents(type,[...gone],catalog).filter(item=>item.optional).map(item=>item.edge));
166
+ return changeSteps({...type,...(type.connections?{connections:type.connections.filter(edge=>!disconnected.has(edge))}:{}),...(type.rules?{rules:type.rules.filter(rule=>!gone.has(rule.stepId)&&!gone.has(rule.endStepId))}:{})},type.stages.filter(step=>!gone.has(step.id)),catalog);
167
+ }
117
168
  export function moveStep(type,id,index,catalog=[]) {
118
169
  const step=type.stages.find(step=>step.id===id);assert(step&&Number.isInteger(index)&&index>=0&&index<type.stages.length,'STEP','Choose a valid Step position');
119
170
  if(type.stages[index]===step)return type;
@@ -18,8 +18,13 @@ import {assertInputSource,assertSuppliedInputSources,inputSourceAllowed} from '.
18
18
  import {projectInputSampleValues} from './project-input-samples.js';
19
19
  import {resolveSemanticInputs} from './semantic-inputs.js';
20
20
  import {packageForStage} from './stage-package.js';
21
+ import {briefInputNames} from './brief-ports.js';
21
22
 
22
- export function workflowPreviewChecks(preview){return preview.results.map(result=>({name:`Connected workflow: ${result.name}`,passed:['completed','skipped'].includes(result.status),status:result.status==='completed'?'passed':result.status==='skipped'?'skipped':result.status==='disabled'?'disabled':['blocked','needs-project','needs-context','needs-hosted-run'].includes(result.status)?'deferred':'failed',...(result.deferral ? {deferral:result.deferral} : {}),...(result.hostedPackageDigests ? {hostedPackageDigests:[...result.hostedPackageDigests]} : {}),...(result.code ? {code:result.code} : {}),message:result.message || (result.status==='skipped' ? 'Condition skipped; input passed through' : result.status==='completed' ? 'Validated connected sample input and output' : result.status)}));}
23
+ export function workflowPreviewChecks(preview){return preview.results.map(result=>({name:`Connected workflow: ${result.name}`,passed:['completed','skipped'].includes(result.status),status:result.status==='completed'?'passed':result.status==='skipped'?'skipped':result.status==='disabled'?'disabled':['blocked','needs-project','needs-context','needs-hosted-run','needs-sample'].includes(result.status)?'deferred':'failed',...(result.deferral ? {deferral:result.deferral} : {}),...(result.hostedPackageDigests ? {hostedPackageDigests:[...result.hostedPackageDigests]} : {}),...(result.code ? {code:result.code} : {}),message:result.message || (result.status==='skipped' ? 'Condition skipped; input passed through' : result.status==='completed' ? 'Validated connected sample input and output' : result.status)}));}
24
+
25
+ // A brief with no text (nothing, an empty string or a document with empty text).
26
+ const blankBrief=value=>value===undefined||value===null||(typeof value==='string'?!value.trim():plain(value)&&typeof value.text==='string'&&!value.text.trim());
27
+ const briefLabel=(manifest,name)=>{const label=manifest.ui?.find(item=>item?.port===name)?.label||name;return String(label).replace(/([a-z0-9])([A-Z])/g,'$1 $2').replace(/[_-]+/g,' ').trim().toLowerCase();};
23
28
 
24
29
  // A side-effect-free rehearsal using exact pinned packages. Hosted workspaces
25
30
  // are marked as requiring a real project; no provider calls are simulated.
@@ -63,8 +68,15 @@ export async function previewProjectType(type,{run=runPackageAsync,inputs={},con
63
68
  else try {
64
69
  // Fixture values seed unconnected sample inputs. Live edge values always win;
65
70
  // a downstream fixture must never hide a broken connection or overwrite it.
71
+ // In the author's preview (interactive) a Block's test fixture never stands in
72
+ // for a Step's brief (brief-ports.js): the author writes a sample brief, as each
73
+ // project's Chat writes the real one. October 17, 2026: the Type builder showed
74
+ // the owner's script Block's first fixture ("32,400 miles, Palm Beach Auto Mall")
75
+ // as the Step's brief. Automated checks (imports, listing samples) still run the
76
+ // Block's own example.
66
77
  assertSuppliedInputSources(m,step.config || {},inputs[step.id] || {},{connected:incoming.map(edge=>edge.to.port)});
67
- const fixture=Object.fromEntries(Object.entries(pkg.tests[0].input).filter(([name])=>m.inputs[name] && !m.inputs[name].context && inputSourceAllowed(m.inputs[name],'user')));
78
+ const briefs=interactive?briefInputNames(m):[];
79
+ const fixture=Object.fromEntries(Object.entries(pkg.tests[0].input).filter(([name])=>m.inputs[name] && !m.inputs[name].context && inputSourceAllowed(m.inputs[name],'user') && !briefs.includes(name)));
68
80
  const values={...step.config,...inputs[step.id]};
69
81
  for(const edge of incoming){
70
82
  const producer=pinned.stages.find(step=>step.id===edge.from.stage),source=packageForStage(pinned,producer,BUILTIN_BLOCKS);
@@ -74,6 +86,9 @@ export async function previewProjectType(type,{run=runPackageAsync,inputs={},con
74
86
  }
75
87
  const semantic=sharedValues.length?resolveSemanticInputs(m,values,sharedValues,{context:authorizedContext,semantics}).inputs:values;
76
88
  const resolved=resolveContextInputs(m,{...fixture,...semantic},authorizedContext);
89
+ const missingBrief=briefs.find(name=>!incoming.some(edge=>edge.to.port===name)&&blankBrief(resolved[name])&&(m.inputs[name].required!==false||pkg.capability?.prompt?.$input===name));
90
+ if(missingBrief)result={...result,status:'needs-sample',message:`Write a sample ${briefLabel(m,missingBrief)} for this Step under Sample inputs to try it. A Block’s own example is never used as a Step’s brief.`};
91
+ else{
77
92
  for(const [name,port] of Object.entries(m.inputs))if(Object.hasOwn(resolved,name)){resolved[name]=media.hydrate(port,resolved[name]);if(interactive&&resolveInput)resolved[name]=await resolveInput(pkg,port,resolved[name],{signal:controller.signal});check();}
78
93
  const input=validateValues(m.inputs,resolved);
79
94
  charge(stableStringify(input).length);
@@ -99,10 +114,11 @@ export async function previewProjectType(type,{run=runPackageAsync,inputs={},con
99
114
  charge(stableStringify(outputs).length);
100
115
  result={...result,status:execution.skipped ? 'skipped' : 'completed',input,outputs,...(execution.comfyuiRun?{comfyuiRun:structuredClone(execution.comfyuiRun)}:{}),...(execution.capabilityRun?{capabilityRun:structuredClone(execution.capabilityRun)}:{}),...(execution.mediaReceipts?{mediaReceipts:structuredClone(execution.mediaReceipts)}:{})};
101
116
  }
117
+ }
102
118
  }catch(error){if(controller.signal.aborted || signal?.aborted || Date.now()>=deadline)throw error;result={...result,status:'failed',code:error.code || 'EXECUTION',message:error.message};}
103
119
  completed.set(step.id,result);results.push(result);await onResult(structuredClone(result));check();
104
120
  }
105
- const failed=results.some(result=>result.status==='failed'),incomplete=results.some(result=>['blocked','needs-project','needs-context','needs-hosted-run'].includes(result.status));
121
+ const failed=results.some(result=>result.status==='failed'),incomplete=results.some(result=>['blocked','needs-project','needs-context','needs-hosted-run','needs-sample'].includes(result.status));
106
122
  return {digest:await digest(pinned),passed:!failed,complete:!failed && !incomplete,status:failed?'failed':incomplete?'deferred':'completed',results};
107
123
  }finally{media.dispose();clearTimeout(timer);signal?.removeEventListener('abort',cancel);controller.abort();}
108
124
  }
@@ -32,7 +32,12 @@ export async function captureWorkspaceInputReadSet({manifest,input,project,scope
32
32
  if(!port.context||input[key]===undefined)continue;
33
33
  const nativeScopes=workspaceContextReadScopes(port.context);
34
34
  if(!nativeScopes.length){const collection=await workspaceCollectionSources({manifest,key,input,project,scope,source,sourceNodeIds});if(collection)collections.push({key,context:port.context,inputDigest:await digest(input[key]),...collection});else unprotected.push({key,context:port.context});continue;}
35
- assert(stableStringify(input[key])===stableStringify(expected[key]),'CONFLICT','The dispatched native workspace input no longer matches its source. Review and run it again.');
35
+ // A project without this document yet (no shot plan, no voiceover take) gives
36
+ // the run the port's declared default. That value is read against the
37
+ // document's empty native form below, so a document made later still blocks
38
+ // applying a result built without it.
39
+ const absent=expected[key]===undefined;
40
+ assert(absent?Object.hasOwn(port,'default')&&stableStringify(input[key])===stableStringify(port.default):stableStringify(input[key])===stableStringify(expected[key]),'CONFLICT','The dispatched native workspace input no longer matches its source. Review and run it again.');
36
41
  if(['shots','scenes'].includes(port.context)||port.context==='script'&&(source!=null||projectContextScope(scope).kind!=='project')){
37
42
  const originals=shotIndexContextSources({manifest,key,project,scope,source,input});if(!originals){unprotected.push({key,context:port.context,reason:'scoped-source-attribution-unavailable'});continue;}
38
43
  for(const [nativeScope,document] of Object.entries(originals)){if(documents.has(nativeScope))assert(stableStringify(documents.get(nativeScope))===stableStringify(document),'CONFLICT','Original derived context documents disagree.');documents.set(nativeScope,document);ports.push({key,context:port.context,scope:nativeScope,inputDigest:await digest(input[key]),derivation:'shot-index'});}continue;
@@ -3,10 +3,13 @@
3
3
  // human developers) receives a rendering of this module, so a rule changes in
4
4
  // one place. Lists that track the platform (hosted operations) are read from
5
5
  // contracts.js, so the contract follows the SDK as it grows.
6
+ // How to write the package itself (format, runtimes, hosted operations, project
7
+ // context, custom views) is authoring-guide.js, rendered for the same audiences;
8
+ // scripts/block-paths-share-sdk.test.js keeps every in-app builder on both.
6
9
  import {HOSTED_OPERATION_NAMES} from './contracts.js';
7
10
 
8
11
  export const BUILDER_CONTRACT_ID='stillmade.block-builder';
9
- export const BUILDER_CONTRACT_VERSION='1.3.0';
12
+ export const BUILDER_CONTRACT_VERSION='1.5.1';
10
13
  export const BUILDER_CONTRACT_STAMP=`${BUILDER_CONTRACT_ID}@${BUILDER_CONTRACT_VERSION}`;
11
14
  export const BUILDER_CONTRACT_DOCS_PATH='/docs/build/block-builder-contract';
12
15
  // Block names appear in step bars, cards and pickers; new releases keep them short.
@@ -36,7 +39,7 @@ const SECTIONS=Object.freeze([
36
39
  'Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator\'s source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.'),
37
40
  rule('block-scope','both',
38
41
  'One Step per job: start one only where a person works with the result, where finding new items turns into handling each, or where the result leaves StillMade; all else is a part.',
39
- 'A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.\n\n1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). That is why Chat, Bible, Canvas and Editor are four Steps.\n2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.\n3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.\n\nA part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task\'s own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.\n\nInside one job, each capability stays its own part: a capability Block performs exactly one hosted operation and a connector one app task, so every part keeps its own permission, approval and credit rules, and StillMade groups the parts of one job into one Step (`groups` in the Project Type). Never invent a combined runtime. When one Block can do the whole job with documented primitives (a JavaScript Block doing several local transforms, a workspace owning its whole document), build it as one Block.\n\nExamples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".'),
42
+ 'A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.\n\n1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). That is why Chat, Bible, Canvas and Editor are four Steps.\n2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.\n3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.\n\nIn a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before; neighbouring workspaces with one main result type are one Step unless another part feeds the second. A video ends in the Editor (1.0), which has captions and export built in, so Export video is never a Step beside it.\n\nA part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task\'s own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.\n\nInside one job, each capability stays its own part: a capability Block performs exactly one hosted operation and a connector one app task, so every part keeps its own permission, approval and credit rules, and StillMade groups the parts of one job into one Step (`groups` in the Project Type). Never invent a combined runtime. When one Block can do the whole job with documented primitives (a JavaScript Block doing several local transforms, a workspace owning its whole document), build it as one Block.\n\nExamples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".'),
40
43
  rule('capability-contract','block',
41
44
  'Define the contract first: typed inputs (optional with safe defaults where possible), outputs, state, permissions, UI, failures and cancellation. Keep the interface small; keep the job whole.',
42
45
  'Translate the request into an explicit capability contract before implementation: what the Block does; each input with its type, meaning and whether it is required (prefer optional settings with safe defaults over extra required questions); each output with its type and meaning; persistent state; project context it reads; permissions; UI; long-running behavior; expected failures; cancellation; determinism; and whether it needs hosted models, media, desktop tools or shared editing. Keep the public interface as small and general as possible (few required inputs, one primary output) and keep implementation details out of it; small means a small interface, never a fragment of a job. A Block that generates images, video or speech declares only its prompt and output: every Step that runs it gets the host’s generation settings (model, quality, length and resolution, voice, speed, how many), saved with the Step, changed on its page or by Chat, and shown with their cost before each run.'),
@@ -53,8 +56,8 @@ const SECTIONS=Object.freeze([
53
56
  `Name the Block in ${BLOCK_NAME_MAX_LENGTH} characters or fewer, in plain words.`,
54
57
  `Give the Block a short, plain name of ${BLOCK_NAME_MAX_LENGTH} characters or fewer that says what it does ("Captioned Clip", not "Captioned Business Clip Generator With Review"). The name appears in step bars, Project Type cards and pickers, so put the detail in the description instead. New releases with a longer name do not pass readiness.`),
55
58
  rule('ui','block',
56
- 'Prefer `manifest.ui` controls; a custom view only when needed: run via `StillMade.run`; show progress, errors, outputs, and previews of options people hear or see; no raw JSON or IDs; fit a 320px phone.',
57
- 'If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction genuinely needs one. Expose the capability, not the implementation: meaningful controls, sensible defaults, preview, direct manipulation, progress, clear errors and visible outputs. Do not show raw JSON, internal IDs, filesystem details, provider terminology or developer settings unless the Block is itself a developer tool. When a person chooses among options they need to hear or see (voices, music, sounds, fonts, looks, colors, templates or images), let them preview each option before choosing: a sample to play or a thumbnail, from the Block\'s own files or the host\'s free samples, never a paid generation. Leave previews out only when the builder states there is nothing to preview. Choices StillMade owns, such as voices and models in the run review, already show their samples. Follow the host appearance (Light, Dark and White) and the mobile layout contract: one interface for every screen size that fits a 320 CSS-pixel phone without horizontal scrolling.'),
59
+ 'Prefer `manifest.ui`; custom views use the Block kit (`sm-*` classes) unless a look is named. Pictures over words: preview each option before choosing; cut needless words, fold the rest; the start button names the action (never "Run") and shows no price (StillMade adds it); fit one screen unscrolled and a 320px phone.',
60
+ 'If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction genuinely needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, picks as pictures in `sm-choices`, one `sm-primary` naming the action (Write, Generate; never "Run") and no price (StillMade\'s run button adds it), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Expose the capability, not the implementation: meaningful controls, sensible defaults, preview, direct manipulation, progress, clear errors and visible outputs. Do not show raw JSON, internal IDs, filesystem details, provider terminology or developer settings unless the Block is itself a developer tool. When a person chooses among options they need to hear or see (voices, music, sounds, fonts, looks, colors, templates or images), let them preview each option before choosing: a sample to play or a thumbnail, from the Block\'s own files or the host\'s free samples, never a paid generation. Leave previews out only when the builder states there is nothing to preview. Choices StillMade owns, such as voices and models in the run review, already show their samples. Follow the host appearance (Light, Dark and White) and the mobile layout contract: one interface for every screen size that fits a 320 CSS-pixel phone without horizontal scrolling.'),
58
61
  rule('multiplayer','block',
59
62
  'Custom views share edits: `StillMade.onShared`, publish every editable field (`data-stillmade-share`, `bindShared`, `updateShared`), respect `canEdit`, never re-run on remote updates.',
60
63
  'Projects are multiplayer. A custom view subscribes through `StillMade.onShared(callback)` and publishes every editable field: give static controls a unique `id` and `data-stillmade-share="field"` (the host owns their synchronization), use `StillMade.bindShared(field, elementId)` for controls created dynamically, and `StillMade.updateShared(values, before, options)` for structured values; private variables and DOM values are not shared. Mark a truly device-only control (playback, hover, device selection) with `data-stillmade-local`. Observe shared outputs as well as state, honor `canEdit` in every mutating callback (read-only participants cannot write), never run, click or call a paid integration in response to a remote update, and preserve conflicting drafts. Shared background work uses `sharedWork` with the `work.shared` permission.'),
@@ -73,6 +76,9 @@ const SECTIONS=Object.freeze([
73
76
  rule('errors','block',
74
77
  'Fail usefully: throw an Error saying what failed, why, whether retrying helps and what to do; StillMade shows it as the run failure. Never "Something went wrong".',
75
78
  'Errors must say what failed, why it likely failed, whether a retry makes sense and what the user can do, so the native project chat and controller can troubleshoot. In JavaScript, throw an `Error` with that message: the host reports it as a `JAVASCRIPT` run failure carrying your message (guest code cannot set its own error code). Reject invalid or oversized input up front with a message naming the input and the limit. Never reduce a failure to "Something went wrong".'),
79
+ rule('grounded-text','block',
80
+ 'Write only supplied facts; drop or generalise a section whose fact is missing.',
81
+ 'A text Block that writes from supplied facts must not let its prompt template outrank its own "never invent" rule. Every section that needs a specific fact (phone, website, address, price, inspection, certification, warranty, features) is written only when the input supplies that fact; otherwise the section is left out or replaced by a generic line that asserts nothing (for example "Call us today to schedule your test drive."). Never default to "inspected", "certified", invented features or placeholder contact details such as 555 numbers or example websites. Prefer extract-then-generate: first list the facts the input actually contains, then write using only that list. Add a check after generation that rejects or removes sentences naming a claim, number or address not present in the input. Include fixtures with the facts omitted and with them supplied.'),
76
82
  rule('adapt-to-platform','both',
77
83
  'If documented primitives cannot do it, return {"unsupported":"exact reason","gapClass":"B|C|D|E|F","primitive":"smallest general primitive"}; never fake a working package.',
78
84
  'Do not modify StillMade core because a request does not fit immediately. First implement the capability entirely through documented SDK and platform primitives. If that is impossible, classify why: A Block implementation problem (fix it), B missing SDK exposure of an existing host capability, C missing generic platform primitive, D security or permission restriction, E runtime or language incompatibility, F fundamentally incompatible capability. For B or C, name the smallest general primitive that would serve this Block and a meaningful class of future Blocks (shaped like `files.watch()` or `media.transform()`, never `supportThisSpecificBlock()`). Return `{"unsupported":"exact reason","gapClass":"B|C|D|E|F","primitive":"..."}` instead of a package that pretends to work.'),
@@ -124,7 +130,7 @@ export const BUILDER_CONTRACT=Object.freeze({
124
130
  gapClasses:GAP_CLASSES,
125
131
  });
126
132
 
127
- const SECTION_TITLES=Object.freeze({'source-of-truth':'Source of truth','reuse-first':'Reuse first','block-scope':'One job per Step','capability-contract':'Define the capability first','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
133
+ const SECTION_TITLES=Object.freeze({'grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-scope':'One job per Step','capability-contract':'Define the capability first','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
128
134
  const inScope=(scope,wanted)=>wanted==='all'||scope==='both'||scope===wanted;
129
135
 
130
136
  /** Render the contract for a model prompt ('compact'), a guide ('full') or an SDK folder ('agents-md'). */
@@ -3,12 +3,14 @@ import {parse} from 'acorn';
3
3
 
4
4
  const sharedKey=/^[a-zA-Z][a-zA-Z0-9_-]{0,99}$/;
5
5
  function annotatedControls(html=''){
6
- const bindings=[],controls=[],problems=[],keys=new Set(),ids=new Set(),source=String(html).replace(/<!--[\s\S]*?-->/g,''),tags=/<(input|textarea|select)\b([^>]*)>/gi;
6
+ const bindings=[],controls=[],problems=[],keys=new Set(),ids=new Set(),source=String(html).replace(/<!--[\s\S]*?-->/g,''),tags=/<(input|textarea|select|fieldset)\b([^>]*)>/gi;
7
7
  let tag;
8
8
  while((tag=tags.exec(source))){
9
9
  const attributes=new Map(),duplicates=new Set(),matcher=/([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;let attribute;
10
10
  while((attribute=matcher.exec(tag[2]))){const name=attribute[1].toLowerCase(),value=attribute[2]??attribute[3]??attribute[4]??null;if(attributes.has(name))duplicates.add(name);else attributes.set(name,value);}
11
- const element=tag[1].toLowerCase(),type=(attributes.get('type')||'text').toLowerCase(),supported=element!=='input'||['text','number','range','checkbox','color'].includes(type),shared=attributes.has('data-stillmade-share'),local=attributes.has('data-stillmade-local'),id=attributes.get('id');
11
+ const element=tag[1].toLowerCase(),type=(attributes.get('type')||'text').toLowerCase(),supported=element!=='input'||['text','search','email','tel','url','number','range','checkbox','color'].includes(type),shared=attributes.has('data-stillmade-share'),local=attributes.has('data-stillmade-local'),id=attributes.get('id');
12
+ // A fieldset is a control only when it shares its radio picks; otherwise it just groups controls.
13
+ if(element==='fieldset'&&!shared)continue;
12
14
  if(shared&&local){problems.push('A control cannot be both shared and device-local.');continue;}
13
15
  if(supported)controls.push({id,shared,local});
14
16
  if(!shared)continue;
@@ -50,6 +50,30 @@ export const ADAPTERS=Object.freeze([...SCALAR_ADAPTERS,...SCALAR_ADAPTERS.filte
50
50
  ...(adapter.resultSchema?{resultSchema:typeof adapter.resultSchema==='string'?`${adapter.resultSchema}[]`:Object.freeze([adapter.resultSchema])}:{}),
51
51
  element:Object.freeze({id:adapter.id,version:adapter.version}),
52
52
  }))]);
53
+ // Narration a Step takes from an earlier Step (the `narrationScript` and
54
+ // `narrationTiming` inputs of Shots 2.2.0 and Image Panels 2.4.0). Those inputs
55
+ // mean narration_script and timed_transcript, so a script Block whose output
56
+ // names another meaning (or none), or Transcribe's transcript_text, cannot
57
+ // connect to them directly. These conversions keep the words unchanged and are valid only
58
+ // where a connection pins them: StillMade's narration wiring when such a Step
59
+ // is added or replaced, or the person's "Use <Step>'s script". Compatibility
60
+ // searches and the resolver never choose them.
61
+ export const NARRATION_ADAPTERS=Object.freeze([
62
+ Object.freeze({...descriptor('script-as-narration','script','script'),targetSemantic:'narration_script',pinnedOnly:true,resultSchema:Object.freeze({schemaVersion:'integer',text:'string'})}),
63
+ Object.freeze({...descriptor('text-as-narration','text','script'),targetSemantic:'narration_script',pinnedOnly:true,resultSchema:Object.freeze({schemaVersion:'integer',text:'string'})}),
64
+ // Timed words (for example Transcribe's transcript of the Voiceover) as the narration timing.
65
+ Object.freeze({...descriptor('transcript-as-narration-timing','transcript','transcript'),targetSemantic:'timed_transcript',pinnedOnly:true}),
66
+ ]);
67
+ // The narration input asks for no role or structure; the output has the
68
+ // conversion's type and asks for no other role. Which output is the script is
69
+ // decided where the pin is made (narration-inputs.js), never here.
70
+ function pinnedNarration(out,into,adapter){
71
+ if(!plain(adapter)||!out||!into)return null;
72
+ const definition=NARRATION_ADAPTERS.find(item=>item.id===adapter.id&&item.version===adapter.version);
73
+ if(!definition||definition.output!==out.type||definition.input!==into.type||into.semantic!==definition.targetSemantic||into.role||into.schema!==undefined)return null;
74
+ if(out.role&&out.role!==definition.targetSemantic)return null;
75
+ return {compatible:true,adapter:pin(definition),requiresValue:true,contextFields:[]};
76
+ }
53
77
  const elementPort=contract=>{
54
78
  const result={...contract,type:contract.type.slice(0,-2),cardinality:'one'};
55
79
  if(Array.isArray(contract.schema) && contract.schema.length===1)result.schema=contract.schema[0];
@@ -75,7 +99,7 @@ export function connectionCompatibility(output,input,{semantics}={}) {
75
99
 
76
100
  /** Validate the declared connection and its optional saved adapter pin. */
77
101
  export function validateConnectionAdapter(output,input,{permissions=[],adapter,semantics}={}) {
78
- const match=connectionCompatibility(output,input,{semantics});
102
+ const match=pinnedNarration(port(output),port(input),adapter)||connectionCompatibility(output,input,{semantics});
79
103
  assert(match.compatible,'ADAPTER_TYPE',match.reason || 'These inputs and outputs cannot connect');
80
104
  if(adapter!==undefined){
81
105
  const valid=match.adapter===null?adapter===null:plain(adapter) && Object.keys(adapter).every(key=>['id','version'].includes(key)) && adapter.id===match.adapter.id && adapter.version===match.adapter.version;
@@ -127,7 +151,7 @@ export function adaptConnectionValue(output,input,value,{context={},permissions=
127
151
  let result=value;
128
152
  if(match.adapter){
129
153
  const id=match.adapter.id;
130
- const definition=ADAPTERS.find(item=>item.id===id),sequence=definition.steps;
154
+ const definition=ADAPTERS.find(item=>item.id===id)||NARRATION_ADAPTERS.find(item=>item.id===id),sequence=definition.steps;
131
155
  if(definition.element){
132
156
  assert(value.length<=100,'ADAPTER_LIMIT','Convert at most 100 items at a time.');
133
157
  const source=elementPort(out),target=elementPort(into);
@@ -149,6 +173,15 @@ export function adaptConnectionValue(output,input,value,{context={},permissions=
149
173
  }
150
174
  else if(id==='csv-to-table')result=csvToTable(value);
151
175
  else if(id==='text-to-script')result={schemaVersion:1,text:value};
176
+ else if(id==='script-as-narration'){
177
+ assert(typeof value.text==='string','ADAPTER_TYPE','This script needs readable words before it can be used as narration.');
178
+ result={schemaVersion:1,text:value.text};
179
+ }
180
+ else if(id==='text-as-narration')result={schemaVersion:1,text:value};
181
+ else if(id==='transcript-as-narration-timing'){
182
+ assert(Array.isArray(value.words),'ADAPTER_TYPE','This transcript has no word timings to use as narration timing.');
183
+ result=value;
184
+ }
152
185
  else if(id==='script-to-text'){
153
186
  assert(typeof value.text==='string','ADAPTER_TYPE','This script needs readable words before it can become text.');
154
187
  result=value.text;
@@ -10,7 +10,10 @@ export const BlockError=core.BlockError,assert=core.assert,plain=core.plain;
10
10
  export const PORT_FIELDS = Object.freeze(['type', 'required', 'default', 'min', 'max', 'description', 'role', 'primary', 'context', 'imageMode', ...SEMANTIC_PORT_FIELDS]);
11
11
  import {standardSemanticIsA} from './semantic-relationships.js';
12
12
  import {semanticDeclarationErrors} from './semantic-declarations.js';
13
- import {firstPartySdkPackage} from './first-party-packages.js';
13
+ // Reserved "sm." identities are checked against the generated id@version list,
14
+ // not the packages: importing them put 185 Block packages under every module
15
+ // that imports a contract helper (October 2026, every Vercel cold start).
16
+ import {isFirstPartySdkIdentity} from './first-party-identities.js';
14
17
  import {isTimelineRange,isApprovalDecision} from './interaction-values.js';
15
18
  import {validateStateMigrations} from './state-migrations.js';
16
19
  import {sdkCompatibility} from './sdk-compatibility.js';
@@ -131,7 +134,7 @@ export function validateManifest(manifest, { builtIn = false } = {}) {
131
134
  check(manifest.schemaVersion === 1, 'schemaVersion', 'Expected schemaVersion 1');
132
135
  check(ID.test(manifest.id || ''), 'id', 'Expected namespace.name');
133
136
  // The reserved namespace admits only registered independently executable SDK releases.
134
- check(manifest.id !== ROOT_CHAT_ID && (builtIn || !manifest.id?.startsWith('sm.') || !!firstPartySdkPackage(manifest)), 'id', 'Reserved platform block identity');
137
+ check(manifest.id !== ROOT_CHAT_ID && (builtIn || !manifest.id?.startsWith('sm.') || isFirstPartySdkIdentity(manifest)), 'id', 'Reserved platform block identity');
135
138
  check(VERSION.test(manifest.version || ''), 'version', 'Expected a semantic version such as 1.0.0');
136
139
  const compatibility=sdkCompatibility(manifest.sdkVersion);
137
140
  check(compatibility.status==='supported','sdkVersion',compatibility.message);
@@ -22,8 +22,8 @@ export const customInterface = {
22
22
  ...textCleanup,
23
23
  manifest:{...textCleanup.manifest,id:'example.clean-text-interface',version:'1.1.0',name:'Clean text studio',description:'Edit text, run the cleanup, and compare the result in a responsive self-contained workspace.'},
24
24
  view:{
25
- html:'<main class="clean-studio"><header><h1>Clean text</h1><p>Shape a draft and keep the result visible while settings move for the available space.</p></header><div class="clean-layout"><section class="clean-settings" aria-labelledby="settings-title"><h2 id="settings-title">Draft</h2><label for="source">Your text</label><textarea id="source" data-stillmade-input="text" data-stillmade-share="text"></textarea><div class="clean-actions"><button id="run" data-stillmade-action="run">Clean text</button></div></section><section class="clean-result" aria-labelledby="result-title"><h2 id="result-title">Result</h2><output id="result" aria-live="polite" data-stillmade-state="empty">Your result appears here.</output></section></div></main>',
26
- css:'.clean-studio{container:clean-studio/inline-size;min-width:0}.clean-studio>header p{max-width:60ch;color:var(--fg-muted)}.clean-layout{display:grid;grid-template-columns:minmax(0,2fr) minmax(240px,1fr);gap:16px;align-items:start}.clean-settings,.clean-result{min-width:0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3)}.clean-settings label{display:block;margin-bottom:8px}.clean-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.clean-actions button{min-width:44px;min-height:44px}.clean-result output{display:block;min-height:120px;white-space:pre-wrap;overflow-wrap:anywhere;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3)}@container clean-studio (max-width:600px){.clean-layout{grid-template-columns:minmax(0,1fr)}.clean-settings,.clean-result{padding:12px}}',
27
- javascript:'const source=document.getElementById("source"), button=document.getElementById("run"), result=document.getElementById("result"); StillMade.onInput(input=>{source.value=input.text||"";document.querySelector("main").dataset.stillmadeState=source.value?"populated":"empty"}); button.addEventListener("click",async()=>{button.disabled=true;result.dataset.stillmadeState="loading";result.textContent="Cleaning…";try{const output=await StillMade.run({text:source.value});result.dataset.stillmadeState="success";result.textContent=output.text||"The result is empty."}catch(error){result.dataset.stillmadeState="error";result.textContent=error.message}finally{button.disabled=false}});',
25
+ html:'<main class="sm-block" data-layout="split"><output class="sm-stage" id="result" aria-live="polite" data-stillmade-state="empty">Your result shows here</output><div class="sm-field"><label for="source">Text</label><textarea id="source" data-stillmade-input="text" data-stillmade-share="text"></textarea></div><div class="sm-actions"><button class="sm-primary" id="run" data-stillmade-action="run">Clean text</button></div></main>',
26
+ css:'#result{place-items:start;min-height:160px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--fg)}#result[data-stillmade-state="empty"]{place-items:center;color:var(--fg-muted)}',
27
+ javascript:'const source=document.getElementById("source"), button=document.getElementById("run"), result=document.getElementById("result"); StillMade.onInput(input=>{source.value=input.text||"";document.querySelector("main").dataset.stillmadeState=source.value?"populated":"empty"}); button.addEventListener("click",async()=>{button.disabled=true;result.dataset.stillmadeState="loading";result.textContent="Cleaning…";try{const output=await StillMade.run({text:source.value});result.dataset.stillmadeState="success";result.textContent=output.text||"Empty result"}catch(error){result.dataset.stillmadeState="error";result.textContent=error.message}finally{button.disabled=false}});',
28
28
  },
29
29
  };