m0saic 0.3.1 → 0.4.0

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 (226) hide show
  1. package/README.md +23 -16
  2. package/dist/browseE2e.js +1 -1
  3. package/dist/connectionsFromEnv.js +1 -1
  4. package/dist/e2eTemplateVariants.js +1 -1
  5. package/dist/frontDoor.js +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/inlineDsl.js +1 -1
  8. package/dist/mcp/bridgeClient.js +1 -1
  9. package/dist/mcp/server.js +1 -1
  10. package/dist/mcp/tools.js +1 -1
  11. package/dist/mosaicxRunner.js +1 -1
  12. package/dist/outputPattern.js +1 -1
  13. package/dist/showcaseFeed.js +1 -1
  14. package/dist/templateDoctor.js +1 -1
  15. package/dist/utils/checkFeasibility.js +1 -1
  16. package/dist/utils/collectRenderErrors.js +1 -1
  17. package/dist/utils/enrichImageAlphaFromDoc.js +1 -1
  18. package/dist/utils/ensureFfmpeg.js +1 -1
  19. package/dist/utils/extractLayoutMeta.js +1 -1
  20. package/dist/utils/failureHints.js +1 -1
  21. package/dist/utils/ffmpegProbe.js +1 -1
  22. package/dist/utils/formatM0saicForCli.js +1 -1
  23. package/dist/utils/hashFile.js +1 -1
  24. package/dist/utils/helpGroups.js +1 -1
  25. package/dist/utils/installContext.js +1 -1
  26. package/dist/utils/invocation.js +1 -1
  27. package/dist/utils/licenseActivation.js +1 -1
  28. package/dist/utils/licenseActivationWorker.js +1 -1
  29. package/dist/utils/mosaicSpinner.js +1 -1
  30. package/dist/utils/parseDimensions.js +1 -1
  31. package/dist/utils/pathInfo.js +1 -1
  32. package/dist/utils/readJsonArg.js +1 -1
  33. package/dist/utils/renderOutcome.js +1 -1
  34. package/dist/utils/renderReport.js +1 -1
  35. package/dist/utils/renderSignals.js +1 -1
  36. package/dist/utils/renderWorkspace.js +1 -1
  37. package/dist/utils/reportOutputFormat.js +1 -1
  38. package/dist/utils/resolveLayoutFileProps.js +1 -0
  39. package/dist/utils/setupFfmpeg.js +1 -1
  40. package/dist/utils/telemetry.js +1 -1
  41. package/dist/utils/telemetryFlushWorker.js +1 -1
  42. package/dist/utils/telemetryUpstream.js +1 -1
  43. package/dist/utils/telemetryUsage.js +1 -1
  44. package/dist/utils/templateInstall.js +1 -0
  45. package/dist/utils/templateRepoKeys.js +1 -0
  46. package/dist/utils/templateSources.js +1 -0
  47. package/dist/utils/toolchainConfig.js +1 -1
  48. package/dist/utils/tutorialDuration.js +1 -1
  49. package/dist/utils/updateCheck.js +1 -1
  50. package/dist/utils/updateCheckWorker.js +1 -1
  51. package/dist/utils/validateSourceCounts.js +1 -1
  52. package/dist/utils/versions.js +1 -1
  53. package/dist/webCustomizeLink.js +1 -1
  54. package/node_modules/@m0saic/core/dist/core/buildChildMosaicSource.js +1 -1
  55. package/node_modules/@m0saic/core/dist/core/buildChunkCommands.js +1 -1
  56. package/node_modules/@m0saic/core/dist/core/buildEncodeCommands.js +1 -1
  57. package/node_modules/@m0saic/core/dist/core/buildLavfiSource.js +1 -1
  58. package/node_modules/@m0saic/core/dist/core/buildMediaSource.js +1 -1
  59. package/node_modules/@m0saic/core/dist/core/buildMosaicNode.js +1 -1
  60. package/node_modules/@m0saic/core/dist/core/buildMosaicPlanFromFile.js +1 -1
  61. package/node_modules/@m0saic/core/dist/core/buildSvgTextSource.js +1 -1
  62. package/node_modules/@m0saic/core/dist/core/buildTextRenderCommand.js +1 -1
  63. package/node_modules/@m0saic/core/dist/core/collectRefTargets.js +1 -1
  64. package/node_modules/@m0saic/core/dist/core/index.js +1 -1
  65. package/node_modules/@m0saic/core/dist/core/maskCacheDir.js +1 -1
  66. package/node_modules/@m0saic/core/dist/core/maskWorkerBridge.js +1 -1
  67. package/node_modules/@m0saic/core/dist/core/playbackPlaySpeed.js +1 -1
  68. package/node_modules/@m0saic/core/dist/core/prepareProps.js +1 -1
  69. package/node_modules/@m0saic/core/dist/core/primaryOutput.js +1 -1
  70. package/node_modules/@m0saic/core/dist/core/rasterizeMaskPng.js +1 -1
  71. package/node_modules/@m0saic/core/dist/core/rasterizeSvgMediaPng.js +1 -1
  72. package/node_modules/@m0saic/core/dist/core/refTargetTap.js +1 -1
  73. package/node_modules/@m0saic/core/dist/core/resolveMasks.js +1 -1
  74. package/node_modules/@m0saic/core/dist/core/resolveMasksSync.js +1 -1
  75. package/node_modules/@m0saic/core/dist/core/resolveStrokeRingsSync.js +1 -1
  76. package/node_modules/@m0saic/core/dist/core/roundedRectMask.js +1 -1
  77. package/node_modules/@m0saic/core/dist/core/strokeRing.js +1 -1
  78. package/node_modules/@m0saic/core/dist/core/svgRaster.js +1 -1
  79. package/node_modules/@m0saic/core/dist/defaults.js +1 -1
  80. package/node_modules/@m0saic/core/dist/ffmpeg/backgroundImageInput.js +1 -1
  81. package/node_modules/@m0saic/core/dist/ffmpeg/bufferGuard.js +1 -1
  82. package/node_modules/@m0saic/core/dist/ffmpeg/ffmpegCommands.js +1 -1
  83. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyBackdropBlur.js +1 -1
  84. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyBlur.js +1 -1
  85. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyBorderRadius.js +1 -1
  86. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyCamera.js +1 -1
  87. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyChromaKey.js +1 -1
  88. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyFade.js +1 -1
  89. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyGrade.js +1 -1
  90. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyMask.js +1 -1
  91. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyNoise.js +1 -1
  92. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyPixelize.js +1 -1
  93. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyRotate.js +1 -1
  94. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyStroke.js +1 -1
  95. package/node_modules/@m0saic/core/dist/ffmpeg/filters/applyTileEffects.js +1 -1
  96. package/node_modules/@m0saic/core/dist/ffmpeg/filters/coverCrop.js +1 -1
  97. package/node_modules/@m0saic/core/dist/ffmpeg/filters/index.js +1 -1
  98. package/node_modules/@m0saic/core/dist/ffmpeg/filters/sourceRectCrop.js +1 -1
  99. package/node_modules/@m0saic/core/dist/ffmpeg/gridLowering.js +1 -1
  100. package/node_modules/@m0saic/core/dist/ffmpeg/index.js +1 -1
  101. package/node_modules/@m0saic/core/dist/ffmpeg/splitAvMux.js +1 -1
  102. package/node_modules/@m0saic/core/dist/ffmpeg/stillOutput.js +1 -0
  103. package/node_modules/@m0saic/core/dist/ffmpeg/threadGuard.js +1 -1
  104. package/node_modules/@m0saic/core/dist/flatten/index.js +1 -1
  105. package/node_modules/@m0saic/core/dist/fs/index.js +1 -1
  106. package/node_modules/@m0saic/core/dist/fs/tempWorkspace.js +1 -1
  107. package/node_modules/@m0saic/core/dist/index.js +1 -1
  108. package/node_modules/@m0saic/core/dist/media/buildAssetManifestFromInputs.js +1 -1
  109. package/node_modules/@m0saic/core/dist/media/buildMediaRegistryFromInput.js +1 -1
  110. package/node_modules/@m0saic/core/dist/media/collectMediaInputsFromInvocationProps.js +1 -1
  111. package/node_modules/@m0saic/core/dist/media/collectMediaInputsFromMosaicRenderableFile.js +1 -1
  112. package/node_modules/@m0saic/core/dist/media/collectMediaInputsFromProps.js +1 -1
  113. package/node_modules/@m0saic/core/dist/media/index.js +1 -1
  114. package/node_modules/@m0saic/core/dist/media/internalMintAsset.js +1 -1
  115. package/node_modules/@m0saic/core/dist/media/remoteUrl.js +1 -1
  116. package/node_modules/@m0saic/core/dist/media/stageUnsafeFilenames.js +1 -1
  117. package/node_modules/@m0saic/core/dist/media/types.js +1 -1
  118. package/node_modules/@m0saic/core/dist/output/index.js +1 -1
  119. package/node_modules/@m0saic/core/dist/output/resolveOutputFormat.js +1 -1
  120. package/node_modules/@m0saic/core/dist/output/types.js +1 -1
  121. package/node_modules/@m0saic/core/dist/probe/cellLuminanceProbe.js +1 -1
  122. package/node_modules/@m0saic/core/dist/probe/extractSubtitleCues.js +1 -1
  123. package/node_modules/@m0saic/core/dist/probe/helpers.js +1 -1
  124. package/node_modules/@m0saic/core/dist/probe/index.js +1 -1
  125. package/node_modules/@m0saic/core/dist/probe/luminanceProbe.js +1 -1
  126. package/node_modules/@m0saic/core/dist/probe/probeMedia.js +1 -1
  127. package/node_modules/@m0saic/core/dist/probe/runFFprobe.js +1 -1
  128. package/node_modules/@m0saic/core/dist/probe/types.js +1 -1
  129. package/node_modules/@m0saic/core/dist/runtime/engineRedaction.js +1 -1
  130. package/node_modules/@m0saic/core/dist/runtime/ffmpegFailure.js +1 -1
  131. package/node_modules/@m0saic/core/dist/runtime/filterPreflight.js +1 -1
  132. package/node_modules/@m0saic/core/dist/runtime/hashFile.js +1 -1
  133. package/node_modules/@m0saic/core/dist/runtime/index.js +1 -1
  134. package/node_modules/@m0saic/core/dist/runtime/pathInfo.js +1 -1
  135. package/node_modules/@m0saic/core/dist/runtime/perfHeatmap.js +1 -1
  136. package/node_modules/@m0saic/core/dist/runtime/perfReport.js +1 -1
  137. package/node_modules/@m0saic/core/dist/runtime/pipelineM0p.js +1 -1
  138. package/node_modules/@m0saic/core/dist/runtime/renderCostModel.constants.js +1 -1
  139. package/node_modules/@m0saic/core/dist/runtime/renderCostModel.js +1 -1
  140. package/node_modules/@m0saic/core/dist/runtime/renderReport.js +1 -1
  141. package/node_modules/@m0saic/core/dist/runtime/renderSidecars.js +1 -1
  142. package/node_modules/@m0saic/core/dist/runtime/resolveMosaicx.js +1 -1
  143. package/node_modules/@m0saic/core/dist/runtime/runPlan.js +1 -1
  144. package/node_modules/@m0saic/core/dist/runtime/types.js +1 -1
  145. package/node_modules/@m0saic/core/dist/runtime/upstream.js +1 -1
  146. package/node_modules/@m0saic/core/dist/runtime/writeSidecars.js +1 -1
  147. package/node_modules/@m0saic/core/dist/types/chunk.js +1 -1
  148. package/node_modules/@m0saic/core/dist/types/index.js +1 -1
  149. package/node_modules/@m0saic/core/dist/types/mosaic-plan.js +1 -1
  150. package/node_modules/@m0saic/core/dist/utils/assertTiming.js +1 -1
  151. package/node_modules/@m0saic/core/dist/utils/command.js +1 -1
  152. package/node_modules/@m0saic/core/dist/utils/createEngineContext.js +1 -1
  153. package/node_modules/@m0saic/core/dist/utils/createTemplateRenderCache.js +1 -1
  154. package/node_modules/@m0saic/core/dist/utils/ffmpegColor.js +1 -1
  155. package/node_modules/@m0saic/core/dist/utils/formatDuration.js +1 -1
  156. package/node_modules/@m0saic/core/dist/utils/fsSafeId.js +1 -1
  157. package/node_modules/@m0saic/core/dist/utils/index.js +1 -1
  158. package/node_modules/@m0saic/core/dist/utils/lowerRenderableToMosaicDocument.js +1 -1
  159. package/node_modules/@m0saic/core/dist/utils/movSafeCodec.js +1 -1
  160. package/node_modules/@m0saic/core/dist/utils/resolveBoxFrac.js +1 -1
  161. package/node_modules/@m0saic/core/dist/validate/index.js +1 -1
  162. package/node_modules/@m0saic/core/package.json +1 -1
  163. package/node_modules/@m0saic/core/scripts/mask-rasterize-core.js +1 -1
  164. package/node_modules/@m0saic/core/scripts/mask-rasterize-worker.js +1 -1
  165. package/node_modules/@m0saic/core/scripts/mask-worker-thread.js +1 -1
  166. package/node_modules/@m0saic/dictionary/package.json +1 -1
  167. package/node_modules/@m0saic/dsl-file-formats/dist/cjs/m0v/m0vFile.js +2 -2
  168. package/node_modules/@m0saic/dsl-file-formats/package.json +1 -1
  169. package/node_modules/@m0saic/knowledge/docs/README.md +1 -0
  170. package/node_modules/@m0saic/knowledge/docs/file-formats/m0p-and-custom-field.md +1 -1
  171. package/node_modules/@m0saic/knowledge/docs/runtime/cli-usage.md +186 -46
  172. package/node_modules/@m0saic/knowledge/docs/runtime/ffmpeg-expression-limits.md +7 -0
  173. package/node_modules/@m0saic/knowledge/docs/templates/README.md +5 -1
  174. package/node_modules/@m0saic/knowledge/docs/templates/patterns/print-authoring.md +97 -0
  175. package/node_modules/@m0saic/knowledge/docs/templates/reference/brand-schema.md +73 -0
  176. package/node_modules/@m0saic/knowledge/docs/templates/reference/prop-bindings.md +31 -1
  177. package/node_modules/@m0saic/knowledge/docs/templates/render-lifecycle.md +26 -1
  178. package/node_modules/@m0saic/knowledge/docs/templates/ui-controls.md +42 -0
  179. package/node_modules/@m0saic/knowledge/package.json +1 -1
  180. package/node_modules/@m0saic/platform/dist/cjs/mosaic/propBindings/index.js +1 -1
  181. package/node_modules/@m0saic/platform/dist/cjs/mosaic/propBindings/propBindings.js +1 -1
  182. package/node_modules/@m0saic/platform/dist/cjs/share/shareQuery.js +1 -1
  183. package/node_modules/@m0saic/platform/package.json +1 -1
  184. package/node_modules/@m0saic/product/dist/__fixtures__/tarGz.js +1 -0
  185. package/node_modules/@m0saic/product/dist/communityRelease.js +1 -1
  186. package/node_modules/@m0saic/product/dist/freeTierLicense.js +1 -1
  187. package/node_modules/@m0saic/product/dist/index.js +1 -1
  188. package/node_modules/@m0saic/product/dist/installedTemplateRepos.js +1 -0
  189. package/node_modules/@m0saic/product/dist/licenseActivationNotice.js +1 -1
  190. package/node_modules/@m0saic/product/dist/licenseFile.js +1 -1
  191. package/node_modules/@m0saic/product/dist/licenseToken.js +1 -1
  192. package/node_modules/@m0saic/product/dist/makeScratchPath.js +1 -1
  193. package/node_modules/@m0saic/product/dist/postRenderWrapper.js +1 -1
  194. package/node_modules/@m0saic/product/dist/publicKeys.js +1 -1
  195. package/node_modules/@m0saic/product/dist/revokedKeys.js +1 -1
  196. package/node_modules/@m0saic/product/dist/templateBundle.js +1 -0
  197. package/node_modules/@m0saic/product/dist/upstreamDefaults.js +1 -1
  198. package/node_modules/@m0saic/product/dist/workspaceScaffold.js +1 -1
  199. package/node_modules/@m0saic/product/package.json +1 -1
  200. package/node_modules/@m0saic/template-utils/dist/media/defaultMedia.browser.js +1 -0
  201. package/node_modules/@m0saic/template-utils/dist/media/starterSvgInline.js +1 -0
  202. package/node_modules/@m0saic/template-utils/package.json +1 -1
  203. package/node_modules/@m0saic/templates/dist/m0saic/alpine/bar-graph/v3/bar-graph.js +1 -0
  204. package/node_modules/@m0saic/templates/dist/m0saic/alpine/bar-graph/v3/index.js +1 -0
  205. package/node_modules/@m0saic/templates/dist/m0saic/alpine/index.js +1 -1
  206. package/node_modules/@m0saic/templates/dist/m0saic/alpine/line-chart/v4/index.js +1 -0
  207. package/node_modules/@m0saic/templates/dist/m0saic/alpine/line-chart/v4/line-chart.js +1 -0
  208. package/node_modules/@m0saic/templates/dist/m0saic/brand/business-card/v3/business-card.js +1 -0
  209. package/node_modules/@m0saic/templates/dist/m0saic/brand/business-card/v3/index.js +1 -0
  210. package/node_modules/@m0saic/templates/dist/m0saic/brand/business-card/v3/layout.js +1 -0
  211. package/node_modules/@m0saic/templates/dist/m0saic/brand/index.js +1 -1
  212. package/node_modules/@m0saic/templates/dist/template-catalog.json +486 -6
  213. package/node_modules/@m0saic/templates/dist/template-registry.js +1 -1
  214. package/node_modules/@m0saic/templates/dist/web.js +1 -1
  215. package/node_modules/@m0saic/templates/package.json +1 -1
  216. package/node_modules/@m0saic/templates/template-catalog.json +486 -6
  217. package/node_modules/@m0saic/templates/template-manifest.json +69 -0
  218. package/node_modules/@m0saic/templates/web-template-ids.json +3 -0
  219. package/node_modules/@m0saic/templates-advanced/package.json +1 -1
  220. package/node_modules/@m0saic/text/package.json +1 -1
  221. package/node_modules/@m0saic/types/package.json +1 -1
  222. package/node_modules/@m0saic/types-internal/dist/analytics/featureKeys.js +1 -1
  223. package/node_modules/@m0saic/types-internal/package.json +1 -1
  224. package/package.json +13 -13
  225. package/resources/starter-base/starter-base.json +4 -4
  226. package/resources/starter-base/starter-base.tgz +0 -0
@@ -1,2 +1,2 @@
1
- "use strict";var f=Object.defineProperty;var o=(e,r)=>f(e,"name",{value:r,configurable:!0});Object.defineProperty(exports,"__esModule",{value:!0}),exports.serializeM0vFile=serializeM0vFile,exports.parseM0vFile=parseM0vFile;const serializeM0File_1=require("../m0/serializeM0File");function serializeM0vFile(e){const r=e.created??new Date;if(e.outputs!==void 0&&(e.outputs===null||typeof e.outputs!="object"||Array.isArray(e.outputs)))throw new Error("serializeM0vFile: outputs must be an object map when provided");if(e.assets!==void 0&&(e.assets===null||typeof e.assets!="object"||Array.isArray(e.assets)))throw new Error("serializeM0vFile: assets must be an object map when provided");const t=e.app??null,n=e.appVersion??null,s=normalizeMeta(e.meta??null),i={format:"m0v",version:1,created:(0,serializeM0File_1.formatISO)(r),app:t&&t.trim()!==""?t.trim():null,appVersion:n&&n.trim()!==""?n.trim():null,meta:s,custom:e.custom??null};return e.outputs&&Object.keys(e.outputs).length>0&&(i.outputs=e.outputs),e.assets&&Object.keys(e.assets).length>0&&(i.assets=e.assets),JSON.stringify(i,null,2)+`
2
- `}o(serializeM0vFile,"serializeM0vFile");function normalizeMeta(e){if(!e)return null;const r={},t=["title","author","source","note"];for(const n of t){const s=e[n];if(s!=null){const i=s.trim();i!==""&&(r[n]=i)}}return Object.keys(r).length?r:null}o(normalizeMeta,"normalizeMeta");function parseM0vFile(e){if(typeof e!="string")throw new Error("parseM0vFile: input must be a string");let r;try{r=JSON.parse(e)}catch{throw new Error("parseM0vFile: invalid JSON")}if(!isPlainObject(r))throw new Error("parseM0vFile: expected JSON object");const t=r;if(t.format!=="m0v")throw new Error(`parseM0vFile: expected format "m0v", got "${String(t.format)}"`);if(t.version!==1)throw new Error(`parseM0vFile: expected version 1, got ${String(t.version)}`);const n=typeof t.created=="string"?t.created.trim():"";if(!n)throw new Error("parseM0vFile: missing created timestamp");const s=parseOptionalMap(t.outputs,"outputs"),i=parseOptionalMap(t.assets,"assets"),u=parseStringOrNull(t.app),l=parseStringOrNull(t.appVersion),c=parseMetaOrNull(t.meta),a={format:"m0v",version:1,created:n,app:u,appVersion:l,meta:c,custom:t.custom===void 0?null:t.custom};return s!==void 0&&(a.outputs=s),i!==void 0&&(a.assets=i),a}o(parseM0vFile,"parseM0vFile");function parseOptionalMap(e,r){if(e!==void 0){if(e===null)throw new Error(`parseM0vFile: invalid ${r} (expected object map or omit, got null)`);if(!isPlainObject(e))throw new Error(`parseM0vFile: invalid ${r} (expected object map, got `+(Array.isArray(e)?"array":typeof e)+")");return e}}o(parseOptionalMap,"parseOptionalMap");function parseStringOrNull(e){if(e==null||typeof e!="string")return null;const r=e.trim();return r||null}o(parseStringOrNull,"parseStringOrNull");function parseMetaOrNull(e){if(e==null)return null;if(!isPlainObject(e))throw new Error("parseM0vFile: invalid meta (expected object or null)");const r=e,t={};for(const n of["title","author","source","note"]){const s=r[n];if(typeof s=="string"){const i=s.trim();i&&(t[n]=i)}}return Object.keys(t).length?t:null}o(parseMetaOrNull,"parseMetaOrNull");function isPlainObject(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}o(isPlainObject,"isPlainObject");
1
+ "use strict";var d=Object.defineProperty;var a=(e,t)=>d(e,"name",{value:t,configurable:!0});Object.defineProperty(exports,"__esModule",{value:!0}),exports.serializeM0vFile=serializeM0vFile,exports.parseM0vFile=parseM0vFile;const serializeM0File_1=require("../m0/serializeM0File");function serializeM0vFile(e){const t=e.created??new Date;if(e.outputs!==void 0&&(e.outputs===null||typeof e.outputs!="object"||Array.isArray(e.outputs)))throw new Error("serializeM0vFile: outputs must be an object map when provided");if(e.assets!==void 0&&(e.assets===null||typeof e.assets!="object"||Array.isArray(e.assets)))throw new Error("serializeM0vFile: assets must be an object map when provided");if(e.brand!==void 0&&(e.brand===null||typeof e.brand!="object"||Array.isArray(e.brand)))throw new Error("serializeM0vFile: brand must be an object map when provided");const r=e.app??null,i=e.appVersion??null,s=normalizeMeta(e.meta??null),n={format:"m0v",version:1,created:(0,serializeM0File_1.formatISO)(t),app:r&&r.trim()!==""?r.trim():null,appVersion:i&&i.trim()!==""?i.trim():null,meta:s,custom:e.custom??null};return e.outputs&&Object.keys(e.outputs).length>0&&(n.outputs=e.outputs),e.assets&&Object.keys(e.assets).length>0&&(n.assets=e.assets),e.brand&&Object.keys(e.brand).length>0&&(n.brand=e.brand),JSON.stringify(n,null,2)+`
2
+ `}a(serializeM0vFile,"serializeM0vFile");function normalizeMeta(e){if(!e)return null;const t={},r=["title","author","source","note"];for(const i of r){const s=e[i];if(s!=null){const n=s.trim();n!==""&&(t[i]=n)}}return Object.keys(t).length?t:null}a(normalizeMeta,"normalizeMeta");function parseM0vFile(e){if(typeof e!="string")throw new Error("parseM0vFile: input must be a string");let t;try{t=JSON.parse(e)}catch{throw new Error("parseM0vFile: invalid JSON")}if(!isPlainObject(t))throw new Error("parseM0vFile: expected JSON object");const r=t;if(r.format!=="m0v")throw new Error(`parseM0vFile: expected format "m0v", got "${String(r.format)}"`);if(r.version!==1)throw new Error(`parseM0vFile: expected version 1, got ${String(r.version)}`);const i=typeof r.created=="string"?r.created.trim():"";if(!i)throw new Error("parseM0vFile: missing created timestamp");const s=parseOptionalMap(r.outputs,"outputs"),n=parseOptionalMap(r.assets,"assets"),u=parseOptionalMap(r.brand,"brand"),l=parseStringOrNull(r.app),c=parseStringOrNull(r.appVersion),f=parseMetaOrNull(r.meta),o={format:"m0v",version:1,created:i,app:l,appVersion:c,meta:f,custom:r.custom===void 0?null:r.custom};return s!==void 0&&(o.outputs=s),n!==void 0&&(o.assets=n),u!==void 0&&(o.brand=u),o}a(parseM0vFile,"parseM0vFile");function parseOptionalMap(e,t){if(e!==void 0){if(e===null)throw new Error(`parseM0vFile: invalid ${t} (expected object map or omit, got null)`);if(!isPlainObject(e))throw new Error(`parseM0vFile: invalid ${t} (expected object map, got `+(Array.isArray(e)?"array":typeof e)+")");return e}}a(parseOptionalMap,"parseOptionalMap");function parseStringOrNull(e){if(e==null||typeof e!="string")return null;const t=e.trim();return t||null}a(parseStringOrNull,"parseStringOrNull");function parseMetaOrNull(e){if(e==null)return null;if(!isPlainObject(e))throw new Error("parseM0vFile: invalid meta (expected object or null)");const t=e,r={};for(const i of["title","author","source","note"]){const s=t[i];if(typeof s=="string"){const n=s.trim();n&&(r[i]=n)}}return Object.keys(r).length?r:null}a(parseMetaOrNull,"parseMetaOrNull");function isPlainObject(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}a(isPlainObject,"isPlainObject");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m0saic/dsl-file-formats",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "main": "./dist/cjs/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -45,6 +45,7 @@ disposes).
45
45
  | Overlays, passthroughs, identity | `skills/overlay-semantics.md`, `skills/passthrough-semantics.md`, `skills/identity.md` |
46
46
  | Build a template | `templates/philosophy-and-contract.md` → `templates/construction-strategy.md` → `templates/geometry-recipes.md` |
47
47
  | Template perf | `templates/patterns/perf-authoring-rules.md` |
48
+ | A template that goes to paper (cards, banners, posters) | `templates/patterns/print-authoring.md` |
48
49
  | Data-driven templates | `templates/data-pipeline.md` |
49
50
  | Read/write `.m0` / `.m0c` / `.m0p` / `.m0v` | `file-formats/m0p-and-custom-field.md` |
50
51
  | Render via the CLI | `runtime/cli-usage.md` |
@@ -23,7 +23,7 @@ These features shipped together in April 2026. They are foundational to v2 work
23
23
  | `.m0c` | JSON: DSL + StableKey-keyed labels + derive image + meta | One layout, with labels / overlays / derive thumbnail / `custom.*` |
24
24
  | `.m0g` | `.m0c`-shaped JSON with `format: "m0g"` + the `M0cGold` `gold` block | A **m0saic golden** ("mogging") — a tried-and-true, signed-off layout; `#mogging` comment-line convention (`types.ts`) |
25
25
  | `.m0p` | JSON: pack of `.m0c`-shaped variants under shared identity | Multiple coordinated variants (cross-canvas, brand pack, content set, themed collection) |
26
- | `.m0v` | JSON: `outputs` / `assets` / `custom` (`src/m0v/m0vFile.ts`) | **Mosaic Vocabulary** — user-tier shared brand language: output presets, palette, typography, asset paths. CLI `--m0v <path>`; `m0saic open` accepts it |
26
+ | `.m0v` | JSON: `outputs` / `assets` / `brand` / `custom` (`src/m0v/m0vFile.ts`) | **Mosaic Vocabulary** — user-tier shared brand language: output presets, asset paths, and since 2026-10-19 the `brand` slot (the shop's name, logo, accent, site, … as strings — Mosaic Desktop's "Your brand" profile, `~/m0saic/solutions/<id>/brand.m0v`; see `templates/reference/brand-schema.md`). Every slot optional; palette, typography still ride in `custom`. CLI `--m0v <path>` reads `outputs`; `m0saic open` accepts it |
27
27
 
28
28
  `.m0` and `.m0c` are unchanged by the later formats' arrival — each is purely additive.
29
29
  This doc covers `.m0`/`.m0c`/`.m0p` in depth; `.m0g` and `.m0v` have their own type
@@ -44,7 +44,11 @@ m0saic layout.m0 # .m0 / .m0c paths also accepted (index.ts:~7184-7270
44
44
  ## Command table
45
45
 
46
46
  18 `.command(` registrations in `index.ts` + `telemetry` registered by
47
- `registerTelemetryCommand(program)` (index.ts:~7138 → `utils/telemetry.ts:~372`).
47
+ `registerTelemetryCommand(program)` (index.ts:~7138 → `utils/telemetry.ts:~372`) +
48
+ `community` (`fetch`), `install`, `uninstall` registered by
49
+ `registerTemplateInstallCommands(program, …)` (`utils/templateInstall.ts`, since
50
+ 0.4.0; also listed in `inlineDsl.ts` `KNOWN_SUBCOMMANDS` so they are never read as
51
+ an inline m0 string).
48
52
  Two of the 18 sit inside `if (DEV_MODE) { … }` and are absent from the public
49
53
  help — see the DEV-GATED rows below.
50
54
 
@@ -58,12 +62,15 @@ help — see the DEV-GATED rows below.
58
62
  | Command (index.ts line) | What it does |
59
63
  |---|---|
60
64
  | `make <input>` (~5095) | Primary render: template id / `.mosaic` / `.mosaicx` → video or image. Full flag surface below. |
61
- | `hello-world` | The first render — a thin alias for `make @m0saic/hello-world/v1` (the brand card; output defaults to `hello-world.mp4`). With `--template-repo <path...>` / `--community-repo <path>` it renders the loaded repo's **front door** instead: the first repo (load order) whose `repo.helloWorld` names a template that registered, announced as `Front door: <id> — <repo>'s hello-world`; no repos, or none that name one → the core card with a one-line why. Takes `-o -w -h --fps --durationMs --props --quiet --verbose` (no engine surface). Picker: the CLI source (not published). |
65
+ | `hello-world` | The first render — a thin alias for `make @m0saic/hello-world/v1` (the brand card; output defaults to `hello-world.mp4`). With `--template-repo <path...>` it renders the loaded folder's **front door** instead: the first flagged repo (load order) whose `repo.helloWorld` names a template that registered (installed repos never are the front door; the community one is `m0saic make @m0saic-community/hello-world/v1` after `m0saic community fetch`), announced as `Front door: <id> — <repo>'s hello-world`; no repos, or none that name one → the core card with a one-line why. Takes `-o -w -h --fps --durationMs --props --quiet --verbose` (no engine surface). Picker: the CLI source (not published). |
62
66
  | `make-wireframe` (~5462) | Static wireframe PNG from `--m0 <string>` or `--mfile <path>`. |
63
67
  | `make-wireframe-animated` (~5568) | Animated wireframe MP4 from `--m0`/`--mfile`; extra `--disable-ui`. |
64
68
  | `flatten <input>` (~5680) | Inline all `type:mosaic` children of a `.mosaic`/template into one flat JSON doc — no render. |
65
69
  | `resolve <input>` (~5800) | `.mosaicx` recipe → resolved `.mosaic` provenance doc, no ffmpeg; `--flatten` also inlines children. |
66
- | `list-templates` (~5946) | Print registered template ids (`--json`); annotates primitive/internal/deprecated. |
70
+ | `list-templates` (~6103) | Print registered template ids, grouped by source (since 0.4.0): `Built-in: N templates`, then `Installed <repo id> (<name>): <tag>, signed|unsigned, N templates` (community first), then `Folder <path>, <repo id> (<name>): N templates` for `--template-repo`. A heading is one line that never starts with `@`; ids stay one per line, flush left. `--source <name>` lists one group: `builtin`, `community`, or a repo id with or without `@` (any case); an unknown name exits 1 and lists the sources. `--json` = a flat sorted id array, filtered by `--source`. Annotates primitive/internal/deprecated. Grouping: the CLI source (not published). |
71
+ | `community fetch` | **Since 0.4.0.** Install or update the official community templates from their latest signed GitHub release bundle, or report them current (`--force` reinstalls, `--quiet`). A valid signature is required. See "Installed template repos" below. Impl: the CLI source (not published). |
72
+ | `install [source]` | **Since 0.4.0.** Install a template repo from its release bundle: `owner/repo[@tag]`, a GitHub repo or release page, a `bundle.json` / `.tgz` URL, or a bundle on disk. No source: list what is installed (`--json`). A third-party repo: once `bundle.json` is read, the third-party code notice, then a question (repo, tag, templates, size; default No) before the runtime downloads; `--yes` skips it and is required with no terminal (CI, pipes: exit 1, nothing installed). A repo signed by m0saic (second-party) gets one line instead, no question. Given the official community repo in any GitHub form, it runs `community fetch` instead. A private repo needs its access key: `--key-env <name>`, then `M0SAIC_REPO_KEY`, then the key saved for it; in a terminal it asks once and saves it. `--force`, `--quiet`. |
73
+ | `uninstall <repoId>` | **Since 0.4.0.** Remove an installed template repo (files + entry) and its saved access key, if any; exit 1 with the installed ids when it is not installed. |
67
74
  | `doctor <repoDir>` | **Public since 0.2.0** (dev-gated at 0.1.0; founder ruling 2026-09-16, BURN-DOWN D5 → publish). Run the template conventions over a template repo folder — the same checks its build gate (`tools/check-registry.mjs`) runs, from outside (agents, reviewers, forks, CI on a fork): definition-time + render-time conventions (the 13 throw-posture ones incl. `latticeSmooth`) and, when the repo commits `layout-fingerprints/` or `.layout.m0` sidecars, the fingerprint diff. `--json` (one object: `ok, repo, mode, loadDiagnostics, rendered, skipped, errors, warnings, notes, fingerprints, conventions`), `--sweep` (also render on the standard 1080p canvases → `canvasEnvelope`), `--entry <path>`, `--src <dir>`. Loads through `loadTemplateRepoFromPath` under the external origin scope, so conventions RECORD rather than throw and one bad template never hides the rest. **Runs the repo's template module bodies in-process** — prints a trust warning to stderr (not under `--json`). Exit 1 on any error-severity finding or a fatal load diagnostic. It is the pack-publishing contract for community/starter authors ("the publish requirement for packs"). Impl: the CLI source (not published); locked public by `__tests__/cli.surface.test.js`. |
68
75
  | `browse-templates` (~6033) | **DEV-GATED** (whole command inside `if (DEV_MODE)`, 4792): interactive TTY picker (template → variant → confirm → delegates to `make`). It drives the E2E variant matrix and writes to `test-output/` — a development harness, not a user surface. |
69
76
  | `momo <message…>` (~6282) | **DEV-GATED** (whole command inside `if (DEV_MODE)`, 5040): relay English geometry instruction to Momo in a running Mosaic Desktop over the loopback bridge; exit 0 applied / 2 not-applied / 1 couldn't-run. |
@@ -177,8 +184,9 @@ mosaic; prints `Validation DEGRADED`), `--report` (`.output.json` /
177
184
  `--keep-temp`, `--quiet`, `--verbose`, `--prefer-pretty-m0` /
178
185
  `--prefer-canonical-m0` (printed-m0 form only; note: NOT `--prefer-*-m0saic`),
179
186
  `--background-color <color>`, `--template-repo <path...>` +
180
- `--template-repo-entry <file>` (external template repos), `--community-repo <path>`
181
- (ONE checkout of the official community repo — see the community gate below).
187
+ `--template-repo-entry <file>` (a template repo folder for one run). Installed
188
+ repos load with no flag; `--community-repo` was removed in 0.4.0 — see
189
+ "Installed template repos" and the community gate below.
182
190
 
183
191
  **Encode surface** (index.ts:~4231-4338, mirrors the Make page's Advanced panel;
184
192
  precedence: flags > `.m0v` preset > doc-on-disk > engine default):
@@ -235,34 +243,140 @@ and neither is `doctor` (public since 0.2.0).
235
243
  > `momo` / `browse-templates` finds hits; that is the router table, not the
236
244
  > gated implementation.
237
245
 
238
- ## External template repos and the community gate (2026-09-16)
239
-
240
- **Three reserved identities, exact-match, nothing else is checked:** template
241
- namespaces `@m0saic/` and `@m0saic-dev/`, repo id `@m0saic-community`
242
- (`index.ts:~907-909`; registry compare is case-insensitive, `templateRegistry.ts:~112`).
243
- Every other namespace / repo id is open. `--template-repo` never grants anything.
244
-
245
- **The grant.** `--community-repo <path>` says "this IS the official repo"; the
246
- `@m0saic-dev/` grant comes only from `verifyCommunityRelease()` (`@m0saic/product`
247
- `communityRelease.ts`) passing on the checkout's `release.json` against
248
- `LICENSE_PUBLIC_KEYS` (`publicKeys.ts` — the licence k1 family; rotation = append
249
- `k2`, ship, sign with `M0SAIC_SIGNING_KID=k2`; older CLIs report `unknown-kid`).
250
- The gate runs BEFORE the entry module is imported. All gate/notice lines go to
251
- stderr (survive `--quiet`, keep `--json` stdout clean):
252
-
253
- - ok → `✓ community repo verified: <tag> (signed, <kid>)`
254
- - else → `⚠ <path> is not a verified release of the official community repo
255
- (<reason>). Loaded as an ordinary template repo — its @m0saic-dev/ ids are
256
- refused. Get the official checkout: the link on m0saic.io/community` → the
257
- reserved ids are refused in ONE collapsed line, `⚠ [template-repo]
258
- TEMPLATE_ID_RESERVED_NAMESPACE: refused <N> ids in reserved namespace
259
- (@m0saic-dev/…): <id1>, <id2>, <id3>, …` (other rejection codes stay one line
260
- each) → the existing `❌ Refusing to use template repo` block → **exit 1**.
246
+ ## External template repos and the community gate (2026-09-16; installs 2026-10-13)
247
+
248
+ **Three reserved namespaces and one reserved repo id, exact-match, nothing else
249
+ is checked:** template namespaces `@m0saic/`, `@m0saic-dev/`, `@m0saic-community/`
250
+ and repo id `@m0saic-community` (`RESERVED_TEMPLATE_NAMESPACES` /
251
+ `RESERVED_COMMUNITY_REPO_ID` in `index.ts`; registry compare is case-insensitive,
252
+ `templateRegistry.ts`). Every other namespace / repo id is open. A `--template-repo`
253
+ folder never gets a grant in a shipped build.
254
+
255
+ **Installed template repos (CLI 0.4.0).** `m0saic community fetch` and
256
+ `m0saic install <source>` put release bundles (`bundle.json` + `runtime.tgz`,
257
+ written by https://github.com/m0saic-project/m0saic-community-templates/blob/main/tools/pack-bundle.mjs — the starter's
258
+ `npm run bundle` — and attached to a GitHub release; previews and samples are
259
+ never downloaded) into `~/m0saic/template-repos/` (`M0SAIC_ROOT` moves it):
260
+ `installed.json` + one unpacked runtime per repo at `<slug>/<tag or sha-…>/`.
261
+ The store, download and checks are `@m0saic/product` `templateBundle.ts` (index
262
+ parse, source resolution, download with a 30 s idle timeout + HTTP Range resume +
263
+ size/sha256 checks, an extractor that writes only regular files and folders —
264
+ links, devices, `..`, absolute paths, backslashes, `node_modules`,
265
+ Windows-illegal names and duplicates are refused) and `installedTemplateRepos.ts`
266
+ (lock, staging + swap, the signature and reserved-namespace checks). `community
267
+ fetch` reads `github.com/m0saic-project/m0saic-community-templates/releases/latest/
268
+ download/bundle.json` (`M0SAIC_COMMUNITY_GITHUB_REPO` overrides; the bundle must
269
+ still verify) and installs, updates, or reports current; a signed install that no
270
+ longer verifies is reinstalled. Install refusals: `not-signed` (any
271
+ `@m0saic-community` bundle that does not verify — dev builds also take an
272
+ unsigned one), `reserved-namespace` (manifest ids in a reserved namespace,
273
+ unless the verified community repo), `check-failed` (the CLI loads the staged
274
+ copy once, `checkStagedTemplateRepo`, before it replaces anything),
275
+ `wrong-repo`, size / hash / tree mismatches, `busy` (another install holds
276
+ `.lock`), `declined` (the question below answered no, cancelled, or no terminal
277
+ and no `--yes`).
278
+
279
+ **Asking first (2026-10-13).** `install` reads `bundle.json`, then decides
280
+ (product `installTemplateBundle({ confirm })`: the hook runs after the index and
281
+ the "current" check, before the runtime download, and the store lock is taken
282
+ after it, so an open question never holds `.lock`; false throws `declined`). A
283
+ copy that is already current is reported with no question. A third-party repo
284
+ (a bare `.tgz` too) gets the notice, then `Install @acme (Acme Templates)
285
+ v2026.10.1 (12 templates, 640 KB)?` (`Update @acme v2026.10.0 → v2026.10.1
286
+ (…)?`, `Reinstall …?` for `--force` over the same tag, `Install the template
287
+ repo at <source>?` with no `bundle.json`), default No: no prints `Nothing
288
+ installed.`, exit 1. With no terminal (stdin or stdout not a TTY, or `CI` set)
289
+ and no `--yes`: `✗ Not installed: m0saic asks before it installs a template
290
+ repo, and there is no terminal to ask on.` + `Approve it explicitly: m0saic
291
+ install <source> --yes`, exit 1, the runtime never downloaded (the `setup`
292
+ rule). `--yes` never asks. The notice prints once per install, never at load.
293
+
294
+ **One way in for the community templates.** `community fetch` never asks.
295
+ `install` given the community repo's GitHub releases in any form
296
+ (`owner/repo[@tag]`, the repo or a release page, a release file's URL; product
297
+ `isCommunityBundleSource`) prints `The community templates install with m0saic
298
+ community fetch; running that.` and runs it: the same pins (signed only, repo id
299
+ `@m0saic-community` only), recorded as `community`, a tag kept
300
+ (`fetchCommunityTemplates({ source })`). A `@m0saic-community` bundle from
301
+ anywhere else (one packed on disk, tested before it is published) installs
302
+ through `install` with no notice and no question, printing
303
+ `@m0saic-community is m0saic's official community repo: it installs only if
304
+ m0saic's signature verifies.`: safe on an unverified claim, since a bundle naming
305
+ that repo id installs only if it verifies and an index that disagrees with the
306
+ manifest inside is refused. A community not-found says `No community templates
307
+ release there.` (a third-party one points at m0saic.io/developers/bundles).
308
+
309
+ **Three tiers (2026-10-13).** First-party: the built-in templates and the community
310
+ repo (signed; the only holder of the reserved namespaces). Second-party: a repo
311
+ signed by m0saic in its own namespace (one m0saic built for a customer, signed with
312
+ `scripts/sign-template-repo.mjs` like the starters; the starters land here too).
313
+ Third-party: unsigned. `install` acts on `bundle.json`'s `signed` before the
314
+ download: a second-party bundle prints `<repo id> (<name>) is signed by m0saic: it
315
+ installs only if that signature verifies.`, with no third-party notice, no question
316
+ and no `--yes` needed. The product refuses any bundle whose `bundle.json` says signed
317
+ and whose signature does not verify (`not-signed`: `bundle.json says it is signed by
318
+ m0saic, but the signature does not verify (<reason>)`), before the CLI's load check
319
+ runs any code; it is never installed unsigned. A signature is not a namespace grant:
320
+ a signed repo other than the community repo using reserved ids is still
321
+ `reserved-namespace`.
322
+
323
+ **Private repos (an access key, 2026-10-13).** A private GitHub repo that publishes
324
+ bundles installs like any other, plus an access key: today a fine-grained GitHub
325
+ token its owner hands out (that repo, Contents read-only). The key comes from
326
+ `--key-env <NAME>` (the variable must be set; a source that is not a GitHub release
327
+ exits 1), then `M0SAIC_REPO_KEY`, then the key saved for that owner/repo in
328
+ `~/m0saic/template-repo-keys.json` (mode 0600, by lowercase owner/repo). There is no
329
+ `--key <value>` flag (shell history, process lists). In a terminal, a GitHub source
330
+ that answers not-found with no key, or refuses the key (`auth-failed`, `no-access`),
331
+ prints why and asks `Access key for owner/repo (Enter to stop)` (masked), retries,
332
+ and saves a key that worked (`Saved the access key for owner/repo (<file>); updates
333
+ use it.`); keys from the environment are never saved. With no terminal, a GitHub
334
+ not-found adds `A private repo needs its access key: run this in a terminal to enter
335
+ it, or set M0SAIC_REPO_KEY.` Transport: `GET api.github.com/repos/<o>/<r>/releases/
336
+ latest` (or `/tags/<tag>`) lists the assets, `bundle.json` is the index, and each
337
+ file is `GET …/releases/assets/<id>` with `Accept: application/octet-stream` and
338
+ `redirect: manual`; GitHub's redirect is followed without the key, which is only ever
339
+ sent to `https://api.github.com` (`downloadToFile` refuses any other host). Every
340
+ GitHub release form carries `githubRelease` (owner/repo[@tag], the repo and release
341
+ pages, release file URLs; product `resolveKeyedGitHubSource`). Errors: `auth-failed`
342
+ (401), `no-access` (404/403 with a key; a used-up rate limit is `http-error`).
343
+ Records gain `github` ("owner/repo", lowercase, on any GitHub install) and
344
+ `access: "key"`, never the key; `m0saic install` lists keyed repos `private`;
345
+ `uninstall` deletes the saved key (`✓ removed … and its saved access key`). Access
346
+ ending changes nothing installed (it keeps working offline); updates stop.
347
+
348
+ **Loading.** `make`, `hello-world`, `list-templates` and `mcp` load the installed
349
+ repos with no flag, once per process (the MCP server sees a new install after a
350
+ restart), after any `--template-repo` folders — a folder with the same repo id
351
+ wins (the developer's clone over its installed release) — and quietly (no notice,
352
+ no "Loaded" line). An installed repo that fails to load is skipped with one
353
+ warning instead of stopping the command. Installed repos never become
354
+ `hello-world`'s front door. A not-registered `@m0saic-dev/…` /
355
+ `@m0saic-community/…` id adds the hint `m0saic community fetch` when the
356
+ community templates are not installed.
357
+
358
+ **The grant.** The reserved namespaces go only to the installed
359
+ `@m0saic-community` whose `release.json` passes `verifyCommunityRelease()`
360
+ (`@m0saic/product` `communityRelease.ts`) against `LICENSE_PUBLIC_KEYS`
361
+ (`publicKeys.ts` — the licence k1 family; rotation = append `k2`, ship, sign with
362
+ `M0SAIC_SIGNING_KID=k2`; older CLIs report `unknown-kid`). It is checked at install
363
+ and again on EVERY load (`gateCommunityRepo`, before the entry module is
364
+ imported): pass → silent; fail → the copy is skipped, never imported (not even as
365
+ an ordinary repo), with one stderr line: `⚠ [template-repo] the installed
366
+ community templates (<tag>) no longer verify (<why>); skipped. Reinstall: m0saic
367
+ community fetch --force`. A `--template-repo` folder whose repo id is
368
+ `@m0saic-community` loads under ordinary rules in a shipped build: its reserved
369
+ ids are refused in ONE collapsed line, `⚠ [template-repo]
370
+ TEMPLATE_ID_RESERVED_NAMESPACE: refused <N> ids in reserved namespaces
371
+ (@m0saic-community/…, @m0saic-dev/…): <id1>, <id2>, <id3>, …` (other rejection
372
+ codes stay one line each) → `❌ Refusing to use template repo` with the hint
373
+ `install its signed release instead: m0saic community fetch` → **exit 1**.
261
374
 
262
375
  **`release.json` v1:** `{ schemaVersion: 1, tag, publishedAt, treeSha256,
263
376
  signature: { kid, alg: "ed25519", sig: <base64url> } }` (`--unsigned` omits
264
377
  `signature`; the legacy `{ tag, publishedAt }` reads as `missing-signature`).
265
- Tree = every file under `dist/**` + `template-manifest.json` + `package.json`,
378
+ Tree = every file under `dist/**` + `template-manifest.json` + `package.json` +
379
+ `template-catalog.json` (each root file only when present),
266
380
  sorted POSIX paths, one `"<path>\0<sha256>\n"` line each; `*.json` hashed on
267
381
  `JSON.stringify(JSON.parse(text))` (BOM stripped; CRLF / re-indent survive, a
268
382
  key reorder does not), other files raw bytes; no symlink is ever followed.
@@ -287,7 +401,8 @@ repo root from the HOST's own `node_modules` walk — the CLI's vendored copies,
287
401
  the desktop's bundle, the workspace under dev — never from the checkout. So a
288
402
  clone with nothing installed loads anywhere (0.2.0 is the first CLI that
289
403
  can), and a `node_modules/@m0saic/template-utils` planted beside a tree is
290
- never evaluated, via `--community-repo` or `--template-repo` alike. Every
404
+ never evaluated through a `--template-repo` folder (an installed copy with a
405
+ `node_modules` beside its tree is refused by the gate before any import). Every
291
406
  other specifier (`sharp`, `@twemoji/svg`, relative paths) resolves as before,
292
407
  from the checkout. CommonJS only — an `.mjs` entry's static `import`s use the
293
408
  ESM resolver, which has no hook.
@@ -297,12 +412,33 @@ with `M0SAIC_SIGNING_KEY_FILE` (REQUIRED — no default path; production laptop
297
412
  only) under `M0SAIC_SIGNING_KID` (default `k1`), self-verifies against the
298
413
  embedded keys, writes `.gitattributes` (`* -text`). Unset or missing key =
299
414
  refused, never a silent unsigned release; `--unsigned` = loud banner, a snapshot
300
- no shipped CLI trusts.
415
+ no shipped CLI trusts. After the tag: `node tools/pack-bundle.mjs --tag <tag>` in
416
+ the snapshot, then `gh release create <tag> -R m0saic-project/m0saic-community-templates
417
+ bundle/bundle.json bundle/runtime.tgz bundle/previews.tgz` — what `community fetch`
418
+ installs (the mint's next steps print both lines).
419
+
420
+ **Samples (2026-10-20).** A pack may ship sample pictures and clips for people
421
+ with no assets yet: `template-manifest.json` names the folder in
422
+ `repo.assets.samplesDir` (say `"samples"`), and `pack-bundle.mjs` packs every
423
+ file under it as a third archive, `samples.tgz` (`bundle.json.samples {file,
424
+ sha256, size, files, unpackedSize, dir} | null`; `--no-samples` skips it; the
425
+ folder must sit inside the repo, hold no links, and stay under 128 MB unpacked —
426
+ `samples-outside-repo`, `samples-missing`, `samples-too-large`). A manifest that
427
+ names no folder ships none. Mosaic Desktop installs it beside the previews (best
428
+ effort, `installed.json.samples = installed | none | failed`) and shows the
429
+ folder as the Files dock's first place, "Samples" (a plain `samples/` folder in
430
+ a clone or the dev stand-in counts too); the CLI never fetches it. Add
431
+ `bundle/samples.tgz` to the `gh release create` line when the packer prints it.
301
432
 
302
433
  **DEV seam (workspace builds only, inside `if (DEV_MODE)`):** reasons
303
- `missing-release` / `missing-signature` are still granted — `✓ community repo:
304
- <path> (dev build: unsigned community checkout trusted)` (the in-repo
305
- https://github.com/m0saic-project/m0saic-community-templates/blob/main has no `release.json`) — and
434
+ `missing-release` / `missing-signature` are still granted — for an installed copy
435
+ (and `m0saic install` takes an unsigned community bundle), and for a
436
+ `--template-repo` folder whose repo id is `@m0saic-community`: `✓ community
437
+ templates from <path> (dev build: --template-repo trusted when it verifies or is
438
+ unsigned)` (the in-repo https://github.com/m0saic-project/m0saic-community-templates/blob/main has no `release.json`;
439
+ `scripts/bake-front-door-preview.mjs` and the community render e2e load it this
440
+ way, against the real `~/m0saic` toolchain and license; a FIFO/socket/device tree
441
+ is refused there) — and
306
442
  `M0SAIC_COMMUNITY_TRUST_KEY_PEM` (+ `M0SAIC_COMMUNITY_TRUST_KEY_KID`, default `k1`)
307
443
  adds a public key for tests. Tampered tree, forged release or unknown kid are
308
444
  refused even in dev. The tarball folds `process.env.M0SAIC_DEV` to `"0"` and the
@@ -310,14 +446,15 @@ audit bans the token, so none of this ships.
310
446
 
311
447
  **Lookalikes are a display concern** — one stderr line for UNSIGNED repos, never a
312
448
  refusal: `⚠ [template-repo] <subject> is not an official m0saic namespace — only
313
- @m0saic/ and @m0saic-dev/ are, and they are signed.` Subject: loaded publisher
314
- namespaces starting with `@m0saic` that are not exactly the two reserved ones →
449
+ @m0saic/, @m0saic-dev/, @m0saic-community/ are, and they are signed.` Printed for a
450
+ `--template-repo` folder on every load, for an installed repo once (at install).
451
+ Subject: loaded publisher namespaces starting with `@m0saic` that are not exactly
452
+ the reserved ones →
315
453
  else a repoId matching `/m0saic|mosaic/i` → else `"<displayName>" (<repoId>)`.
316
454
  Never for repoId exactly `@m0saic-community`: via `--template-repo` that gets
317
455
  `⚠ [template-repo] repo id "@m0saic-community" is reserved for the official
318
- community repo and this checkout is unverified — loading it under ordinary
319
- rules.` A `--community-repo` checkout that fails the gate gets ONLY the
320
- not-verified notice.
456
+ community repo; this folder loads under ordinary rules (the signed release
457
+ installs with: m0saic community fetch).`
321
458
 
322
459
  **Desktop:** `installVersion()` runs `validateCommunityTree` → `verifyCommunityTree`
323
460
  (inline-requires `@m0saic/product`) → refuses `tag-mismatch`; failure =
@@ -327,20 +464,23 @@ not-verified notice.
327
464
  seed/installed load path (`loadCommunityRepo`) does not re-verify; consent gate
328
465
  unchanged.
329
466
 
330
- Help wording for `make` / `hello-world` / `list-templates` is ONE string,
331
- test-locked (`__tests__/cli.flags.test.js`): "…Grants the @m0saic-dev/ namespace
332
- when its release signature verifies (unsigned or modified checkouts load as
333
- ordinary repos); the community checkout loads only this way".
467
+ Help (test-locked, `__tests__/cli.surface.test.js`): the root epilog lists
468
+ `m0saic community fetch`, `m0saic install <source>` and `--template-repo`;
469
+ `make` / `hello-world` / `list-templates` carry `--template-repo` and point at
470
+ `m0saic community fetch`; `--community-repo` is an unknown option
471
+ (`cli.flags.test.js`). The gate and every on-disk tampering case are locked by
472
+ `__tests__/community-install.signing.e2e.test.js`, the install flow by
473
+ `__tests__/template-install.e2e.test.js`.
334
474
 
335
475
  ### Hardening (rounds 2 + 3, 2026-09-16)
336
476
 
337
- **Host-first `@m0saic/*` resolution.** https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/template-repos/hostFirstResolution.ts (wired in `loadTemplateRepoFromPath.ts` via `registerHostFirstRepoRoot(repoRoot)` before the native `import(file://…)` of the entry): ONE wrapper around Node's real `Module._resolveFilename` (found by walking the prototype chain — under Jest `node:module` exports a subclass copy), installed once per process, persistent so render-time lazy requires are covered. A request matching `/^@m0saic\/[^/\\]+(?:\/.*)?$/` whose requesting module lies under a registered root (realpath'd) is resolved from the HOST's own `node_modules` walk — never the checkout's. Everything else (relative paths, non-`@m0saic` deps, host code) is untouched. CJS only. Consequences: a community clone loads from ANY directory with no `node_modules` (before this, `--community-repo` only worked inside the monorepo); a planted `node_modules/@m0saic/*` beside a verified tree is never evaluated, via either flag; a starter's `file:` link to `@m0saic/template-utils` resolves to the same physical package — no drift.
477
+ **Host-first `@m0saic/*` resolution.** https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/template-repos/hostFirstResolution.ts (wired in `loadTemplateRepoFromPath.ts` via `registerHostFirstRepoRoot(repoRoot)` before the native `import(file://…)` of the entry): ONE wrapper around Node's real `Module._resolveFilename` (found by walking the prototype chain — under Jest `node:module` exports a subclass copy), installed once per process, persistent so render-time lazy requires are covered. A request matching `/^@m0saic\/[^/\\]+(?:\/.*)?$/` whose requesting module lies under a registered root (realpath'd) is resolved from the HOST's own `node_modules` walk — never the checkout's. Everything else (relative paths, non-`@m0saic` deps, host code) is untouched. CJS only. Consequences: a template repo loads from ANY directory with no `node_modules` (an installed bundle never has one); a planted `node_modules/@m0saic/*` beside a tree is never evaluated through a `--template-repo` folder (and an installed copy carrying one is refused by the gate before import); a starter's `file:` link to `@m0saic/template-utils` resolves to the same physical package — no drift.
338
478
 
339
- **Fail-closed tree verification** (`inspectReleaseTree` in `communityRelease.ts`; desktop mirror `inspectCommunityTree` in the Mosaic Desktop / Web app source (not published), run by `installVersion`). A release tree is plain files and directories, nothing else. Refused, in this order after the signature check and before the hash compare: `symlink-in-tree` (ANY symlink — file or dir — under `dist/**`, `dist` itself, a signed root file, or ANY entry at the checkout root); `special-file-in-tree` (FIFO / socket / device anywhere in the same places — a FIFO named `dist/publishers.js` shadows the signed `dist/publishers/` dir and blocks or executes whoever opens it); `foreign-modules` (a `node_modules` dir at the root or under `dist/`, matched case-insensitively). `verifyCommunityRelease` lstats `release.json` before opening it (a FIFO planted there used to hang the verifier). The hasher THROWS `ReleaseTreeError` on a special entry rather than skipping it; `signCommunityRelease` refuses to sign such a tree. The signed payload is `"m0saic-community-release/v1\n<tag>\n<publishedAt>\n<treeSha256>\n"`. The CLI notice carries the detail for these reasons — `(foreign-modules: node_modules)`; for `special-file-in-tree` the CLI does NOT load the folder at all (an ordinary load provably hangs): notice + `❌ Refusing to use template repo` + exit 1.
479
+ **Fail-closed tree verification** (`inspectReleaseTree` in `communityRelease.ts`; desktop mirror `inspectCommunityTree` in the Mosaic Desktop / Web app source (not published), run by `installVersion`). A release tree is plain files and directories, nothing else. Refused, in this order after the signature check and before the hash compare: `symlink-in-tree` (ANY symlink — file or dir — under `dist/**`, `dist` itself, a signed root file, or ANY entry at the checkout root); `special-file-in-tree` (FIFO / socket / device anywhere in the same places — a FIFO named `dist/publishers.js` shadows the signed `dist/publishers/` dir and blocks or executes whoever opens it); `foreign-modules` (a `node_modules` dir at the root or under `dist/`, matched case-insensitively). `verifyCommunityRelease` lstats `release.json` before opening it (a FIFO planted there used to hang the verifier). The hasher THROWS `ReleaseTreeError` on a special entry rather than skipping it; `signCommunityRelease` refuses to sign such a tree. The signed payload is `"m0saic-community-release/v1\n<tag>\n<publishedAt>\n<treeSha256>\n"`. The CLI's skip line carries the detail for these reasons — `(foreign-modules: node_modules)`; an installed copy that fails is never imported, so a `special-file-in-tree` (an ordinary load provably hangs) cannot block the process; a dev `--template-repo` community folder with one is refused (`❌ Refusing to use template repo` + exit 1).
340
480
 
341
481
  **Community deps.** The `@m0saic-dev/community-m` pack (the only `sharp` / `@twemoji/svg` user) was deleted as defunct on 2026-09-16; both left the community `package.json` and BOTH allowlists (`dep-allowlist.json` ↔ `TEMPLATE_REPO_DEP_ALLOWLIST`, lockstep-tested). Community templates may import only what the hosts ship: `@m0saic/{types,template-utils,dsl-stdlib,platform,dsl}` + `node:path`. A native or third-party runtime dep is a founder decision. `sharp` survives only as a root devDependency for core/templates tests.
342
482
 
343
- **Accepted residual.** A third-party `--template-repo` checkout with its own `node_modules` runs its own copies of non-`@m0saic` deps — third-party code, with the notice printed; it cannot obtain a reserved identity that way (adversary-verified). DEV builds (`M0SAIC_DEV=1`, compile-stripped from the tarball) still trust an unsigned checkout for `missing-release` / `missing-signature` only; a tampered or forged tree is refused even in dev.
483
+ **Accepted residual.** A third-party `--template-repo` checkout with its own `node_modules` runs its own copies of non-`@m0saic` deps — third-party code, with the notice printed; it cannot obtain a reserved identity that way (adversary-verified). DEV builds (`M0SAIC_DEV=1`, compile-stripped from the tarball) still trust an unsigned community copy (installed, or a `--template-repo` folder) for `missing-release` / `missing-signature` only; a tampered or forged tree is refused even in dev.
344
484
 
345
485
  ## Wireframe commands
346
486
 
@@ -115,3 +115,10 @@ at t=0 or grind at ~1 fps. That one is neither parse depth nor `geq` eval cost
115
115
  that wall is engine-internal: `.ai/moat/runtime/macos-swscale-thread-cap.md`
116
116
  (absent in the shipped copy; symptom summary: dense still-image canvases freeze
117
117
  at t=0 / grind ~1 fps on macOS — the engine's threadGuard mitigates it).
118
+ Since 0.4.0 the same guard also caps every video decoder in a dense command:
119
+ past six threaded video inputs each `-i` takes `-threads K` (K = 32 / inputs,
120
+ between 1 and 4), because each mp4 decoder otherwise keeps a frame-thread pool of
121
+ one thread per core plus that many frames in flight — 22 inputs on an 8 GB machine
122
+ swapped until the kernel panicked. Stills, lavfi graphs and the engine's own qtrle
123
+ intermediates are not counted. Measured on the panic case (16 videos, 5 s at
124
+ 1080p, 8 GB): 29 s, 1.3 GB peak, no swap.
@@ -53,7 +53,8 @@ duplicated here.
53
53
  [`prop type: json`](reference/json-prop-type.md) ·
54
54
  [`template flags`](reference/template-flags.md) ·
55
55
  [`grid builder`](reference/grid.md) · [`MosaicColor`](reference/mosaic-color.md) ·
56
- [`prop bindings`](reference/prop-bindings.md)
56
+ [`prop bindings`](reference/prop-bindings.md) ·
57
+ [`brand schema`](reference/brand-schema.md)
57
58
 
58
59
  ## patterns/ — distilled experience
59
60
 
@@ -63,3 +64,6 @@ duplicated here.
63
64
  early template generation.
64
65
  - [`primitive-extraction-pattern.md`](patterns/primitive-extraction-pattern.md)
65
66
  — when/how to extract primitives (incl. Rule 7: verify NESTED).
67
+ - [`print-authoring.md`](patterns/print-authoring.md) — P1–P6 for a deliverable
68
+ that goes to paper: paper's black floor, the gap a pattern needs, recheck the
69
+ small inks, lock it in a test, proof on the stock.
@@ -0,0 +1,97 @@
1
+ # Print authoring rules
2
+
3
+ > Templates layer. For any template whose deliverable goes to paper (cards, banners,
4
+ > wraps, posters). A screen is backlit; paper only reflects, and has a black floor.
5
+ > Tune a print palette on the screen and the shadows that read there print as one
6
+ > flat black. P1–P5 were paid for by a printed business card (2026-10-18); P6 by a
7
+ > 10 ft booth banner (2026-10-16).
8
+
9
+ ## P1 — A dark pattern must clear paper's black floor
10
+ Paper only gets so black. Soft-proofed through Adobe's press profiles, a near-black
11
+ navy (#050314, L\* 1.3 on screen) prints at **L\* 11 on coated stock (FOGRA39)** and
12
+ **L\* 29 on uncoated stock (FOGRA29)**. Everything darker than the floor collapses
13
+ onto it, and the gaps just above it shrink. Keep every ink that must stay distinct
14
+ well clear of it: the pattern at **L\* 28 or more** in the file, at least **25 L\*
15
+ over the ground**. That prints about 20 apart on coated stock and 10 on uncoated.
16
+
17
+ Paid for: `@m0saic/brand/business-card/v1` and `v2` tuned the dark field on screen,
18
+ tiles at L\* 6.5 on an L\* 1.3 navy (`DEFAULT_FIELD_OPACITY = 0.28`,
19
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/brand/business-card/v1/business-card.ts#L142). The
20
+ printed cards were flat black; the brick showed only under a strong light. The
21
+ soft proof agrees: those tiles print 2.4 L\* above the navy on coated stock and 1.6 on
22
+ uncoated. v3's dark tiles default to #434574 (L\* 31.1), which prints 20 and 10.6 above
23
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/brand/business-card/v3/business-card.ts#L174).
24
+
25
+ **Lift the lightness, keep the hue, and raise the chroma — then raise it again for
26
+ paper.** A dark brand colour lifted to print lightness at its old chroma reads as
27
+ grey: v3's first cut, #48465C (chroma 14), was the navy's hue but looked like slate.
28
+ Luminance stays put as chroma rises, so the print gap and every text contrast hold.
29
+ Print also takes chroma away: soft-proofed, chroma 24 printed at 21 (coated) and 17
30
+ (uncoated), and chroma 30 printed at 26 and 20, with the hue steady (294° → 288°).
31
+ Only 30 still read blue on uncoated stock, so v3 ships 30. Judge hue and chroma in
32
+ CIELCh through the press profile, not by eye on a backlit screen.
33
+
34
+ ## P2 — A light pattern needs a smaller gap
35
+ Highlights survive print well. About **9 L\* under the page** reads as "clearly there,
36
+ still soft": business card v3's light tiles #E5DCD0 under an ivory #FAF7F2, the same
37
+ tint as the jewelry booth's printed fabric banner.
38
+
39
+ ## P3 — Lifting a dark pattern lowers text contrast; recheck the small inks
40
+ Text sits on the pattern as well as the ground. When the tiles move from L\* 6.5 to
41
+ 30.8, v2's secondary grey #8A89A3 falls to 2.7:1. v3 lifts it to #B0AFC6 (4.2:1 on
42
+ the tiles, 9.5:1 on the navy) so the 8 pt role / handle / command lines still read
43
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/brand/business-card/v3/business-card.ts#L257). Hold
44
+ secondary inks to **4:1 or better against the pattern**.
45
+
46
+ ## P4 — Lock print contrast in a test
47
+ Compute L\* and contrast from the template's palette function, and assert P1–P3. A
48
+ later screen-only tweak then cannot quietly undo them. Reference: the "print
49
+ contrast" block in
50
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/brand/business-card/v3/business-card.test.ts#L138.
51
+
52
+ ## P5 — Soft-proof, then proof on the stock you are ordering
53
+ Stocks differ: uncoated, coated and fabric each have their own floor and gamut.
54
+ First soft-proof. Take each ink sRGB → the press CMYK profile (relative colorimetric
55
+ with black-point compensation, a RIP's usual RGB path), then → Lab (absolute
56
+ colorimetric) for what the sheet will measure. littleCMS through Pillow's
57
+ `ImageCms` does it with Adobe's `CoatedFOGRA39.icc` / `UncoatedFOGRA29.icc`. Then give
58
+ the reviewer a ladder, a few strengths of the knob that sets the pattern, to print
59
+ before the order. Keep that knob exposed (v3: `fieldOpacity`) so a stock that needs
60
+ more can get it without a new version.
61
+
62
+ Re-verify (as of 2026-10-18):
63
+ `grep -n "DEFAULT_FIELD_OPACITY\|DARK_FIELD_INK\|#B0AFC6" https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/brand/business-card/v3/business-card.ts
64
+
65
+ ## P6 — Print scale: the walls past 8K
66
+ A 10 ft banner at 150 DPI is 18000 × 14400 px, 259 MP. Measured on the 8 GB M1 Air
67
+ (`@qsbuilds/booth/backdrop/v1`, 2026-10-16; re-measured 2026-10-20 through the
68
+ engine's own jpeg path):
69
+
70
+ - **ffmpeg's frame limit** is `(8·W + 1024)·(H + 128) < 2^31 − 1`
71
+ (`av_image_check_size`). 18000 × 14400 passes by about 2 %; at that width a frame
72
+ can be at most 14679 px tall, so a 120 × 102 in artwork (pocket allowance) does not
73
+ fit at 150 DPI (146 DPI is the most). Compute this in the template and refuse with
74
+ the max DPI; do not discover it in ffmpeg.
75
+ - **It renders.** About 105 flat tiles plus one masked M: 128–148 s and ~1.5 GB peak
76
+ RSS for the lossless PNG master, which is under 1 MB for flat colour art. One scaled
77
+ image at the same size goes straight to a jpeg in 6 s at 1.4 GB.
78
+ - **JPEG at that size is the engine's job since 0.4.0.** Every `.jpg` still is
79
+ `mjpeg` at `yuvj444p` with the quantizer pinned to the best tables
80
+ (`-q:v 1 -qmin 1 -qmax 1`), one encoder thread, and `-huffman default` once the
81
+ canvas is 64 MP or more: the encoder's default `-huffman optimal` buffers every
82
+ coefficient of ~12 M blocks and failed to open (`ENOMEM`) at 259 MP. Huffman is
83
+ lossless, so quality is unchanged. `encodes` to `jpeg` / `png` / `tiff` / `bmp`
84
+ reach a real muxer too. A `.jpg` output or a `jpeg` encode is enough; the `mjpeg`
85
+ container with hand-set encoder options is no longer needed.
86
+ - **Metadata:** the engine writes no DPI and no ICC. A JPEG's JFIF APP0 density and
87
+ an APP2 `ICC_PROFILE` can be written afterwards by rebuilding only the header
88
+ segments, leaving the scan data byte-identical (the booth's `render.mjs` does this).
89
+ - **Proof labels:** svg-rasterized text is one inline mask per line; ~45 labels over
90
+ a full-canvas drawbox track drove `OVERLAY_CHAIN_DEEP` to 24. Plain `drawtext`
91
+ labels (no `rasterizer`) carry no mask; the only mask left is the logo.
92
+ - **Keep inches authoritative** and convert edges, not sizes, with one rounding
93
+ rule. Never upscale a proof. Verify the written file (dimensions, chroma sampling,
94
+ density, profile, a full decode), not the plan.
95
+
96
+ Re-verify (as of 2026-10-20):
97
+ `grep -n "DEFAULT_JPEG_STILL_HUGE_PIXELS\|huffman" the render engine source (not published)
@@ -0,0 +1,73 @@
1
+ # A template repo's brand schema (`m0saic.brand.json`)
2
+
3
+ **Since:** Mosaic Desktop 0.4.0 line (2026-10-19). **Applies to:** any template repo a solution or a
4
+ Mosaic Consumer install connects. The CLI ignores the file.
5
+
6
+ ## What it is
7
+
8
+ A person using Mosaic for Jewelry, or a Mosaic Consumer install built around one business, fills in
9
+ their details **once** — "Your brand" on the Templates page: a name, a logo, colours, a website, where
10
+ they post most — and every design that has a place for those opens with them already in. The profile is
11
+ kept per solution on the machine as `~/m0saic/solutions/<id>/brand.m0v`, the `brand` slot of a Mosaic
12
+ Vocabulary (`.m0v`; see `file-formats/m0p-and-custom-field.md` §1).
13
+
14
+ A template repo says **what to ask for** and **which props take it** by shipping `m0saic.brand.json`
15
+ at its root, beside `m0saic.workspace.json`:
16
+
17
+ ```json
18
+ {
19
+ "fields": [
20
+ { "key": "lyricFont", "label": "Your font", "placeholder": "The one your lyrics land in", "kind": "choice",
21
+ "options": [{ "value": "anton", "label": "Anton" }, { "value": "bebas-neue", "label": "Bebas Neue" }] },
22
+ { "key": "textColor", "label": "Lyric colour", "placeholder": "#FFFFFF", "kind": "color" },
23
+ { "key": "fontFile", "label": "Your own font file", "placeholder": "A .ttf or .otf", "kind": "file" },
24
+ { "key": "channel", "label": "Where you post most", "kind": "choice",
25
+ "options": [{ "value": "instagram-reel", "label": "Instagram Reels" }, { "value": "tiktok", "label": "TikTok" }] }
26
+ ],
27
+ "props": { "font": "lyricFont", "textColor": "textColor", "fontFile": "fontFile", "platform": "channel" }
28
+ }
29
+ ```
30
+
31
+ - `fields[]` — what the profile asks for. `key` (a prop-style identifier), `label`, optional
32
+ `placeholder`, and `kind`: `text` | `color` | `file` | `choice`. A `choice` needs `options`
33
+ (`{ value, label }`; a bare string is a value that is its own label). A value outside the options is
34
+ never seeded.
35
+ - `props` — template prop name → field key. **Top-level props only.** The field must be one of
36
+ `fields[]` (or one of the app's own, below).
37
+
38
+ ## What Desktop does with it
39
+
40
+ - Reads the file when the repo loads (a local folder, or a GitHub source once installed), beside the
41
+ manifest; drops it when the source is forgotten.
42
+ - **Seeds on a fresh open.** When a template opens with no props of its own (not from a `.mosaicx`, a
43
+ share link or an agent), every mapped prop the template declares gets the profile's value, folded into
44
+ the template's **defaults** — so the open is not dirty, a rebuild keeps the values, and a saved recipe
45
+ carries them like any value. The person can still change any of them per post. A `media` prop takes
46
+ the file path; a `media[]` prop takes a one-item list.
47
+ - **Asks only what some prop takes.** The profile shows the fields that some mapped prop reads; a field
48
+ nothing reads would promise what no design does.
49
+ - **Merges across repos.** A solution may have a map of its own (the jewelry pack's prop names are declared
50
+ in the app) and several connected repos; the solution's map comes first, then each repo's, and a prop
51
+ is never re-mapped once mapped.
52
+ - **The app's own fields win for their keys:** `name`, `tagline`, `logo`, `accent`, `site`, `email`,
53
+ `phone`, `address`, `platform` (the app's channel list: story / feed / square / landscape / email). A
54
+ repo mapping `brand → name` gets the app's "Brand name" field. A repo whose `platform` prop takes
55
+ **its own** values should map it to a key of its own (`channel`), not `platform`.
56
+
57
+ ## Authoring rules
58
+
59
+ - One schema per repo, at the root. Keep the prop names consistent across the repo's templates — the
60
+ schema is the API the templates adhere to; a template that spells `brandName` differently is simply
61
+ not seeded.
62
+ - Keys are identifiers (`^[a-zA-Z][a-zA-Z0-9]*$`); at most 24 fields. Odd fields and unmapped props are
63
+ dropped, not fatal.
64
+ - The file is outside the signature (`dist/` + the manifest, package and catalog): adding or changing it never
65
+ moves a release's verdict. It rides in `runtime.tgz` when present (packer `BRAND_FILE`, 2026-10-20), so a repo
66
+ installed from a release has it too — **re-pack and re-publish the bundle** after changing it; a clone or the
67
+ dev stand-in reads it straight from disk.
68
+ - The CLI reads none of this; a `.mosaicx` written by Make carries the seeded values as plain props.
69
+
70
+ ## See also
71
+
72
+ - `templates/reference/prop-bindings.md` — how props reach tiles on the canvas.
73
+ - `file-formats/m0p-and-custom-field.md` §1 — `.m0v`, the Mosaic Vocabulary, and its `brand` slot.