@fugood/bricks-project 2.25.0-beta.5 → 2.25.0-beta.50

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 (175) hide show
  1. package/compile/action-name-map.ts +64 -0
  2. package/compile/config-diff.ts +155 -0
  3. package/compile/index.ts +273 -32
  4. package/compile/util.ts +26 -7
  5. package/package.json +8 -3
  6. package/package.json.bak +8 -3
  7. package/skills/bricks-ctor/SKILL.md +23 -17
  8. package/skills/bricks-ctor/{rules → references}/animation.md +3 -2
  9. package/skills/bricks-ctor/{rules → references}/architecture-patterns.md +18 -0
  10. package/skills/bricks-ctor/{rules → references}/automations.md +11 -0
  11. package/skills/bricks-ctor/references/buttress.md +245 -0
  12. package/skills/bricks-ctor/references/data-calculation.md +239 -0
  13. package/skills/bricks-ctor/references/simulator.md +132 -0
  14. package/skills/bricks-ctor/references/source-editing-tools.md +81 -0
  15. package/skills/bricks-ctor/references/verification-toolchain.md +200 -0
  16. package/skills/bricks-design/SKILL.md +150 -45
  17. package/skills/bricks-design/references/architecture-truths.md +132 -0
  18. package/skills/bricks-design/references/avoiding-complexity.md +91 -0
  19. package/skills/bricks-design/references/design-critique.md +195 -0
  20. package/skills/bricks-design/references/design-languages.md +265 -0
  21. package/skills/bricks-design/references/performance.md +116 -0
  22. package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
  23. package/skills/bricks-design/references/translating-inputs.md +152 -0
  24. package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
  25. package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
  26. package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
  27. package/skills/bricks-design/references/workflow.md +134 -0
  28. package/skills/bricks-ux/SKILL.md +114 -0
  29. package/skills/bricks-ux/references/accessibility.md +162 -0
  30. package/skills/bricks-ux/references/flow-states.md +175 -0
  31. package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
  32. package/skills/bricks-ux/references/monitoring-screens.md +153 -0
  33. package/skills/bricks-ux/references/pressable-composition.md +126 -0
  34. package/skills/bricks-ux/references/user-journey.md +168 -0
  35. package/skills/bricks-ux/references/ux-critique.md +256 -0
  36. package/tools/_cli-error.ts +17 -0
  37. package/tools/_edits-log.ts +41 -0
  38. package/tools/_git-author.ts +10 -2
  39. package/tools/_last-pushed-commit.ts +28 -0
  40. package/tools/_shell.ts +8 -1
  41. package/tools/deploy.ts +17 -6
  42. package/tools/mcp-env.ts +13 -0
  43. package/tools/mcp-server.ts +8 -0
  44. package/tools/mcp-tools/_editing-helpers.ts +58 -0
  45. package/tools/mcp-tools/_verify.ts +50 -0
  46. package/tools/mcp-tools/compile.ts +21 -9
  47. package/tools/mcp-tools/data-calc-editing.ts +1349 -0
  48. package/tools/mcp-tools/entry-editing.ts +2336 -0
  49. package/tools/mcp-tools/huggingface.ts +23 -13
  50. package/tools/mcp-tools/icons.ts +23 -7
  51. package/tools/mcp-tools/media.ts +4 -1
  52. package/tools/postinstall.ts +80 -3
  53. package/tools/pull.ts +93 -22
  54. package/tools/push-config.ts +114 -0
  55. package/tools/{preview-main.mjs → simulator-main.mjs} +207 -12
  56. package/tools/simulator-preload.cjs +16 -0
  57. package/tools/{preview.ts → simulator.ts} +4 -4
  58. package/types/{animation.ts → animation.d.ts} +24 -8
  59. package/types/{automation.ts → automation.d.ts} +16 -20
  60. package/types/{brick-base.ts → brick-base.d.ts} +1 -1
  61. package/types/bricks/{Camera.ts → Camera.d.ts} +8 -8
  62. package/types/bricks/{Chart.ts → Chart.d.ts} +4 -4
  63. package/types/bricks/{GenerativeMedia.ts → GenerativeMedia.d.ts} +15 -15
  64. package/types/bricks/{Icon.ts → Icon.d.ts} +7 -7
  65. package/types/bricks/{Image.ts → Image.d.ts} +21 -9
  66. package/types/bricks/{Items.ts → Items.d.ts} +7 -7
  67. package/types/bricks/{Lottie.ts → Lottie.d.ts} +10 -10
  68. package/types/bricks/{Maps.ts → Maps.d.ts} +11 -11
  69. package/types/bricks/{QrCode.ts → QrCode.d.ts} +7 -7
  70. package/types/bricks/{Rect.ts → Rect.d.ts} +7 -7
  71. package/types/bricks/{RichText.ts → RichText.d.ts} +12 -9
  72. package/types/bricks/{Rive.ts → Rive.d.ts} +9 -9
  73. package/types/bricks/Scene3D.d.ts +676 -0
  74. package/types/bricks/{Sketch.ts → Sketch.d.ts} +6 -6
  75. package/types/bricks/{Slideshow.ts → Slideshow.d.ts} +7 -7
  76. package/types/bricks/{Svg.ts → Svg.d.ts} +7 -7
  77. package/types/bricks/{Text.ts → Text.d.ts} +9 -9
  78. package/types/bricks/{TextInput.ts → TextInput.d.ts} +10 -10
  79. package/types/bricks/{Video.ts → Video.d.ts} +12 -12
  80. package/types/bricks/{VideoStreaming.ts → VideoStreaming.d.ts} +10 -10
  81. package/types/bricks/{WebRtcStream.ts → WebRtcStream.d.ts} +1 -1
  82. package/types/bricks/{WebView.ts → WebView.d.ts} +4 -4
  83. package/types/bricks/{index.ts → index.d.ts} +1 -0
  84. package/types/{common.ts → common.d.ts} +3 -6
  85. package/types/data-calc-command/base.d.ts +57 -0
  86. package/types/data-calc-command/collection.d.ts +418 -0
  87. package/types/data-calc-command/color.d.ts +432 -0
  88. package/types/data-calc-command/constant.d.ts +50 -0
  89. package/types/data-calc-command/datetime.d.ts +147 -0
  90. package/types/data-calc-command/file.d.ts +129 -0
  91. package/types/data-calc-command/index.d.ts +13 -0
  92. package/types/data-calc-command/iteratee.d.ts +23 -0
  93. package/types/data-calc-command/logictype.d.ts +190 -0
  94. package/types/data-calc-command/math.d.ts +275 -0
  95. package/types/data-calc-command/object.d.ts +119 -0
  96. package/types/data-calc-command/sandbox.d.ts +66 -0
  97. package/types/data-calc-command/string.d.ts +407 -0
  98. package/types/{data-calc.ts → data-calc.d.ts} +1 -0
  99. package/types/{data.ts → data.d.ts} +4 -2
  100. package/types/generators/{Assistant.ts → Assistant.d.ts} +19 -0
  101. package/types/generators/{LlmGgml.ts → LlmGgml.d.ts} +43 -1
  102. package/types/generators/{LlmMlx.ts → LlmMlx.d.ts} +1 -0
  103. package/types/generators/{RerankerGgml.ts → RerankerGgml.d.ts} +5 -1
  104. package/types/generators/{SoundRecorder.ts → SoundRecorder.d.ts} +10 -1
  105. package/types/generators/{SpeechToTextGgml.ts → SpeechToTextGgml.d.ts} +6 -1
  106. package/types/generators/{SttAppleBuiltin.ts → SttAppleBuiltin.d.ts} +27 -4
  107. package/types/generators/{ThermalPrinter.ts → ThermalPrinter.d.ts} +9 -7
  108. package/types/generators/{VadGgml.ts → VadGgml.d.ts} +12 -2
  109. package/types/{subspace.ts → subspace.d.ts} +1 -1
  110. package/utils/calc.ts +5 -1
  111. package/utils/data.ts +5 -7
  112. package/utils/event-props.ts +17 -0
  113. package/utils/id.ts +109 -56
  114. package/skills/bricks-ctor/rules/buttress.md +0 -156
  115. package/skills/bricks-ctor/rules/data-calculation.md +0 -209
  116. package/skills/bricks-design/LICENSE.txt +0 -180
  117. package/types/data-calc-command.ts +0 -7005
  118. /package/skills/bricks-ctor/{rules → references}/local-sync.md +0 -0
  119. /package/skills/bricks-ctor/{rules → references}/media-flow.md +0 -0
  120. /package/skills/bricks-ctor/{rules → references}/remote-data-bank.md +0 -0
  121. /package/skills/bricks-ctor/{rules → references}/standby-transition.md +0 -0
  122. /package/types/{canvas.ts → canvas.d.ts} +0 -0
  123. /package/types/{data-calc-script.ts → data-calc-script.d.ts} +0 -0
  124. /package/types/generators/{AlarmClock.ts → AlarmClock.d.ts} +0 -0
  125. /package/types/generators/{BleCentral.ts → BleCentral.d.ts} +0 -0
  126. /package/types/generators/{BlePeripheral.ts → BlePeripheral.d.ts} +0 -0
  127. /package/types/generators/{CanvasMap.ts → CanvasMap.d.ts} +0 -0
  128. /package/types/generators/{CastlesPay.ts → CastlesPay.d.ts} +0 -0
  129. /package/types/generators/{DataBank.ts → DataBank.d.ts} +0 -0
  130. /package/types/generators/{File.ts → File.d.ts} +0 -0
  131. /package/types/generators/{GraphQl.ts → GraphQl.d.ts} +0 -0
  132. /package/types/generators/{Http.ts → Http.d.ts} +0 -0
  133. /package/types/generators/{HttpServer.ts → HttpServer.d.ts} +0 -0
  134. /package/types/generators/{Information.ts → Information.d.ts} +0 -0
  135. /package/types/generators/{Intent.ts → Intent.d.ts} +0 -0
  136. /package/types/generators/{Iterator.ts → Iterator.d.ts} +0 -0
  137. /package/types/generators/{Keyboard.ts → Keyboard.d.ts} +0 -0
  138. /package/types/generators/{LlmAnthropicCompat.ts → LlmAnthropicCompat.d.ts} +0 -0
  139. /package/types/generators/{LlmAppleBuiltin.ts → LlmAppleBuiltin.d.ts} +0 -0
  140. /package/types/generators/{LlmMediaTekNeuroPilot.ts → LlmMediaTekNeuroPilot.d.ts} +0 -0
  141. /package/types/generators/{LlmOnnx.ts → LlmOnnx.d.ts} +0 -0
  142. /package/types/generators/{LlmOpenAiCompat.ts → LlmOpenAiCompat.d.ts} +0 -0
  143. /package/types/generators/{LlmQualcommAiEngine.ts → LlmQualcommAiEngine.d.ts} +0 -0
  144. /package/types/generators/{Mcp.ts → Mcp.d.ts} +0 -0
  145. /package/types/generators/{McpServer.ts → McpServer.d.ts} +0 -0
  146. /package/types/generators/{MediaFlow.ts → MediaFlow.d.ts} +0 -0
  147. /package/types/generators/{MqttBroker.ts → MqttBroker.d.ts} +0 -0
  148. /package/types/generators/{MqttClient.ts → MqttClient.d.ts} +0 -0
  149. /package/types/generators/{Question.ts → Question.d.ts} +0 -0
  150. /package/types/generators/{RealtimeTranscription.ts → RealtimeTranscription.d.ts} +0 -0
  151. /package/types/generators/{SerialPort.ts → SerialPort.d.ts} +0 -0
  152. /package/types/generators/{SoundPlayer.ts → SoundPlayer.d.ts} +0 -0
  153. /package/types/generators/{SpeechToTextOnnx.ts → SpeechToTextOnnx.d.ts} +0 -0
  154. /package/types/generators/{SpeechToTextPlatform.ts → SpeechToTextPlatform.d.ts} +0 -0
  155. /package/types/generators/{SqLite.ts → SqLite.d.ts} +0 -0
  156. /package/types/generators/{Step.ts → Step.d.ts} +0 -0
  157. /package/types/generators/{Tcp.ts → Tcp.d.ts} +0 -0
  158. /package/types/generators/{TcpServer.ts → TcpServer.d.ts} +0 -0
  159. /package/types/generators/{TextToSpeechAppleBuiltin.ts → TextToSpeechAppleBuiltin.d.ts} +0 -0
  160. /package/types/generators/{TextToSpeechGgml.ts → TextToSpeechGgml.d.ts} +0 -0
  161. /package/types/generators/{TextToSpeechOnnx.ts → TextToSpeechOnnx.d.ts} +0 -0
  162. /package/types/generators/{TextToSpeechOpenAiLike.ts → TextToSpeechOpenAiLike.d.ts} +0 -0
  163. /package/types/generators/{Tick.ts → Tick.d.ts} +0 -0
  164. /package/types/generators/{Udp.ts → Udp.d.ts} +0 -0
  165. /package/types/generators/{VadOnnx.ts → VadOnnx.d.ts} +0 -0
  166. /package/types/generators/{VadTraditional.ts → VadTraditional.d.ts} +0 -0
  167. /package/types/generators/{VectorStore.ts → VectorStore.d.ts} +0 -0
  168. /package/types/generators/{Watchdog.ts → Watchdog.d.ts} +0 -0
  169. /package/types/generators/{WebCrawler.ts → WebCrawler.d.ts} +0 -0
  170. /package/types/generators/{WebRtc.ts → WebRtc.d.ts} +0 -0
  171. /package/types/generators/{WebSocket.ts → WebSocket.d.ts} +0 -0
  172. /package/types/generators/{index.ts → index.d.ts} +0 -0
  173. /package/types/{index.ts → index.d.ts} +0 -0
  174. /package/types/{switch.ts → switch.d.ts} +0 -0
  175. /package/types/{system.ts → system.d.ts} +0 -0
@@ -350,6 +350,63 @@ export const templateActionNameMap = {
350
350
  strokeWidth: 'BRICK_SKETCH_STROKE_WIDTH',
351
351
  },
352
352
  },
353
+ BRICK_SCENE_3D: {
354
+ BRICK_SCENE_3D_ADD_OBJECT: {
355
+ objectId: 'BRICK_SCENE_3D_OBJECT_ID',
356
+ objectType: 'BRICK_SCENE_3D_OBJECT_TYPE',
357
+ objectUrl: 'BRICK_SCENE_3D_OBJECT_URL',
358
+ objectMd5: 'BRICK_SCENE_3D_OBJECT_MD5',
359
+ objectPosition: 'BRICK_SCENE_3D_OBJECT_POSITION',
360
+ objectRotation: 'BRICK_SCENE_3D_OBJECT_ROTATION',
361
+ objectScale: 'BRICK_SCENE_3D_OBJECT_SCALE',
362
+ objectColor: 'BRICK_SCENE_3D_OBJECT_COLOR',
363
+ },
364
+ BRICK_SCENE_3D_REMOVE_OBJECT: {
365
+ objectId: 'BRICK_SCENE_3D_OBJECT_ID',
366
+ },
367
+ BRICK_SCENE_3D_UPDATE_OBJECT: {
368
+ objectId: 'BRICK_SCENE_3D_OBJECT_ID',
369
+ objectPosition: 'BRICK_SCENE_3D_OBJECT_POSITION',
370
+ objectRotation: 'BRICK_SCENE_3D_OBJECT_ROTATION',
371
+ objectScale: 'BRICK_SCENE_3D_OBJECT_SCALE',
372
+ objectVisible: 'BRICK_SCENE_3D_OBJECT_VISIBLE',
373
+ objectColor: 'BRICK_SCENE_3D_OBJECT_COLOR',
374
+ objectNodes: 'BRICK_SCENE_3D_OBJECT_NODES',
375
+ },
376
+ BRICK_SCENE_3D_SET_CAMERA: {
377
+ cameraPosition: 'BRICK_SCENE_3D_CAMERA_POSITION',
378
+ cameraTarget: 'BRICK_SCENE_3D_CAMERA_TARGET',
379
+ cameraFov: 'BRICK_SCENE_3D_CAMERA_FOV',
380
+ cameraAnimateMs: 'BRICK_SCENE_3D_CAMERA_ANIMATE_MS',
381
+ },
382
+ BRICK_SCENE_3D_LOOK_AT: {
383
+ objectId: 'BRICK_SCENE_3D_OBJECT_ID',
384
+ },
385
+ BRICK_SCENE_3D_PLAY_ANIMATION: {
386
+ objectId: 'BRICK_SCENE_3D_OBJECT_ID',
387
+ animationName: 'BRICK_SCENE_3D_ANIMATION_NAME',
388
+ animationLoop: 'BRICK_SCENE_3D_ANIMATION_LOOP',
389
+ animationSpeed: 'BRICK_SCENE_3D_ANIMATION_SPEED',
390
+ },
391
+ BRICK_SCENE_3D_STOP_ANIMATION: {
392
+ objectId: 'BRICK_SCENE_3D_OBJECT_ID',
393
+ animationName: 'BRICK_SCENE_3D_ANIMATION_NAME',
394
+ },
395
+ BRICK_SCENE_3D_SET_BACKGROUND: {
396
+ backgroundColor: 'BRICK_SCENE_3D_BACKGROUND_COLOR',
397
+ backgroundHdrUrl: 'BRICK_SCENE_3D_BACKGROUND_HDR_URL',
398
+ backgroundMd5: 'BRICK_SCENE_3D_BACKGROUND_MD5',
399
+ },
400
+ BRICK_SCENE_3D_SET_CONTROLS: {
401
+ controlsEnabled: 'BRICK_SCENE_3D_CONTROLS_ENABLED',
402
+ controlsAutoRotate: 'BRICK_SCENE_3D_CONTROLS_AUTO_ROTATE',
403
+ controlsAutoRotateSpeed: 'BRICK_SCENE_3D_CONTROLS_AUTO_ROTATE_SPEED',
404
+ },
405
+ BRICK_SCENE_3D_SCREENSHOT: {
406
+ screenshotFormat: 'BRICK_SCENE_3D_SCREENSHOT_FORMAT',
407
+ screenshotQuality: 'BRICK_SCENE_3D_SCREENSHOT_QUALITY',
408
+ },
409
+ },
353
410
 
354
411
  GENERATOR_FILE: {
355
412
  GENERATOR_FILE_READ_CONTENT: {
@@ -782,6 +839,11 @@ export const templateActionNameMap = {
782
839
  seed: 'GENERATOR_LLM_SEED',
783
840
  typicalP: 'GENERATOR_LLM_TYPICAL_P',
784
841
  ignoreEos: 'GENERATOR_LLM_IGNORE_EOS',
842
+ mtpSpeculativeDecoding: 'GENERATOR_LLM_MTP_SPECULATIVE_DECODING',
843
+ mtpDraftTokens: 'GENERATOR_LLM_MTP_DRAFT_TOKENS',
844
+ mtpDraftMinTokens: 'GENERATOR_LLM_MTP_DRAFT_MIN_TOKENS',
845
+ mtpDraftMinProbability: 'GENERATOR_LLM_MTP_DRAFT_MIN_PROBABILITY',
846
+ mtpDraftSplitProbability: 'GENERATOR_LLM_MTP_DRAFT_SPLIT_PROBABILITY',
785
847
  functionCallEnabled: 'GENERATOR_LLM_FUNCTION_CALL_ENABLED',
786
848
  functionCallSchema: 'GENERATOR_LLM_FUNCTION_CALL_SCHEMA',
787
849
  },
@@ -894,10 +956,12 @@ export const templateActionNameMap = {
894
956
  GENERATOR_APPLE_STT_TRANSCRIBE_FILE: {
895
957
  fileUrl: 'GENERATOR_APPLE_STT_FILE_URL',
896
958
  language: 'GENERATOR_APPLE_STT_LANGUAGE',
959
+ contextualStrings: 'GENERATOR_APPLE_STT_CONTEXTUAL_STRINGS',
897
960
  },
898
961
  GENERATOR_APPLE_STT_TRANSCRIBE_DATA: {
899
962
  data: 'GENERATOR_APPLE_STT_DATA',
900
963
  language: 'GENERATOR_APPLE_STT_LANGUAGE',
964
+ contextualStrings: 'GENERATOR_APPLE_STT_CONTEXTUAL_STRINGS',
901
965
  },
902
966
  },
903
967
  GENERATOR_APPLE_TTS: {
@@ -0,0 +1,155 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ // The compiled config artifact written by `bun compile` (see the generated
5
+ // project's `compile.ts`; bricks-project-generator/index.js:762).
6
+ export const BUILD_CONFIG_RELATIVE = '.bricks/build/application-config.json'
7
+
8
+ // `compile()` derives these top-level fields from `Date.now()`
9
+ // (compile/index.ts `title: \`${app.name}(${timestamp})\`` and `update_timestamp`),
10
+ // so they differ on every run regardless of source. Excluded from the comparison —
11
+ // only the *top-level* keys are dropped, so nested subspace/brick `title`s still diff.
12
+ const VOLATILE_TOP_LEVEL_FIELDS = ['title', 'update_timestamp']
13
+
14
+ // A minimal, path-keyed config change (Option A): a generic patch, not coupled to
15
+ // bricks-config-editor's action vocabulary. `path` is an array of object keys /
16
+ // array indices, ready for the editor's `setYValueAtPath` (or a delete) so applying
17
+ // the patch is a single Yjs `'local'` transaction == one undo/redo entry.
18
+ export type ConfigPatchOp =
19
+ | { op: 'set'; path: Array<string | number>; value: unknown }
20
+ | { op: 'unset'; path: Array<string | number> }
21
+
22
+ export type ConfigChange =
23
+ | { status: 'ok'; ops: ConfigPatchOp[]; opCount: number }
24
+ | { status: 'no_baseline' }
25
+ | { status: 'unavailable' }
26
+
27
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
28
+ typeof value === 'object' && value !== null && !Array.isArray(value)
29
+
30
+ const isJsonDroppedValue = (value: unknown) =>
31
+ value === undefined || typeof value === 'function' || typeof value === 'symbol'
32
+
33
+ const toJsonComparableScalar = (value: unknown) => {
34
+ if (typeof value === 'number' && !Number.isFinite(value)) return null
35
+ return value
36
+ }
37
+
38
+ const toJsonCompatibleValue = (value: unknown): unknown => {
39
+ if (isJsonDroppedValue(value)) return undefined
40
+ if (Array.isArray(value)) {
41
+ return value.map((item) => (isJsonDroppedValue(item) ? null : toJsonCompatibleValue(item)))
42
+ }
43
+ if (isRecord(value)) {
44
+ return Object.entries(value).reduce((acc, [key, item]) => {
45
+ if (!isJsonDroppedValue(item)) acc[key] = toJsonCompatibleValue(item)
46
+ return acc
47
+ }, {})
48
+ }
49
+ return toJsonComparableScalar(value)
50
+ }
51
+
52
+ const hasJsonObjectKey = (value: Record<string, unknown>, key: string) =>
53
+ Object.prototype.hasOwnProperty.call(value, key) && !isJsonDroppedValue(value[key])
54
+
55
+ const getJsonArrayItem = (value: unknown[], index: number) => {
56
+ const item = value[index]
57
+ return isJsonDroppedValue(item) ? null : item
58
+ }
59
+
60
+ const deepEqual = (a: unknown, b: unknown): boolean => {
61
+ a = toJsonComparableScalar(a)
62
+ b = toJsonComparableScalar(b)
63
+ if (a === b) return true
64
+ if (Array.isArray(a) && Array.isArray(b)) {
65
+ if (a.length !== b.length) return false
66
+ for (let index = 0; index < a.length; index += 1) {
67
+ if (!deepEqual(getJsonArrayItem(a, index), getJsonArrayItem(b, index))) return false
68
+ }
69
+ return true
70
+ }
71
+ if (isRecord(a) && isRecord(b)) {
72
+ let comparableAKeys = 0
73
+ for (const key of Object.keys(a)) {
74
+ if (isJsonDroppedValue(a[key])) continue
75
+ comparableAKeys += 1
76
+ if (!hasJsonObjectKey(b, key) || !deepEqual(a[key], b[key])) return false
77
+ }
78
+
79
+ let comparableBKeys = 0
80
+ for (const key of Object.keys(b)) {
81
+ if (!isJsonDroppedValue(b[key])) comparableBKeys += 1
82
+ }
83
+ return comparableAKeys === comparableBKeys
84
+ }
85
+ return false
86
+ }
87
+
88
+ const normalizeConfig = (config: unknown) => {
89
+ if (!isRecord(config)) return config
90
+ const normalized = { ...config }
91
+ for (const field of VOLATILE_TOP_LEVEL_FIELDS) delete normalized[field]
92
+ return normalized
93
+ }
94
+
95
+ // Objects recurse key-wise; equal-length arrays recurse element-wise; everything else
96
+ // (scalars, type changes, length-changed arrays) emits one whole-value `set` at that
97
+ // path. The editor's `applyJsonDiffToYType` minimizes the actual Yjs ops downstream,
98
+ // so a whole-array `set` on insert/remove still yields a minimal CRDT mutation.
99
+ const diffInto = (
100
+ before: unknown,
101
+ after: unknown,
102
+ currentPath: Array<string | number>,
103
+ ops: ConfigPatchOp[],
104
+ ) => {
105
+ if (deepEqual(before, after)) return
106
+
107
+ if (isRecord(before) && isRecord(after)) {
108
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)])
109
+ for (const key of keys) {
110
+ const beforeHasKey = hasJsonObjectKey(before, key)
111
+ const afterHasKey = hasJsonObjectKey(after, key)
112
+ if (!beforeHasKey && !afterHasKey) continue
113
+
114
+ const nextPath = [...currentPath, key]
115
+ if (!afterHasKey) ops.push({ op: 'unset', path: nextPath })
116
+ else if (!beforeHasKey) {
117
+ ops.push({ op: 'set', path: nextPath, value: toJsonCompatibleValue(after[key]) })
118
+ } else diffInto(before[key], after[key], nextPath, ops)
119
+ }
120
+ return
121
+ }
122
+
123
+ if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
124
+ for (let index = 0; index < after.length; index += 1) {
125
+ diffInto(
126
+ getJsonArrayItem(before, index),
127
+ getJsonArrayItem(after, index),
128
+ [...currentPath, index],
129
+ ops,
130
+ )
131
+ }
132
+ return
133
+ }
134
+
135
+ ops.push({ op: 'set', path: currentPath, value: toJsonCompatibleValue(after) })
136
+ }
137
+
138
+ // Diff two compiled configs. `before == null` means there was no prior build to compare
139
+ // (first compile); `after == null` means the fresh artifact could not be read.
140
+ export const computeConfigChange = (before: unknown, after: unknown): ConfigChange => {
141
+ if (before == null) return { status: 'no_baseline' }
142
+ if (after == null) return { status: 'unavailable' }
143
+ const ops: ConfigPatchOp[] = []
144
+ diffInto(normalizeConfig(before), normalizeConfig(after), [], ops)
145
+ return { status: 'ok', ops, opCount: ops.length }
146
+ }
147
+
148
+ // Read the last-compiled config artifact. Returns null when it is absent or unreadable.
149
+ export const readBuildConfig = async (projectDir: string): Promise<unknown> => {
150
+ try {
151
+ return JSON.parse(await readFile(path.join(projectDir, BUILD_CONFIG_RELATIVE), 'utf8'))
152
+ } catch {
153
+ return null
154
+ }
155
+ }
package/compile/index.ts CHANGED
@@ -6,10 +6,14 @@ import omit from 'lodash/omit'
6
6
  import { parse as parseAST } from 'acorn'
7
7
  import type { ExportNamedDeclaration, FunctionDeclaration } from 'acorn'
8
8
  import escodegen from 'escodegen'
9
- import { makeId } from '../utils/id'
9
+ import { makeSeededId } from '../utils/id'
10
10
  import { generateCalulationMap } from './util'
11
11
  import { templateActionNameMap } from './action-name-map'
12
12
  import { templateEventPropsMap } from '../utils/event-props'
13
+ import { sh } from '../tools/_shell'
14
+ import { computeConfigChange, readBuildConfig } from './config-diff'
15
+ import { appendEditRecord, editProvenance } from '../tools/_edits-log'
16
+ import { isTruthyEnv } from '../tools/mcp-env'
13
17
  import type {
14
18
  Application,
15
19
  Data,
@@ -154,11 +158,13 @@ const basicAnimationEvents = ['show', 'standby', 'breatheStart']
154
158
 
155
159
  const compileAnimations = (
156
160
  templateKey: string,
157
- animations: { [key: string]: Animation },
161
+ animations: { [key: string]: Animation | (() => Animation) },
158
162
  errorReference: string,
159
163
  ) =>
160
164
  Object.entries(animations).reduce((acc, [key, animation]) => {
161
- const animationId = assertEntryId(animation?.id, 'ANIMATION', errorReference)
165
+ // Animation events accept either a direct Animation or a getter; unwrap.
166
+ const resolved = typeof animation === 'function' ? animation() : animation
167
+ const animationId = assertEntryId(resolved?.id, 'ANIMATION', errorReference)
162
168
  acc[convertEventKey(basicAnimationEvents.includes(key) ? 'BRICK' : templateKey, key)] =
163
169
  `ANIMATION#${animationId}`
164
170
  return acc
@@ -179,10 +185,17 @@ const compileEvents = (
179
185
 
180
186
  let handlerKey
181
187
  let handlerTemplateKey
182
- if (handler === 'system' || typeof handler === 'string') {
183
- if (handler.startsWith('SUBSPACE_')) handlerKey = handler
184
- else handlerKey = handler.toUpperCase()
185
- if (handlerKey === 'SYSTEM') handlerTemplateKey = 'SYSTEM'
188
+ if (typeof handler === 'string') {
189
+ // Only the literal 'system' handler is normalized to the SYSTEM template key.
190
+ // SubspaceID (SUBSPACE_*) and ItemBrickID handlers are kept verbatim: the runtime
191
+ // resolves them case-sensitively (see mapEventMapHandlersWithNewId), so uppercasing
192
+ // a mixed-case ItemBrickID would break handler-to-item event wiring.
193
+ if (handler === 'system') {
194
+ handlerKey = 'SYSTEM'
195
+ handlerTemplateKey = 'SYSTEM'
196
+ } else {
197
+ handlerKey = handler
198
+ }
186
199
  } else if (typeof handler === 'function') {
187
200
  let instance = handler()
188
201
  if (instance?.id) {
@@ -301,6 +314,162 @@ const animationTypeMap = {
301
314
  AnimationTimingConfig: 'timing',
302
315
  AnimationSpringConfig: 'spring',
303
316
  AnimationDecayConfig: 'decay',
317
+ } as const
318
+
319
+ type CompiledAnimationType = (typeof animationTypeMap)[keyof typeof animationTypeMap]
320
+ type WarningMetadata = Record<string, unknown>
321
+
322
+ const animationProperties = new Set([
323
+ 'transform.translateX',
324
+ 'transform.translateY',
325
+ 'transform.scale',
326
+ 'transform.scaleX',
327
+ 'transform.scaleY',
328
+ 'transform.rotate',
329
+ 'transform.rotateX',
330
+ 'transform.rotateY',
331
+ 'opacity',
332
+ ])
333
+
334
+ const animationComposeTypes = new Set(['parallel', 'sequence'])
335
+ const springConfigFamilies = [
336
+ ['stiffness', 'damping', 'mass'],
337
+ ['tension', 'friction'],
338
+ ['bounciness', 'speed'],
339
+ ]
340
+ const springConfigFamilyKeys = new Set(springConfigFamilies.flat())
341
+
342
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
343
+ Boolean(value) && typeof value === 'object' && !Array.isArray(value)
344
+
345
+ const hasDefinedConfigValue = (config: Record<string, unknown>, key: string) =>
346
+ config[key] !== undefined
347
+
348
+ const assertConfigValue = (
349
+ config: Record<string, unknown>,
350
+ key: string,
351
+ errorReference: string,
352
+ ) => {
353
+ if (!hasDefinedConfigValue(config, key)) {
354
+ throw new Error(`Invalid animation config ${errorReference}: missing "${key}"`)
355
+ }
356
+ }
357
+
358
+ const assertAnimationProperty = (property: unknown, errorReference: string) => {
359
+ if (typeof property !== 'string' || !animationProperties.has(property)) {
360
+ throw new Error(
361
+ `Invalid animation property${errorReference ? ` ${errorReference}` : ''}: ${String(
362
+ property,
363
+ )}`,
364
+ )
365
+ }
366
+ return property
367
+ }
368
+
369
+ const getAnimationType = (config: unknown, errorReference: string): CompiledAnimationType => {
370
+ if (!isRecord(config)) {
371
+ throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
372
+ }
373
+
374
+ const animationType = animationTypeMap[config.__type as keyof typeof animationTypeMap]
375
+ if (!animationType) {
376
+ throw new Error(`Invalid animation config type ${errorReference}: ${String(config.__type)}`)
377
+ }
378
+ return animationType
379
+ }
380
+
381
+ const assertAnimationComposeType = (composeType: unknown, errorReference: string) => {
382
+ if (typeof composeType !== 'string' || !animationComposeTypes.has(composeType)) {
383
+ throw new Error(`Invalid animation compose type ${errorReference}: ${String(composeType)}`)
384
+ }
385
+ return composeType
386
+ }
387
+
388
+ const pickDefinedConfigValues = (config: Record<string, unknown>, keys: string[]) =>
389
+ keys.reduce((acc, key) => {
390
+ if (hasDefinedConfigValue(config, key)) acc[key] = config[key]
391
+ return acc
392
+ }, {})
393
+
394
+ const getDefinedConfigKeys = (config: Record<string, unknown>, keys: string[]) =>
395
+ keys.filter((key) => hasDefinedConfigValue(config, key))
396
+
397
+ const formatWarningMetadata = (metadata: WarningMetadata = {}) =>
398
+ Object.entries(metadata)
399
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
400
+ .map(([key, value]) => `${key}: ${String(value)}`)
401
+ .join(', ')
402
+
403
+ const formatWarningReference = (metadata?: WarningMetadata) => {
404
+ const metadataText = formatWarningMetadata(metadata)
405
+ return metadataText ? ` [${metadataText}]` : ''
406
+ }
407
+
408
+ const normalizeSpringConfig = (
409
+ config: Record<string, unknown>,
410
+ errorReference: string,
411
+ warningMetadata?: WarningMetadata,
412
+ ): Record<string, unknown> => {
413
+ assertConfigValue(config, 'toValue', errorReference)
414
+
415
+ const usedFamilies = springConfigFamilies.filter((keys) =>
416
+ keys.some((key) => hasDefinedConfigValue(config, key)),
417
+ )
418
+ if (usedFamilies.length <= 1) return config
419
+
420
+ const configWithoutSpringFamily = Object.entries(config).reduce((acc, [key, value]) => {
421
+ if (!springConfigFamilyKeys.has(key)) acc[key] = value
422
+ return acc
423
+ }, {})
424
+
425
+ // Match runtime normalization: physical spring values are most explicit,
426
+ // otherwise preserve BRICKS' historical tension/friction controls.
427
+ const resolvedFamily =
428
+ usedFamilies.find((keys) => keys.includes('stiffness')) ||
429
+ usedFamilies.find((keys) => keys.includes('tension')) ||
430
+ usedFamilies[0]
431
+ const resolvedFamilyKeys = getDefinedConfigKeys(config, resolvedFamily)
432
+ const droppedFamilyKeys = usedFamilies
433
+ .filter((keys) => keys !== resolvedFamily)
434
+ .flatMap((keys) => getDefinedConfigKeys(config, keys))
435
+
436
+ console.warn(
437
+ `[Warning] Resolved animation spring config${formatWarningReference(
438
+ warningMetadata,
439
+ )}: using ${resolvedFamilyKeys.join('/')}, dropping ${droppedFamilyKeys.join('/')}`,
440
+ )
441
+
442
+ return {
443
+ ...configWithoutSpringFamily,
444
+ ...pickDefinedConfigValues(config, resolvedFamily),
445
+ }
446
+ }
447
+
448
+ const compileAnimationConfig = (
449
+ animationType: CompiledAnimationType,
450
+ config: unknown,
451
+ errorReference: string,
452
+ warningMetadata?: WarningMetadata,
453
+ ) => {
454
+ if (!isRecord(config)) {
455
+ throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
456
+ }
457
+
458
+ const compiledConfig = compileProperty(omit(config, '__type'), errorReference)
459
+
460
+ if (!isRecord(compiledConfig)) {
461
+ throw new Error(`Invalid animation config ${errorReference}: config must compile to an object`)
462
+ }
463
+
464
+ if (animationType === 'timing') {
465
+ assertConfigValue(compiledConfig, 'toValue', errorReference)
466
+ } else if (animationType === 'spring') {
467
+ return normalizeSpringConfig(compiledConfig, errorReference, warningMetadata)
468
+ } else if (animationType === 'decay') {
469
+ assertConfigValue(compiledConfig, 'velocity', errorReference)
470
+ }
471
+
472
+ return compiledConfig
304
473
  }
305
474
 
306
475
  const compileFrame = (frame: Canvas['items'][number]['frame']) => ({
@@ -324,9 +493,12 @@ const preloadTypes = [
324
493
  'media-resource-audio',
325
494
  'media-resource-file',
326
495
  'lottie-file-uri',
496
+ 'rive-file-uri',
327
497
  'ggml-model-asset',
328
498
  'gguf-model-asset',
329
499
  'binary-asset',
500
+ 'mlx-model-asset',
501
+ 'scene3d-objects',
330
502
  ]
331
503
 
332
504
  const compileKind = (kind: Data['kind']) => {
@@ -353,9 +525,10 @@ const compileKind = (kind: Data['kind']) => {
353
525
  }
354
526
 
355
527
  const compileRemoteUpdate = (remoteUpdate: Data['remoteUpdate']) => {
356
- if (!remoteUpdate) return {}
357
- if (remoteUpdate.type === 'auto') return { enable_remote_update: true }
528
+ if (!remoteUpdate) return { bank_type: 'none' }
529
+ if (remoteUpdate.type === 'auto') return { bank_type: 'create', enable_remote_update: true }
358
530
  return {
531
+ bank_type: remoteUpdate.type === 'device-specific' ? 'create-device-specific' : 'global',
359
532
  enable_remote_update: true,
360
533
  ...(remoteUpdate.type === 'device-specific' ? { use_remote_id_prefix: true } : {}),
361
534
  ...(remoteUpdate.type === 'global-data' ? { global_remote_update_prop: remoteUpdate.id } : {}),
@@ -485,6 +658,7 @@ const compileAutomationTest = (
485
658
 
486
659
  return {
487
660
  id: testId,
661
+ alias: test.alias,
488
662
  title: test.title,
489
663
  hide_short_ref: test.hideShortRef,
490
664
  timeout: test.timeout,
@@ -538,8 +712,38 @@ const compileAutomation = (automationMap: AutomationMap) =>
538
712
  }),
539
713
  )
540
714
 
715
+ // Record the minimal compiled-config delta this compile produced to the shared audit
716
+ // log (`.bricks/edits.jsonl`), so editing files directly and running `bun compile`
717
+ // leaves the same trail as the MCP source-editing tools. Maintained only in the
718
+ // editing-tools context (`BRICKS_CTOR_ENABLE_EDITING_TOOLS`); the source-editing tools
719
+ // turn it off for their verify compiles (see _verify.ts) so a tool edit records one
720
+ // richer entry instead of an extra generic compile entry. Also silent when there is no
721
+ // prior build to diff against (fresh projects, package tests, tooling outside a project).
722
+ const recordConfigChange = async (previousConfig: unknown, config: unknown) => {
723
+ if (previousConfig == null) return
724
+ if (!isTruthyEnv(process.env.BRICKS_CTOR_ENABLE_EDITING_TOOLS)) return
725
+ // The baseline was parsed from JSON; `computeConfigChange` applies the same
726
+ // JSON-omitted-field rules lazily so compile avoids cloning the full config.
727
+ const change = computeConfigChange(previousConfig, config)
728
+ if (change.status !== 'ok') return
729
+ await appendEditRecord(process.cwd(), {
730
+ ts: new Date().toISOString(),
731
+ tool: 'compile',
732
+ provenance: editProvenance(),
733
+ outcome: 'ok',
734
+ summary:
735
+ change.opCount === 0
736
+ ? 'compile: no config change'
737
+ : `compile: ${change.opCount} config op(s)`,
738
+ configChange: change,
739
+ }).catch(() => undefined)
740
+ }
741
+
541
742
  export const compile = async (app: Application) => {
542
743
  await new Promise((resolve) => setImmediate(resolve, 0))
744
+ // Snapshot the prior build artifact before the caller's compile.ts overwrites it, so
745
+ // the config change introduced by this compile can be recorded on return.
746
+ const previousConfig = await readBuildConfig(process.cwd())
543
747
  const timestamp = Date.now()
544
748
  // Pre-index subspace ids so the canvas-item validation below stays O(1).
545
749
  const subspaceIdSet = new Set(app.subspaces.map((s) => s.id))
@@ -559,7 +763,14 @@ export const compile = async (app: Application) => {
559
763
  // validation (root_canvas_id is required before the conditional
560
764
  // schema fix is published).
561
765
  if (subspace.module?.link) {
562
- const placeholderCanvasId = makeId('canvas')
766
+ // Seed the placeholder id from the (stable) subspace id. `makeId('canvas')` would take
767
+ // the count-fallback branch (a process-global counter that is never reset), so the
768
+ // placeholder id depended on how many prior count-fallback ids had been minted — making
769
+ // it differ between recompiles and breaking compile's byte-stable-output contract
770
+ // (phantom config-change ops). `makeSeededId` keeps no global state, so identical source
771
+ // recompiles to an identical id. (`makeId('canvas', alias)` would instead throw
772
+ // "Duplicate makeId alias" on the second compile in a long-lived process.)
773
+ const placeholderCanvasId = makeSeededId('canvas', `${subspaceId}:module-placeholder`)
563
774
  subspaceMap[subspaceId] = {
564
775
  title: subspace.title,
565
776
  description: subspace.description,
@@ -629,39 +840,63 @@ export const compile = async (app: Application) => {
629
840
  `(animation index: ${animationIndex}, subspace: ${subspaceId})`,
630
841
  )
631
842
 
632
- if (animation.__typename === 'Animation') {
843
+ const animationTypename = animation.__typename
844
+ if (animationTypename === 'Animation') {
633
845
  const animationDef = animation as AnimationDef
846
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
847
+ const animationWarningMetadata = {
848
+ animationIndex,
849
+ animationTitle: animationDef.title,
850
+ animationAlias: animationDef.alias,
851
+ animationProperty: animationDef.property,
852
+ subspaceIndex,
853
+ subspaceTitle: subspace.title,
854
+ }
855
+ const animationType = getAnimationType(animationDef.config, animationErrorReference)
634
856
  map[animationId] = {
635
857
  alias: animationDef.alias,
636
858
  title: animationDef.title,
637
859
  description: animationDef.description,
638
860
  hide_short_ref: animationDef.hideShortRef,
639
861
  animationRunType: animationDef.runType,
640
- property: animationDef.property,
641
- type: animationTypeMap[animationDef.config.__type],
642
- config: compileProperty(
643
- omit(animationDef.config, '__type'),
644
- `(animation: ${animationId}, subspace ${subspaceId})`,
862
+ property: assertAnimationProperty(animationDef.property, animationErrorReference),
863
+ type: animationType,
864
+ config: compileAnimationConfig(
865
+ animationType,
866
+ animationDef.config,
867
+ animationErrorReference,
868
+ animationWarningMetadata,
645
869
  ),
646
870
  }
647
- } else if (animation.__typename === 'AnimationCompose') {
871
+ } else if (animationTypename === 'AnimationCompose') {
648
872
  const animationDef = animation as AnimationComposeDef
873
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
649
874
  map[animationId] = {
650
875
  alias: animationDef.alias,
651
876
  title: animationDef.title,
652
877
  description: animationDef.description,
653
878
  hide_short_ref: animationDef.hideShortRef,
654
879
  animationRunType: animationDef.runType,
655
- compose_type: animationDef.composeType,
880
+ compose_type: assertAnimationComposeType(
881
+ animationDef.composeType,
882
+ animationErrorReference,
883
+ ),
656
884
  item_list: animationDef.items.map((item, index) => {
657
885
  const innerAnimation = item()
658
- if (!innerAnimation?.id)
659
- throw new Error(
660
- `Invalid animation index: ${index} (animation: ${innerAnimation.id}, subspace ${subspaceId})`,
661
- )
662
- return { animation_id: innerAnimation.id }
886
+ const innerAnimationId = assertEntryId(
887
+ innerAnimation?.id,
888
+ 'ANIMATION',
889
+ `(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
890
+ )
891
+ return { animation_id: innerAnimationId }
663
892
  }),
664
893
  }
894
+ } else {
895
+ throw new Error(
896
+ `Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
897
+ animationTypename,
898
+ )}`,
899
+ )
665
900
  }
666
901
  return map
667
902
  }, {}),
@@ -1009,7 +1244,7 @@ export const compile = async (app: Application) => {
1009
1244
  ...compileRemoteUpdate(data.remoteUpdate),
1010
1245
  routing: data.routing,
1011
1246
  schema: data.schema,
1012
- type: data.type,
1247
+ type: data.type === 'boolean' ? 'bool' : data.type,
1013
1248
  ...compileKind(data.kind),
1014
1249
  value: compileProperty(data.value, `(data: ${dataId}, subspace ${subspaceId})`),
1015
1250
  event_map: compileEvents('PROPERTY_BANK', data.events || {}, {
@@ -1029,6 +1264,7 @@ export const compile = async (app: Application) => {
1029
1264
  )
1030
1265
 
1031
1266
  const calc: any = {
1267
+ alias: dataCalc.alias,
1032
1268
  title: dataCalc.title,
1033
1269
  description: dataCalc.description,
1034
1270
  hide_short_ref: dataCalc.hideShortRef,
@@ -1227,12 +1463,7 @@ export const compile = async (app: Application) => {
1227
1463
  : null,
1228
1464
  }
1229
1465
 
1230
- Object.assign(
1231
- calc,
1232
- generateCalulationMap(calc.script_config, {
1233
- snapshotMode: process.env.BRICKS_SNAPSHOT_MODE === '1',
1234
- }),
1235
- )
1466
+ Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
1236
1467
  }
1237
1468
  map[dataCalcId] = calc
1238
1469
  return map
@@ -1269,10 +1500,20 @@ export const compile = async (app: Application) => {
1269
1500
  automation_map: compiledAutomationMap || app.metadata?.TEMP_automation_map || {},
1270
1501
  update_timestamp: timestamp,
1271
1502
  }
1503
+ await recordConfigChange(previousConfig, config)
1272
1504
  return config
1273
1505
  }
1274
1506
 
1275
1507
  export const checkConfig = async (configPath: string) => {
1276
- const { sh } = await import('../tools/_shell')
1277
- await sh`bricks app check-config ${configPath}`
1508
+ // --validate-automation surfaces broken automation_map / test_map refs early,
1509
+ // which catches agent-authored automations that reference deleted bricks.
1510
+ await sh`bricks app check-config --validate-automation ${configPath}`
1511
+ // Doctor adds semantic lint checks after structural validation. Warnings are
1512
+ // surfaced in the compile log, but only errors fail by default. Older published
1513
+ // bricks-cli builds lack `app doctor` — skip rather than fail the compile.
1514
+ const doctor = await sh`bricks app doctor --validate-automation ${configPath}`.nothrow()
1515
+ if (doctor.exitCode !== 0) {
1516
+ if (/unknown command/i.test(doctor.stderr?.toString() ?? '')) return
1517
+ throw new Error(`bricks app doctor failed with exit ${doctor.exitCode}`)
1518
+ }
1278
1519
  }