stillmade-sdk 0.1.36 → 0.1.38

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 (60) hide show
  1. package/AGENTS.md +8 -8
  2. package/IMAGE_DESCRIPTION.md +2 -0
  3. package/MODULES.md +68 -10
  4. package/NATIVE_MCP.md +18 -7
  5. package/PASTE_TO_LLM.md +121 -50
  6. package/PORTABLE.md +7 -3
  7. package/README.md +19 -6
  8. package/REMIX.md +6 -0
  9. package/SDK.md +226 -103
  10. package/TRANSCRIPTION.md +4 -2
  11. package/UI_AESTHETIC.md +2 -0
  12. package/api-index.json +1 -1
  13. package/blocks/narration/package.js +1 -1
  14. package/blocks/narration/versions/1.0.1/package.js +221 -0
  15. package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +769 -0
  16. package/blocks/native/sm.board-editor/versions/2.2.11/package.js +551 -0
  17. package/blocks/native/sm.describe-media/versions/2.1.0/package.js +140 -0
  18. package/blocks/native/sm.panel-strip/versions/2.4.2/package.js +1316 -0
  19. package/blocks/native/sm.photo-notes/versions/1.1.0/package.js +170 -0
  20. package/blocks/native/sm.pipeline/versions/2.2.6/package.js +1095 -0
  21. package/blocks/screen-recorder/package.js +1 -1
  22. package/blocks/screen-recorder/versions/1.0.1/package.js +223 -0
  23. package/package.json +1 -1
  24. package/packages/block-cli/cli.js +91 -35
  25. package/packages/block-cli/dev-host/assets/host.js +35 -32
  26. package/packages/block-cli/dev-host/assets/index.css +1 -1
  27. package/packages/block-cli/dev-server.js +4 -0
  28. package/packages/block-cli/ready-gate.js +19 -4
  29. package/packages/block-cli/templates.js +33 -21
  30. package/packages/block-platform/block-readiness.js +10 -1
  31. package/packages/block-platform/catalog.js +14 -3
  32. package/packages/block-platform/chat-packages.js +6 -0
  33. package/packages/block-platform/flow-inspection.js +3 -1
  34. package/packages/block-platform/project-target.js +57 -0
  35. package/packages/block-platform/project-type-composer.js +4 -3
  36. package/packages/block-platform/project-types.js +5 -2
  37. package/packages/block-sdk/admission.js +4 -1
  38. package/packages/block-sdk/block-tier.js +23 -0
  39. package/packages/block-sdk/builder-contract.js +12 -10
  40. package/packages/block-sdk/capabilities.js +23 -10
  41. package/packages/block-sdk/collaboration-admission.js +3 -0
  42. package/packages/block-sdk/contracts.js +13 -1
  43. package/packages/block-sdk/example-media-data.js +16 -0
  44. package/packages/block-sdk/example-media.js +42 -0
  45. package/packages/block-sdk/first-party-identities.js +8 -0
  46. package/packages/block-sdk/first-party-packages.js +11 -1
  47. package/packages/block-sdk/index.d.ts +6 -2
  48. package/packages/block-sdk/local-admission.js +6 -5
  49. package/packages/block-sdk/media-transform.js +59 -3
  50. package/packages/block-sdk/mock-host.js +23 -2
  51. package/packages/block-sdk/mocks.js +4 -1
  52. package/packages/block-sdk/module-hosted.js +12 -2
  53. package/packages/block-sdk/port-types.d.ts +6 -0
  54. package/packages/block-sdk/port-types.js +32 -10
  55. package/packages/block-sdk/shared-agent-controls.js +25 -2
  56. package/packages/block-sdk/shared-controls.js +17 -3
  57. package/packages/block-sdk/testing.js +46 -3
  58. package/packages/block-sdk/view-design.js +67 -0
  59. package/packages/block-sdk/view-renderer.js +50 -8
  60. package/packages/design-system/block-kit.css +6 -4
@@ -36,7 +36,7 @@ import {textGenerate} from '../block-sdk/capability-example.js';
36
36
  import {scanPackage} from '../block-sdk/admission.js';
37
37
  import {admitLocalPackage} from '../block-sdk/local-admission.js';
38
38
  import {requiresDesktopExecution} from '../block-sdk/desktop-execution.js';
39
- import {digest,validateManifest} from '../block-sdk/contracts.js';
39
+ import {assertStorableText,digest,validateManifest,stableStringify} from '../block-sdk/contracts.js';
40
40
  import { testPackage, validatePackage } from '../block-sdk/testing.js';
41
41
  import { runPackageAsync } from '../block-sdk/runtime.js';
42
42
  import { validateProjectType } from '../block-platform/project-types.js';
@@ -76,20 +76,31 @@ const moduleExecutor=(pkg,host)=>{const files=moduleFiles.get(pkg);return files?
76
76
  // Stand-ins for host calls (hosted operations, files, network, connections,
77
77
  // storage, project reads): on with --mock, or whenever the folder has
78
78
  // tests/mocks.json. They never contact a provider or spend credits.
79
+ const blockFolder=async path=>{try{if((await lstat(path)).isDirectory())return path;}catch{}return null;};
80
+ const localFileReader=folder=>async name=>{
81
+ if(!folder)throw Object.assign(new Error('Local files are read from a Block folder.'),{code:'MODULE_FILE_READ'});
82
+ const full=resolve(folder,name);
83
+ if(!full.startsWith(folder+sep))throw Object.assign(new Error(`${name} is outside the Block folder.`),{code:'MODULE_FILE_READ'});
84
+ const info=await lstat(full);if(!info.isFile())throw Object.assign(new Error(`${name} is not a file.`),{code:'MODULE_FILE_READ'});
85
+ return new Uint8Array(await readFile(full));
86
+ };
79
87
  async function standIns(path,pkg,requested){
80
- let mocks,folder=null;
81
- try{if((await lstat(path)).isDirectory())folder=path;}catch{}
88
+ let mocks;const folder=await blockFolder(path);
82
89
  if(folder){try{mocks=JSON.parse(await readFile(join(folder,MOCKS_FILE),'utf8'));}catch(error){if(error.code!=='ENOENT')throw Object.assign(new Error(`${MOCKS_FILE}: ${error.message}`),{code:'MOCK_HOST'});}}
83
90
  if(!requested&&mocks===undefined)return null;
84
- const readLocalFile=async name=>{
85
- if(!folder)throw Object.assign(new Error('Local files are read from a Block folder.'),{code:'MODULE_FILE_READ'});
86
- const full=resolve(folder,name);
87
- if(!full.startsWith(folder+sep))throw Object.assign(new Error(`${name} is outside the Block folder.`),{code:'MODULE_FILE_READ'});
88
- const info=await lstat(full);if(!info.isFile())throw Object.assign(new Error(`${name} is not a file.`),{code:'MODULE_FILE_READ'});
89
- return new Uint8Array(await readFile(full));
90
- };
91
- return createMockHost({mocks:validateMocks(mocks),readLocalFile,manifest:pkg.manifest});
91
+ return createMockHost({mocks:validateMocks(mocks),readLocalFile:localFileReader(folder),manifest:pkg.manifest});
92
+ }
93
+ // A module fixture's own mocks take the place of tests/mocks.json for that fixture (MODULES.md), with or
94
+ // without --mock, as ready and StillMade's import review already did (October 27, 2026: test, pack and dev
95
+ // answered every fixture from tests/mocks.json, or left it unanswered when there was none).
96
+ // Returns hostFor(mocks), a fresh stand-in host for one fixture, or null for other runtimes.
97
+ async function fixtureStandIns(path,pkg){
98
+ if(pkg.manifest?.runtime!=='module')return null;
99
+ const readLocalFile=localFileReader(await blockFolder(path));
100
+ return mocks=>createMockHost({mocks:validateMocks(mocks),readLocalFile,manifest:pkg.manifest});
92
101
  }
102
+ // A run whose input is a fixture's (dev's Example picker, run without an input file) uses that fixture's mocks.
103
+ const fixtureFor=(pkg,input)=>pkg.manifest?.runtime==='module'?pkg.tests?.find(test=>test?.mocks&&stableStringify(test.input)===stableStringify(input)):undefined;
93
104
  // `dev`: preview the folder in StillMade's view host with hot reload. Hosted
94
105
  // calls always use stand-ins here; the page runs only on this machine.
95
106
  // --live also lets the StillMade site (--site, default STILLMADE_SITE_URL or
@@ -113,7 +124,7 @@ async function dev(path,options){
113
124
  }
114
125
  if(!(await lstat(path)).isDirectory())throw Object.assign(new Error('dev previews a Block folder.'),{code:'USAGE'});
115
126
  const {startDevServer}=await import('./dev-server.js');
116
- let mock,mocksText,memory;
127
+ let mock,mocksText,memory,answered;
117
128
  const load=async()=>{
118
129
  let pkg;
119
130
  try{pkg=await readPackage(path);}catch(error){return {problem:`${error.message}${error.code?` (${error.code})`:''}`};}
@@ -128,15 +139,18 @@ async function dev(path,options){
128
139
  };
129
140
  const run=async(pkg,input)=>{
130
141
  const state=pkg.manifest.state?memory??initialBlockState(pkg.manifest):undefined;
131
- const result=await runPackageAsync(pkg,input,{...(state?{state}:{}),...(pkg.manifest.runtime==='module'?{module:moduleExecutor(pkg,mock?.host)}:{}),...(pkg.manifest.runtime==='capability'&&mock?{capability:mock.capability}:{})});
142
+ // The Example picker sends that example's input: unedited, it runs against the example's own mocks.
143
+ const fixture=fixtureFor(pkg,input),own=fixture?(await fixtureStandIns(path,pkg))(fixture.mocks):null;
144
+ answered=own?{host:own,files:fixture.mocks.files}:null;
145
+ const result=await runPackageAsync(pkg,input,{...(state?{state}:{}),...(pkg.manifest.runtime==='module'?{module:moduleExecutor(pkg,(own||mock)?.host)}:{}),...(pkg.manifest.runtime==='capability'&&mock?{capability:mock.capability}:{})});
132
146
  if(pkg.manifest.state)memory=result.state;
133
- const calls=mock?.calls.splice(0)||[];
147
+ const calls=own?own.calls.map(call=>({...call,source:call.source==='mocks'?'fixture mocks':call.source})):mock?.calls.splice(0)||[];
134
148
  return {outputs:result.outputs,...(result.logs?{logs:result.logs}:{}),...(calls.length?{standIns:calls}:{}),...(result.state!==undefined?{state:result.state}:{})};
135
149
  };
136
150
  const media=async url=>{
137
- const made=mock?.file(url);if(made)return made;
151
+ const made=answered?.host.file(url)||mock?.file(url);if(made)return made;
138
152
  let mocks={};try{mocks=JSON.parse(mocksText||'{}');}catch{}
139
- const local=mocks.files?.[url];if(typeof local!=='string')return null;
153
+ const local=answered?.files?.[url]??mocks.files?.[url];if(typeof local!=='string')return null;
140
154
  const full=resolve(path,local);if(!full.startsWith(path+sep))return null;
141
155
  try{const bytes=await readFile(full);return {bytes,mediaType:{png:'image/png',jpg:'image/jpeg',jpeg:'image/jpeg',webp:'image/webp',gif:'image/gif',mp4:'video/mp4',webm:'video/webm',mp3:'audio/mpeg',wav:'audio/wav',m4a:'audio/mp4',ogg:'audio/ogg'}[full.split('.').pop().toLowerCase()]||'application/octet-stream'};}catch{return null;}
142
156
  };
@@ -149,7 +163,8 @@ async function dev(path,options){
149
163
  const liveUrl=link&&`${link.origin}/create/import?live=${encodeURIComponent(`${server.url}/__live/${link.key}`)}`;
150
164
  return {dev:server.url,folder:path,...(build?{build}:{}),...(liveUrl?{live:liveUrl}:{}),close:server.close};
151
165
  }
152
- const standInNote=mock=>mock?.calls.length?{standIns:mock.calls.map(call=>({api:call.api,name:call.name,source:call.source==='mocks'?MOCKS_FILE:'recorded stand-in'})),liveVerified:false}:{};
166
+ // own: the hosts that answered fixtures from their own mocks.
167
+ const standInNote=(mock,own=[])=>{const calls=[...(mock?.calls||[]).map(call=>[call,MOCKS_FILE]),...own.flatMap(host=>host.calls.map(call=>[call,'fixture mocks']))];return calls.length?{standIns:calls.map(([call,file])=>({api:call.api,name:call.name,source:call.source==='mocks'?file:'recorded stand-in'})),liveVerified:false}:{};};
153
168
  const mobileProfile=()=>({policy:PLATFORM_CHECK_POLICY,targets:PLATFORM_CHECK_TARGETS,themes:PLATFORM_CHECK_THEMES,passed:null,message:'Static SDK validation passed. Import this exact source or run the repository platform runner to complete the trusted embedded UI profile.'});
154
169
  export async function readPackage(path) {
155
170
  const info = await lstat(path);
@@ -188,7 +203,7 @@ export async function main(rawArgs) {
188
203
  }
189
204
  if (command === 'create') {
190
205
  if(args.length<2)throw Object.assign(new Error('Name the folder to create: stillmade-block create my-block [template]'),{code:'USAGE'});
191
- const template=templateNamed(extra),files=await template.files();
206
+ const template=templateNamed(extra),files=await template.files({folder:path});
192
207
  // Every new Block starts with a feature map (one entry from its name and description) for the builder to rewrite as it grows.
193
208
  {const created=JSON.parse(files['stillmade.block.json']);if(!created.features&&!created.stages){created.features=blockFeatures(created).map(({source,...feature})=>feature);files['stillmade.block.json']=`${JSON.stringify(created,null,2)}\n`;}}
194
209
  // Editors pick up the generated types for the view, src/run.js and module code.
@@ -198,15 +213,18 @@ export async function main(rawArgs) {
198
213
  await mkdir(path,{recursive:true});
199
214
  const names=['stillmade.block.json',...Object.keys(files).filter(name=>name!=='stillmade.block.json')];
200
215
  for(const name of names){await mkdir(dirname(join(path,name)),{recursive:true});await writeFile(join(path,name),files[name],{flag:'wx'});}
201
- // Coding agents read command output: restate the readiness rule where they will see it.
202
- return { created: path, sdkVersion: '0.1.0', template:template.name, runtime:template.runtime, files:names, next: 'Build the Block and write its feature map (`manifest.features`: one entry per thing it can do), then run `stillmade-block gate .`. Do not call the Block ready until the gate passes; report the status it prints.' };
216
+ // Coding agents read command output: restate the readiness rule where they will see it, with the folder as it
217
+ // was typed (October 27, 2026: `gate .` in the folder holding an extracted SDK judged the SDK's examples too).
218
+ return { created: path, sdkVersion: '0.1.0', template:template.name, runtime:template.runtime, files:names, next: `Build the Block and write its feature map (\`manifest.features\`: one entry per thing it can do), then run \`stillmade-block gate ${/\s/.test(target)?`"${target}"`:target}\`. Do not call the Block ready until the gate passes; report the status it prints.` };
203
219
  }
204
220
  if (command === 'gate') {
205
221
  // Every new or edited Block in a folder must pass readiness before anyone calls it ready.
206
222
  const {runReadyGate,gateMessage}=await import('./ready-gate.js');
207
- const verdict=await runReadyGate({root:path,main});
208
- if(!verdict.passed)throw Object.assign(new Error(gateMessage(verdict)),{code:'READINESS_GATE_FAILED',report:{results:verdict.results}});
209
- return {passed:true,checked:verdict.checked,results:verdict.results.map(({package:name,status,limitations})=>({package:name,status,limitations}))};
223
+ // Each ready this gate runs says how many fixtures ran and passed (a verdict reused for unchanged content has no count).
224
+ const counts=new Map(),judge=async gateArgs=>{try{const result=await main(gateArgs);if(gateArgs[0]==='ready')counts.set(gateArgs[1],result?.fixtures);return result;}catch(error){if(gateArgs[0]==='ready')counts.set(gateArgs[1],error.report?.fixtures);throw error;}};
225
+ const verdict=await runReadyGate({root:path,main:judge}),counted=result=>{const fixtures=counts.get(resolve(path,result.package));return fixtures?{fixtures}:{};};
226
+ if(!verdict.passed)throw Object.assign(new Error(gateMessage(verdict)),{code:'READINESS_GATE_FAILED',report:{results:verdict.results.map(result=>({...result,...counted(result)}))}});
227
+ return {passed:true,checked:verdict.checked,results:verdict.results.map(({package:name,status,limitations})=>({package:name,status,limitations,...counted({package:name})}))};
210
228
  }
211
229
  if(command==='run')return main(['preview',...args.slice(1),...(mockRequested?['--mock']:[])]);
212
230
  if(command==='dev')return dev(path,args.slice(2));
@@ -237,10 +255,15 @@ export async function main(rawArgs) {
237
255
  // 2026: ready refused every module Block at DOD-01, so none could pass the gate from the SDK).
238
256
  const module=pkg.manifest?.runtime==='module';
239
257
  if(module&&noModuleExecution)throw Object.assign(new Error(MODULE_EXECUTION_REQUIRED_MESSAGE),{code:'MODULE_EXECUTION_REQUIRED'});
240
- const mock=module?await standIns(path,pkg,true):null;
241
- const readiness=await assessBlockReadiness(pkg,{moduleExecution:true,files:moduleFiles.get(pkg),...(mock?{host:mock.host}:{})});
242
- if(['FAILED','BLOCKED BY PLATFORM CAPABILITY'].includes(readiness.status))throw Object.assign(new Error(`Block readiness: ${readiness.status}`),{report:readiness});
243
- return readiness;
258
+ const mock=module?await standIns(path,pkg,true):null,hostFor=await fixtureStandIns(path,pkg),files=moduleFiles.get(pkg);
259
+ // Admission runs first, exactly as readiness would run it, so the report can say how many fixtures ran
260
+ // and passed (October 27, 2026); readiness keeps its own budget (20 s by default) for both.
261
+ const started=Date.now();let admission;
262
+ try{admission=await admitLocalPackage(pkg,{moduleExecution:true,files,...(mock?{host:mock.host}:{}),...(hostFor?{hostFor:mocks=>hostFor(mocks).host}:{})});}catch{}
263
+ const readiness=await assessBlockReadiness(pkg,{moduleExecution:true,files,...(mock?{host:mock.host}:{}),...(admission?{admission,budgetMs:Math.max(1000,20000-(Date.now()-started))}:{})});
264
+ const report={...readiness,...(admission?{fixtures:fixtureCounts(pkg,admission)}:{})};
265
+ if(['FAILED','BLOCKED BY PLATFORM CAPABILITY'].includes(readiness.status))throw Object.assign(new Error(`Block readiness: ${readiness.status}`),{report});
266
+ return report;
244
267
  }
245
268
  if(command==='remix'){
246
269
  const result=await remixBlock(pkg);
@@ -252,6 +275,8 @@ export async function main(rawArgs) {
252
275
  return {file:output,parent:result.parent,digest:await digest(result.content),draft:true,validated:false,message:'Editable remix created. Run validate and test before packaging a release. No source code was executed by remix.'};
253
276
  }
254
277
 
278
+ // StillMade cannot store text with a NUL character; say where it is before the author imports the package.
279
+ if(['validate','test','pack','ready'].includes(command))assertStorableText(pkg);
255
280
  if (pkg.stages) {
256
281
  const valid = validateProjectType(pkg, BUILTIN_BLOCKS);
257
282
  if (!valid.valid) throw new Error(valid.errors.map(e => e.message).join('. '));
@@ -332,10 +357,11 @@ export async function main(rawArgs) {
332
357
  if(saved.packageDigest!==sourceDigest)throw Error('Saved memory belongs to another Block build');
333
358
  state=validateBlockState(pkg.manifest,saved.state);
334
359
  }
335
- const mock=await standIns(path,pkg,mockRequested);
336
- const result=await runPackageAsync(pkg,inputPath?JSON.parse(await readFile(resolve(inputPath),'utf8')):pkg.tests[0].input,{...(state?{state}:{}),...(pkg.manifest.runtime==='module'?{module:moduleExecutor(pkg,mock?.host)}:{}),...(pkg.manifest.runtime==='capability'&&mock?{capability:mock.capability}:{})});
360
+ const mock=await standIns(path,pkg,mockRequested),input=inputPath?JSON.parse(await readFile(resolve(inputPath),'utf8')):pkg.tests[0].input;
361
+ const fixture=fixtureFor(pkg,input),own=fixture?(await fixtureStandIns(path,pkg))(fixture.mocks):null;
362
+ const result=await runPackageAsync(pkg,input,{...(state?{state}:{}),...(pkg.manifest.runtime==='module'?{module:moduleExecutor(pkg,(own||mock)?.host)}:{}),...(pkg.manifest.runtime==='capability'&&mock?{capability:mock.capability}:{})});
337
363
  if(savePath)await writeFile(resolve(savePath),JSON.stringify({packageDigest:sourceDigest,state:validateBlockState(pkg.manifest,result.state)},null,2),{flag:'wx'});
338
- return {...result,...standInNote(mock)};
364
+ return {...result,...(own?standInNote(null,[own]):standInNote(mock))};
339
365
  }
340
366
  if(requiresDesktopExecution(pkg)&&['test','pack'].includes(command)){
341
367
  const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Desktop source admission failed'),{report});
@@ -351,29 +377,59 @@ export async function main(rawArgs) {
351
377
  await writeFile(output,JSON.stringify(pkg,null,2),{flag:'wx'});
352
378
  return {file:output,digest:await digest(pkg),tests:0,reviewRequired:true,...(pkg.manifest.runtime==='capability'?{liveVerified:false}:{}),message:pkg.manifest.runtime==='capability'?'Source packaged without provider execution. Import into StillMade, choose a model and payment method, and approve the fixtures and sample before confirming installation.':'Source packaged. Import into StillMade to run all fixtures and preview on your connected ComfyUI backend before confirming installation.'};
353
379
  }
354
- const mock=['module','capability'].includes(pkg.manifest.runtime)?await standIns(path,pkg,mockRequested):null;
380
+ const mock=['module','capability'].includes(pkg.manifest.runtime)?await standIns(path,pkg,mockRequested):null,hostFor=await fixtureStandIns(path,pkg),own=[];
355
381
  let report;
356
382
  if(pkg.manifest.runtime==='capability'&&mock){
357
383
  // Checks on the model's words are listed rather than run against a stand-in.
358
384
  const {pkg:checked,notChecked}=standInFixtures(pkg);
359
385
  report={...await testPackage(checked,{capability:mock.capability}),digest:await digest(pkg),execution:'stand-ins',liveVerified:false,reviewRequired:true,...(notChecked.length?{notChecked}:{})};
360
- }else report = await testPackage(pkg,pkg.manifest.runtime==='module'?{module:moduleExecutor(pkg,mock?.host),timeoutMs:MODULE_LIMITS.wallClockMs}:{});
361
- Object.assign(report,standInNote(mock));
386
+ }else if(pkg.manifest.runtime==='module'){
387
+ const module=moduleExecutor(pkg,mock?.host);
388
+ const moduleFor=module&&(test=>{if(!test?.mocks)return module;const host=hostFor(test.mocks);own.push(host);return moduleExecutor(pkg,host.host);});
389
+ report=await testPackage(pkg,{module,...(moduleFor?{moduleFor}:{}),timeoutMs:MODULE_LIMITS.wallClockMs});
390
+ }else report = await testPackage(pkg,{});
391
+ Object.assign(report,standInNote(mock,own));
362
392
  if (!report.passed) throw Object.assign(new Error('Fixture tests failed'), { report });
363
393
  if (command === 'test') return report;
364
394
  if (hasDigestFiles(pkg)) {
365
395
  const files=moduleFiles.get(pkg),output=resolve(extra || `${pkg.manifest.id}-${pkg.manifest.version}${MODULE_BUNDLE_SUFFIX}`);
366
396
  if(!output.endsWith(MODULE_BUNDLE_SUFFIX))throw new Error(`Module output must end in ${MODULE_BUNDLE_SUFFIX}`);
367
397
  await writeFile(output,JSON.stringify(createModuleBundle(pkg,files)),{flag:'wx'});
368
- return {file:output,digest:report.digest,tests:report.results.length,files:Object.keys(pkg.module?.files||{}).length+Object.keys(pkg.view?.files?.files||{}).length,...standInNote(mock),message:'Import this bundle in StillMade (Create, then Import). StillMade checks every file digest and uploads the files once before saving the Block.'};
398
+ return {file:output,digest:report.digest,tests:report.results.length,files:Object.keys(pkg.module?.files||{}).length+Object.keys(pkg.view?.files?.files||{}).length,...standInNote(mock,own),message:'Import this bundle in StillMade (Create, then Import). StillMade checks every file digest and uploads the files once before saving the Block.'};
369
399
  }
370
400
  const output = jsonOutput(extra,`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`);
371
401
  await writeFile(output, JSON.stringify(pkg, null, 2), { flag: 'wx' });
372
402
  return { file: output, digest: report.digest, tests: report.results.length };
373
403
  }
404
+ // How many fixtures admission ran and passed, for the ready and gate reports.
405
+ function fixtureCounts(pkg,admission){
406
+ const rows=(admission.results||[]).filter(row=>/ contract test: /.test(row.name)),failed=rows.filter(row=>!row.passed);
407
+ return {total:pkg.tests?.length||0,ran:rows.length,passed:rows.length-failed.length,...(admission.execution?{execution:admission.execution}:{}),...(failed.length?{failed:failed.slice(0,10).map(row=>({name:row.name.replace(/^.*? contract test: /,''),message:String(row.message||row.code||'failed').slice(0,300)}))}:{})};
408
+ }
409
+ // Readable ready and gate results, which format.js prints as JSON (October 27, 2026: --text printed JSON): the
410
+ // verdict, how many fixtures ran and passed, then what failed and what is not verified.
411
+ const plural=(count,word)=>`${count} ${word}${count===1?'':'s'}`;
412
+ const fixtureText=fixtures=>fixtures.ran?`${fixtures.passed} of ${plural(fixtures.total,'fixture')} passed`:`${plural(fixtures.total,'fixture')} not run here`;
413
+ const readinessLines=report=>{
414
+ const failed=(report.items||[]).filter(item=>['failed','blocked'].includes(item.state));
415
+ return [report.fixtures?`${fixtureText(report.fixtures)}.`:null,...(report.fixtures?.failed||[]).map(item=>` FAIL ${item.name}: ${item.message}`),report.fixtures?.execution==='local-stand-ins'?'Hosted calls were answered by each fixture\'s own mocks, else tests/mocks.json, else stand-ins. No provider was called.':null,
416
+ failed.length?'Failed:':null,...failed.map(item=>` ${item.id} ${item.title}: ${item.reason}`),report.limitations?.length?`Not verified (${report.limitations.length}):`:null,...(report.limitations||[]).map(line=>` ${line}`)].filter(Boolean);
417
+ };
418
+ const gateLines=results=>results.map(result=>` ${result.package}: ${result.status}${result.fixtures?`, ${fixtureText(result.fixtures)}`:''}${result.limitations?`, ${result.limitations} not verified`:''}`);
419
+ export function readinessText(command,result){
420
+ if(command==='ready'&&Array.isArray(result?.items))return `${[`Readiness: ${result.status}`,...readinessLines(result)].join('\n')}\n`;
421
+ if(command==='gate'&&Array.isArray(result?.results))return `${[result.checked?`Readiness gate passed: ${plural(result.checked,'Block')} checked.`:'Readiness gate passed: no new or edited Blocks to check.',...gateLines(result.results)].join('\n')}\n`;
422
+ return null;
423
+ }
424
+ export function readinessErrorText(command,error){
425
+ const report=error?.report,head=`Error: ${error?.message||error}${error?.code?` (${error.code})`:''}`;
426
+ if(command==='ready'&&Array.isArray(report?.items))return `${[head,...readinessLines(report)].join('\n')}\n`;
427
+ if(command==='gate'&&Array.isArray(report?.results))return `${[head,'Checked:',...gateLines(report.results)].join('\n')}\n`;
428
+ return null;
429
+ }
374
430
  if (process.argv[1] && (()=>{try{return realpathSync(process.argv[1])===fileURLToPath(import.meta.url);}catch{return false;}})()) {
375
431
  // Readable text in a terminal; the JSON result object when piped (scripts,
376
432
  // coding agents) or with --json. --text forces readable text.
377
433
  const argv=process.argv.slice(2),asJson=argv.includes('--json')||!process.stdout.isTTY&&!argv.includes('--text'),args=argv.filter(value=>!['--json','--text'].includes(value));
378
- main(args).then(result => process.stdout.write(asJson?`${JSON.stringify(result, null, 2)}\n`:formatResult(args,result))).catch(error => { process.stderr.write(asJson?`${JSON.stringify({ error: error.message, code: error.code, report: error.report })}\n`:formatError(error)); process.exitCode = 1; });
434
+ main(args).then(result => process.stdout.write(asJson?`${JSON.stringify(result, null, 2)}\n`:readinessText(args[0],result)??formatResult(args,result))).catch(error => { process.stderr.write(asJson?`${JSON.stringify({ error: error.message, code: error.code, report: error.report })}\n`:readinessErrorText(args[0],error)??formatError(error)); process.exitCode = 1; });
379
435
  }