@fugood/bricks-ctor 2.25.0-beta.5 → 2.25.0-beta.51

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 (187) hide show
  1. package/compile/__tests__/config-diff.test.js +100 -0
  2. package/compile/__tests__/index.test.js +386 -0
  3. package/compile/__tests__/util.test.js +337 -0
  4. package/compile/action-name-map.ts +64 -0
  5. package/compile/config-diff.ts +155 -0
  6. package/compile/index.ts +278 -34
  7. package/compile/util.ts +34 -10
  8. package/package.json +7 -3
  9. package/skills/bricks-ctor/SKILL.md +23 -17
  10. package/skills/bricks-ctor/{rules → references}/animation.md +3 -2
  11. package/skills/bricks-ctor/{rules → references}/architecture-patterns.md +18 -0
  12. package/skills/bricks-ctor/{rules → references}/automations.md +11 -0
  13. package/skills/bricks-ctor/references/buttress.md +245 -0
  14. package/skills/bricks-ctor/references/data-calculation.md +239 -0
  15. package/skills/bricks-ctor/references/simulator.md +132 -0
  16. package/skills/bricks-ctor/references/source-editing-tools.md +81 -0
  17. package/skills/bricks-ctor/references/verification-toolchain.md +200 -0
  18. package/skills/bricks-design/SKILL.md +150 -45
  19. package/skills/bricks-design/references/architecture-truths.md +132 -0
  20. package/skills/bricks-design/references/avoiding-complexity.md +91 -0
  21. package/skills/bricks-design/references/design-critique.md +195 -0
  22. package/skills/bricks-design/references/design-languages.md +265 -0
  23. package/skills/bricks-design/references/performance.md +116 -0
  24. package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
  25. package/skills/bricks-design/references/translating-inputs.md +152 -0
  26. package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
  27. package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
  28. package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
  29. package/skills/bricks-design/references/workflow.md +134 -0
  30. package/skills/bricks-ux/SKILL.md +114 -0
  31. package/skills/bricks-ux/references/accessibility.md +162 -0
  32. package/skills/bricks-ux/references/flow-states.md +175 -0
  33. package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
  34. package/skills/bricks-ux/references/monitoring-screens.md +153 -0
  35. package/skills/bricks-ux/references/pressable-composition.md +126 -0
  36. package/skills/bricks-ux/references/user-journey.md +168 -0
  37. package/skills/bricks-ux/references/ux-critique.md +256 -0
  38. package/tools/__tests__/_cli-error.test.ts +35 -0
  39. package/tools/__tests__/_mcp-config.test.ts +67 -0
  40. package/tools/_cli-error.ts +17 -0
  41. package/tools/_edits-log.ts +41 -0
  42. package/tools/_git-author.ts +10 -2
  43. package/tools/_last-pushed-commit.ts +28 -0
  44. package/tools/_mcp-config.ts +42 -0
  45. package/tools/_shell.ts +8 -1
  46. package/tools/deploy.ts +17 -6
  47. package/tools/mcp-env.ts +13 -0
  48. package/tools/mcp-server.ts +8 -0
  49. package/tools/mcp-tools/__tests__/data-calc-editing.test.js +516 -0
  50. package/tools/mcp-tools/__tests__/entry-editing.test.js +866 -0
  51. package/tools/mcp-tools/__tests__/huggingface.test.ts +49 -0
  52. package/tools/mcp-tools/__tests__/icons.test.ts +21 -0
  53. package/tools/mcp-tools/__tests__/mcp-env.test.js +19 -0
  54. package/tools/mcp-tools/_editing-helpers.ts +58 -0
  55. package/tools/mcp-tools/_verify.ts +50 -0
  56. package/tools/mcp-tools/compile.ts +21 -9
  57. package/tools/mcp-tools/data-calc-editing.ts +1349 -0
  58. package/tools/mcp-tools/entry-editing.ts +2336 -0
  59. package/tools/mcp-tools/huggingface.ts +23 -13
  60. package/tools/mcp-tools/icons.ts +23 -7
  61. package/tools/mcp-tools/media.ts +4 -1
  62. package/tools/postinstall.ts +95 -38
  63. package/tools/pull.ts +93 -22
  64. package/tools/push-config.ts +114 -0
  65. package/tools/{preview-main.mjs → simulator-main.mjs} +207 -12
  66. package/tools/simulator-preload.cjs +16 -0
  67. package/tools/{preview.ts → simulator.ts} +4 -4
  68. package/types/{animation.ts → animation.d.ts} +24 -8
  69. package/types/{automation.ts → automation.d.ts} +16 -20
  70. package/types/{brick-base.ts → brick-base.d.ts} +1 -1
  71. package/types/bricks/{Camera.ts → Camera.d.ts} +8 -8
  72. package/types/bricks/{Chart.ts → Chart.d.ts} +4 -4
  73. package/types/bricks/{GenerativeMedia.ts → GenerativeMedia.d.ts} +15 -15
  74. package/types/bricks/{Icon.ts → Icon.d.ts} +7 -7
  75. package/types/bricks/{Image.ts → Image.d.ts} +21 -9
  76. package/types/bricks/{Items.ts → Items.d.ts} +7 -7
  77. package/types/bricks/{Lottie.ts → Lottie.d.ts} +10 -10
  78. package/types/bricks/{Maps.ts → Maps.d.ts} +11 -11
  79. package/types/bricks/{QrCode.ts → QrCode.d.ts} +7 -7
  80. package/types/bricks/{Rect.ts → Rect.d.ts} +7 -7
  81. package/types/bricks/{RichText.ts → RichText.d.ts} +12 -9
  82. package/types/bricks/{Rive.ts → Rive.d.ts} +9 -9
  83. package/types/bricks/Scene3D.d.ts +676 -0
  84. package/types/bricks/{Sketch.ts → Sketch.d.ts} +6 -6
  85. package/types/bricks/{Slideshow.ts → Slideshow.d.ts} +7 -7
  86. package/types/bricks/{Svg.ts → Svg.d.ts} +7 -7
  87. package/types/bricks/{Text.ts → Text.d.ts} +9 -9
  88. package/types/bricks/{TextInput.ts → TextInput.d.ts} +10 -10
  89. package/types/bricks/{Video.ts → Video.d.ts} +12 -12
  90. package/types/bricks/{VideoStreaming.ts → VideoStreaming.d.ts} +10 -10
  91. package/types/bricks/{WebRtcStream.ts → WebRtcStream.d.ts} +1 -1
  92. package/types/bricks/{WebView.ts → WebView.d.ts} +4 -4
  93. package/types/bricks/{index.ts → index.d.ts} +1 -0
  94. package/types/{common.ts → common.d.ts} +3 -6
  95. package/types/data-calc-command/base.d.ts +57 -0
  96. package/types/data-calc-command/collection.d.ts +418 -0
  97. package/types/data-calc-command/color.d.ts +432 -0
  98. package/types/data-calc-command/constant.d.ts +50 -0
  99. package/types/data-calc-command/datetime.d.ts +147 -0
  100. package/types/data-calc-command/file.d.ts +129 -0
  101. package/types/data-calc-command/index.d.ts +13 -0
  102. package/types/data-calc-command/iteratee.d.ts +23 -0
  103. package/types/data-calc-command/logictype.d.ts +190 -0
  104. package/types/data-calc-command/math.d.ts +275 -0
  105. package/types/data-calc-command/object.d.ts +119 -0
  106. package/types/data-calc-command/sandbox.d.ts +66 -0
  107. package/types/data-calc-command/string.d.ts +407 -0
  108. package/types/{data-calc.ts → data-calc.d.ts} +1 -0
  109. package/types/{data.ts → data.d.ts} +4 -2
  110. package/types/generators/{Assistant.ts → Assistant.d.ts} +19 -0
  111. package/types/generators/{LlmGgml.ts → LlmGgml.d.ts} +43 -1
  112. package/types/generators/{LlmMlx.ts → LlmMlx.d.ts} +1 -0
  113. package/types/generators/{RerankerGgml.ts → RerankerGgml.d.ts} +5 -1
  114. package/types/generators/{SoundRecorder.ts → SoundRecorder.d.ts} +10 -1
  115. package/types/generators/{SpeechToTextGgml.ts → SpeechToTextGgml.d.ts} +6 -1
  116. package/types/generators/{SttAppleBuiltin.ts → SttAppleBuiltin.d.ts} +27 -4
  117. package/types/generators/{ThermalPrinter.ts → ThermalPrinter.d.ts} +9 -7
  118. package/types/generators/{VadGgml.ts → VadGgml.d.ts} +12 -2
  119. package/types/{subspace.ts → subspace.d.ts} +1 -1
  120. package/utils/__tests__/calc.test.js +25 -0
  121. package/utils/__tests__/id.test.js +154 -0
  122. package/utils/calc.ts +5 -1
  123. package/utils/data.ts +5 -7
  124. package/utils/event-props.ts +17 -0
  125. package/utils/id.ts +109 -56
  126. package/skills/bricks-ctor/rules/buttress.md +0 -156
  127. package/skills/bricks-ctor/rules/data-calculation.md +0 -209
  128. package/skills/bricks-design/LICENSE.txt +0 -180
  129. package/types/data-calc-command.ts +0 -7005
  130. /package/skills/bricks-ctor/{rules → references}/local-sync.md +0 -0
  131. /package/skills/bricks-ctor/{rules → references}/media-flow.md +0 -0
  132. /package/skills/bricks-ctor/{rules → references}/remote-data-bank.md +0 -0
  133. /package/skills/bricks-ctor/{rules → references}/standby-transition.md +0 -0
  134. /package/types/{canvas.ts → canvas.d.ts} +0 -0
  135. /package/types/{data-calc-script.ts → data-calc-script.d.ts} +0 -0
  136. /package/types/generators/{AlarmClock.ts → AlarmClock.d.ts} +0 -0
  137. /package/types/generators/{BleCentral.ts → BleCentral.d.ts} +0 -0
  138. /package/types/generators/{BlePeripheral.ts → BlePeripheral.d.ts} +0 -0
  139. /package/types/generators/{CanvasMap.ts → CanvasMap.d.ts} +0 -0
  140. /package/types/generators/{CastlesPay.ts → CastlesPay.d.ts} +0 -0
  141. /package/types/generators/{DataBank.ts → DataBank.d.ts} +0 -0
  142. /package/types/generators/{File.ts → File.d.ts} +0 -0
  143. /package/types/generators/{GraphQl.ts → GraphQl.d.ts} +0 -0
  144. /package/types/generators/{Http.ts → Http.d.ts} +0 -0
  145. /package/types/generators/{HttpServer.ts → HttpServer.d.ts} +0 -0
  146. /package/types/generators/{Information.ts → Information.d.ts} +0 -0
  147. /package/types/generators/{Intent.ts → Intent.d.ts} +0 -0
  148. /package/types/generators/{Iterator.ts → Iterator.d.ts} +0 -0
  149. /package/types/generators/{Keyboard.ts → Keyboard.d.ts} +0 -0
  150. /package/types/generators/{LlmAnthropicCompat.ts → LlmAnthropicCompat.d.ts} +0 -0
  151. /package/types/generators/{LlmAppleBuiltin.ts → LlmAppleBuiltin.d.ts} +0 -0
  152. /package/types/generators/{LlmMediaTekNeuroPilot.ts → LlmMediaTekNeuroPilot.d.ts} +0 -0
  153. /package/types/generators/{LlmOnnx.ts → LlmOnnx.d.ts} +0 -0
  154. /package/types/generators/{LlmOpenAiCompat.ts → LlmOpenAiCompat.d.ts} +0 -0
  155. /package/types/generators/{LlmQualcommAiEngine.ts → LlmQualcommAiEngine.d.ts} +0 -0
  156. /package/types/generators/{Mcp.ts → Mcp.d.ts} +0 -0
  157. /package/types/generators/{McpServer.ts → McpServer.d.ts} +0 -0
  158. /package/types/generators/{MediaFlow.ts → MediaFlow.d.ts} +0 -0
  159. /package/types/generators/{MqttBroker.ts → MqttBroker.d.ts} +0 -0
  160. /package/types/generators/{MqttClient.ts → MqttClient.d.ts} +0 -0
  161. /package/types/generators/{Question.ts → Question.d.ts} +0 -0
  162. /package/types/generators/{RealtimeTranscription.ts → RealtimeTranscription.d.ts} +0 -0
  163. /package/types/generators/{SerialPort.ts → SerialPort.d.ts} +0 -0
  164. /package/types/generators/{SoundPlayer.ts → SoundPlayer.d.ts} +0 -0
  165. /package/types/generators/{SpeechToTextOnnx.ts → SpeechToTextOnnx.d.ts} +0 -0
  166. /package/types/generators/{SpeechToTextPlatform.ts → SpeechToTextPlatform.d.ts} +0 -0
  167. /package/types/generators/{SqLite.ts → SqLite.d.ts} +0 -0
  168. /package/types/generators/{Step.ts → Step.d.ts} +0 -0
  169. /package/types/generators/{Tcp.ts → Tcp.d.ts} +0 -0
  170. /package/types/generators/{TcpServer.ts → TcpServer.d.ts} +0 -0
  171. /package/types/generators/{TextToSpeechAppleBuiltin.ts → TextToSpeechAppleBuiltin.d.ts} +0 -0
  172. /package/types/generators/{TextToSpeechGgml.ts → TextToSpeechGgml.d.ts} +0 -0
  173. /package/types/generators/{TextToSpeechOnnx.ts → TextToSpeechOnnx.d.ts} +0 -0
  174. /package/types/generators/{TextToSpeechOpenAiLike.ts → TextToSpeechOpenAiLike.d.ts} +0 -0
  175. /package/types/generators/{Tick.ts → Tick.d.ts} +0 -0
  176. /package/types/generators/{Udp.ts → Udp.d.ts} +0 -0
  177. /package/types/generators/{VadOnnx.ts → VadOnnx.d.ts} +0 -0
  178. /package/types/generators/{VadTraditional.ts → VadTraditional.d.ts} +0 -0
  179. /package/types/generators/{VectorStore.ts → VectorStore.d.ts} +0 -0
  180. /package/types/generators/{Watchdog.ts → Watchdog.d.ts} +0 -0
  181. /package/types/generators/{WebCrawler.ts → WebCrawler.d.ts} +0 -0
  182. /package/types/generators/{WebRtc.ts → WebRtc.d.ts} +0 -0
  183. /package/types/generators/{WebSocket.ts → WebSocket.d.ts} +0 -0
  184. /package/types/generators/{index.ts → index.d.ts} +0 -0
  185. /package/types/{index.ts → index.d.ts} +0 -0
  186. /package/types/{switch.ts → switch.d.ts} +0 -0
  187. /package/types/{system.ts → system.d.ts} +0 -0
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 } : {}),
@@ -411,7 +584,7 @@ function compileRunArray(run: unknown[]): unknown[] {
411
584
  /**
412
585
  * Compile typed TestCase to raw format (strips __typename)
413
586
  */
414
- const compileTestCase = (testCase: TestCase) => ({
587
+ export const compileTestCase = (testCase: TestCase) => ({
415
588
  id: testCase.id,
416
589
  name: testCase.name,
417
590
  hide_short_ref: testCase.hideShortRef,
@@ -433,7 +606,10 @@ const compileTestCase = (testCase: TestCase) => ({
433
606
  variable: cond.variable,
434
607
  operator: cond.operator,
435
608
  value: cond.value,
436
- jump_to: cond.jump_to,
609
+ // `jump_to` may be a getter (() => TestCase) for dynamic case ids (the project generator
610
+ // emits this form). Resolve it to its id like the `run` array does — otherwise the function
611
+ // is JSON-serialized to nothing and the conditional jump silently vanishes from the config.
612
+ jump_to: compileRunElement(cond.jump_to),
437
613
  }
438
614
  }),
439
615
  })
@@ -485,6 +661,7 @@ const compileAutomationTest = (
485
661
 
486
662
  return {
487
663
  id: testId,
664
+ alias: test.alias,
488
665
  title: test.title,
489
666
  hide_short_ref: test.hideShortRef,
490
667
  timeout: test.timeout,
@@ -538,8 +715,38 @@ const compileAutomation = (automationMap: AutomationMap) =>
538
715
  }),
539
716
  )
540
717
 
718
+ // Record the minimal compiled-config delta this compile produced to the shared audit
719
+ // log (`.bricks/edits.jsonl`), so editing files directly and running `bun compile`
720
+ // leaves the same trail as the MCP source-editing tools. Maintained only in the
721
+ // editing-tools context (`BRICKS_CTOR_ENABLE_EDITING_TOOLS`); the source-editing tools
722
+ // turn it off for their verify compiles (see _verify.ts) so a tool edit records one
723
+ // richer entry instead of an extra generic compile entry. Also silent when there is no
724
+ // prior build to diff against (fresh projects, package tests, tooling outside a project).
725
+ const recordConfigChange = async (previousConfig: unknown, config: unknown) => {
726
+ if (previousConfig == null) return
727
+ if (!isTruthyEnv(process.env.BRICKS_CTOR_ENABLE_EDITING_TOOLS)) return
728
+ // The baseline was parsed from JSON; `computeConfigChange` applies the same
729
+ // JSON-omitted-field rules lazily so compile avoids cloning the full config.
730
+ const change = computeConfigChange(previousConfig, config)
731
+ if (change.status !== 'ok') return
732
+ await appendEditRecord(process.cwd(), {
733
+ ts: new Date().toISOString(),
734
+ tool: 'compile',
735
+ provenance: editProvenance(),
736
+ outcome: 'ok',
737
+ summary:
738
+ change.opCount === 0
739
+ ? 'compile: no config change'
740
+ : `compile: ${change.opCount} config op(s)`,
741
+ configChange: change,
742
+ }).catch(() => undefined)
743
+ }
744
+
541
745
  export const compile = async (app: Application) => {
542
746
  await new Promise((resolve) => setImmediate(resolve, 0))
747
+ // Snapshot the prior build artifact before the caller's compile.ts overwrites it, so
748
+ // the config change introduced by this compile can be recorded on return.
749
+ const previousConfig = await readBuildConfig(process.cwd())
543
750
  const timestamp = Date.now()
544
751
  // Pre-index subspace ids so the canvas-item validation below stays O(1).
545
752
  const subspaceIdSet = new Set(app.subspaces.map((s) => s.id))
@@ -559,7 +766,14 @@ export const compile = async (app: Application) => {
559
766
  // validation (root_canvas_id is required before the conditional
560
767
  // schema fix is published).
561
768
  if (subspace.module?.link) {
562
- const placeholderCanvasId = makeId('canvas')
769
+ // Seed the placeholder id from the (stable) subspace id. `makeId('canvas')` would take
770
+ // the count-fallback branch (a process-global counter that is never reset), so the
771
+ // placeholder id depended on how many prior count-fallback ids had been minted — making
772
+ // it differ between recompiles and breaking compile's byte-stable-output contract
773
+ // (phantom config-change ops). `makeSeededId` keeps no global state, so identical source
774
+ // recompiles to an identical id. (`makeId('canvas', alias)` would instead throw
775
+ // "Duplicate makeId alias" on the second compile in a long-lived process.)
776
+ const placeholderCanvasId = makeSeededId('canvas', `${subspaceId}:module-placeholder`)
563
777
  subspaceMap[subspaceId] = {
564
778
  title: subspace.title,
565
779
  description: subspace.description,
@@ -629,39 +843,63 @@ export const compile = async (app: Application) => {
629
843
  `(animation index: ${animationIndex}, subspace: ${subspaceId})`,
630
844
  )
631
845
 
632
- if (animation.__typename === 'Animation') {
846
+ const animationTypename = animation.__typename
847
+ if (animationTypename === 'Animation') {
633
848
  const animationDef = animation as AnimationDef
849
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
850
+ const animationWarningMetadata = {
851
+ animationIndex,
852
+ animationTitle: animationDef.title,
853
+ animationAlias: animationDef.alias,
854
+ animationProperty: animationDef.property,
855
+ subspaceIndex,
856
+ subspaceTitle: subspace.title,
857
+ }
858
+ const animationType = getAnimationType(animationDef.config, animationErrorReference)
634
859
  map[animationId] = {
635
860
  alias: animationDef.alias,
636
861
  title: animationDef.title,
637
862
  description: animationDef.description,
638
863
  hide_short_ref: animationDef.hideShortRef,
639
864
  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})`,
865
+ property: assertAnimationProperty(animationDef.property, animationErrorReference),
866
+ type: animationType,
867
+ config: compileAnimationConfig(
868
+ animationType,
869
+ animationDef.config,
870
+ animationErrorReference,
871
+ animationWarningMetadata,
645
872
  ),
646
873
  }
647
- } else if (animation.__typename === 'AnimationCompose') {
874
+ } else if (animationTypename === 'AnimationCompose') {
648
875
  const animationDef = animation as AnimationComposeDef
876
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
649
877
  map[animationId] = {
650
878
  alias: animationDef.alias,
651
879
  title: animationDef.title,
652
880
  description: animationDef.description,
653
881
  hide_short_ref: animationDef.hideShortRef,
654
882
  animationRunType: animationDef.runType,
655
- compose_type: animationDef.composeType,
883
+ compose_type: assertAnimationComposeType(
884
+ animationDef.composeType,
885
+ animationErrorReference,
886
+ ),
656
887
  item_list: animationDef.items.map((item, index) => {
657
888
  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 }
889
+ const innerAnimationId = assertEntryId(
890
+ innerAnimation?.id,
891
+ 'ANIMATION',
892
+ `(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
893
+ )
894
+ return { animation_id: innerAnimationId }
663
895
  }),
664
896
  }
897
+ } else {
898
+ throw new Error(
899
+ `Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
900
+ animationTypename,
901
+ )}`,
902
+ )
665
903
  }
666
904
  return map
667
905
  }, {}),
@@ -1009,7 +1247,7 @@ export const compile = async (app: Application) => {
1009
1247
  ...compileRemoteUpdate(data.remoteUpdate),
1010
1248
  routing: data.routing,
1011
1249
  schema: data.schema,
1012
- type: data.type,
1250
+ type: data.type === 'boolean' ? 'bool' : data.type,
1013
1251
  ...compileKind(data.kind),
1014
1252
  value: compileProperty(data.value, `(data: ${dataId}, subspace ${subspaceId})`),
1015
1253
  event_map: compileEvents('PROPERTY_BANK', data.events || {}, {
@@ -1029,6 +1267,7 @@ export const compile = async (app: Application) => {
1029
1267
  )
1030
1268
 
1031
1269
  const calc: any = {
1270
+ alias: dataCalc.alias,
1032
1271
  title: dataCalc.title,
1033
1272
  description: dataCalc.description,
1034
1273
  hide_short_ref: dataCalc.hideShortRef,
@@ -1227,12 +1466,7 @@ export const compile = async (app: Application) => {
1227
1466
  : null,
1228
1467
  }
1229
1468
 
1230
- Object.assign(
1231
- calc,
1232
- generateCalulationMap(calc.script_config, {
1233
- snapshotMode: process.env.BRICKS_SNAPSHOT_MODE === '1',
1234
- }),
1235
- )
1469
+ Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
1236
1470
  }
1237
1471
  map[dataCalcId] = calc
1238
1472
  return map
@@ -1269,10 +1503,20 @@ export const compile = async (app: Application) => {
1269
1503
  automation_map: compiledAutomationMap || app.metadata?.TEMP_automation_map || {},
1270
1504
  update_timestamp: timestamp,
1271
1505
  }
1506
+ await recordConfigChange(previousConfig, config)
1272
1507
  return config
1273
1508
  }
1274
1509
 
1275
1510
  export const checkConfig = async (configPath: string) => {
1276
- const { sh } = await import('../tools/_shell')
1277
- await sh`bricks app check-config ${configPath}`
1511
+ // --validate-automation surfaces broken automation_map / test_map refs early,
1512
+ // which catches agent-authored automations that reference deleted bricks.
1513
+ await sh`bricks app check-config --validate-automation ${configPath}`
1514
+ // Doctor adds semantic lint checks after structural validation. Warnings are
1515
+ // surfaced in the compile log, but only errors fail by default. Older published
1516
+ // bricks-cli builds lack `app doctor` — skip rather than fail the compile.
1517
+ const doctor = await sh`bricks app doctor --validate-automation ${configPath}`.nothrow()
1518
+ if (doctor.exitCode !== 0) {
1519
+ if (/unknown command/i.test(doctor.stderr?.toString() ?? '')) return
1520
+ throw new Error(`bricks app doctor failed with exit ${doctor.exitCode}`)
1521
+ }
1278
1522
  }
package/compile/util.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { makeId } from '../utils/id'
1
+ import { makeSeededId } from '../utils/id'
2
2
 
3
3
  type ScriptConfig = {
4
4
  title?: string
@@ -18,30 +18,54 @@ const errorMsg = 'Not allow duplicate set property id between inputs / outputs /
18
18
  export const validateConfig = (config: ScriptConfig) => {
19
19
  // Skip input/output overlap validation in manual mode (allows same data node in both)
20
20
  if (config.trigger_mode === 'manual') return
21
- if (config.error && config.inputs[config.error]) {
21
+ // `inputs` is keyed by data-node id with the OBJECT_SET path as the *value*. The overlap
22
+ // check is "is this id also an input?" — a key-existence question — so test key presence,
23
+ // not the path's truthiness. An empty path ('' = OBJECT_SET's default "set whole object")
24
+ // is a legitimate input value that would otherwise slip the overlap guard.
25
+ const isInput = (id: string) => Object.prototype.hasOwnProperty.call(config.inputs, id)
26
+ if (config.error && isInput(config.error)) {
22
27
  throw new Error(`${errorMsg}. key: error`)
23
28
  }
24
- if (config.output && config.inputs[config.output]) {
29
+ if (config.output && isInput(config.output)) {
25
30
  throw new Error(`${errorMsg}. key: output`)
26
31
  }
27
- if (Object.values(config.outputs).some((value) => value.some((id) => config.inputs[id]))) {
32
+ if (Object.values(config.outputs).some((value) => value.some(isInput))) {
28
33
  throw new Error(`${errorMsg}. key: outputs`)
29
34
  }
35
+ // The same data-node id reused across the output-side targets (output / error / outputs)
36
+ // also collides: generateCalulationMap spreads their node objects last-wins, so the later
37
+ // one silently overwrites the earlier wiring (e.g. error == output drops the error change
38
+ // link). The checks above only compare against `inputs`, so guard the output-side pairs
39
+ // too. (The same id appearing in multiple `outputs` entries is a supported last-wins case
40
+ // and stays allowed.)
41
+ if (config.error && config.output && config.error === config.output) {
42
+ throw new Error(`${errorMsg}. key: error/output`)
43
+ }
44
+ const outputsIds = Object.values(config.outputs).flat()
45
+ if (config.output && outputsIds.includes(config.output)) {
46
+ throw new Error(`${errorMsg}. key: output/outputs`)
47
+ }
48
+ if (config.error && outputsIds.includes(config.error)) {
49
+ throw new Error(`${errorMsg}. key: error/outputs`)
50
+ }
30
51
  }
31
52
 
32
53
  const padding = 15
33
54
  const layerXInterval = 300
34
55
  const layerYInterval = 150
35
56
 
36
- export const generateCalulationMap = (config: ScriptConfig, opts?: { snapshotMode?: boolean }) => {
57
+ // `calcId` (the owning script calc's stable id) seeds every derived command-node id, so an
58
+ // unchanged calc recompiles to identical ids and editing one calc never shifts another's —
59
+ // keeping the compiled config byte-stable for change detection. See makeSeededId in utils/id.
60
+ export const generateCalulationMap = (config: ScriptConfig, calcId: string) => {
37
61
  validateConfig(config)
38
- const sandboxId = makeId('property_bank_command', opts)
39
- const sandboxErrorId = makeId('property_bank_command', opts)
40
- const sandboxResultId = makeId('property_bank_command', opts)
62
+ const sandboxId = makeSeededId('property_bank_command', `${calcId}:sandbox-run`)
63
+ const sandboxErrorId = makeSeededId('property_bank_command', `${calcId}:sandbox-error`)
64
+ const sandboxResultId = makeSeededId('property_bank_command', `${calcId}:sandbox-result`)
41
65
 
42
66
  const inputs = Object.entries(config.inputs).reduce(
43
67
  (acc, [key, value], index) => {
44
- const commandId = makeId('property_bank_command', opts)
68
+ const commandId = makeSeededId('property_bank_command', `${calcId}:input:${key}`)
45
69
  acc.map[key] = {
46
70
  type: 'data-node',
47
71
  properties: {},
@@ -123,7 +147,7 @@ export const generateCalulationMap = (config: ScriptConfig, opts?: { snapshotMod
123
147
  let y = 0
124
148
  const outputs = Object.entries(config.outputs).reduce(
125
149
  (acc, [key, pbList], index) => {
126
- const commandId = makeId('property_bank_command', opts)
150
+ const commandId = makeSeededId('property_bank_command', `${calcId}:output:${key}`)
127
151
  acc.commandIdList.push(commandId)
128
152
  acc.map[commandId] = {
129
153
  type: 'command-node-object',
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.25.0-beta.5",
3
+ "version": "2.25.0-beta.51",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
7
7
  "build": "bun scripts/build.js"
8
8
  },
9
9
  "dependencies": {
10
- "@fugood/bricks-cli": "^2.25.0-beta.5",
10
+ "@babel/generator": "7.28.5",
11
+ "@babel/parser": "7.28.5",
12
+ "@babel/traverse": "7.28.5",
13
+ "@babel/types": "7.28.5",
14
+ "@fugood/bricks-cli": "^2.25.0-beta.51",
11
15
  "@huggingface/gguf": "^0.3.2",
12
16
  "@iarna/toml": "^3.0.0",
13
17
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -25,5 +29,5 @@
25
29
  "peerDependencies": {
26
30
  "oxfmt": "^0.36.0"
27
31
  },
28
- "gitHead": "ff0abf1f06694c6d71a1a14ac52c37f9a72d1ac3"
32
+ "gitHead": "9a6ddecabc4a2e11fc6ae9a256de730cd3d744ca"
29
33
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bricks-ctor
3
- description: Advanced BRICKS configuration knowledge. Covers Standby Transition, Automations (E2E testing), Data Calculation (JS sandbox libraries), Local Sync, Remote Data Bank, Media Flow, and Buttress (remote inference). Triggers on multi-device sync, cloud data, media assets, AI offloading, E2E testing, or canvas transitions.
3
+ description: Advanced BRICKS configuration knowledge. Covers Standby Transition, Automations (E2E testing), Data Calculation (JS sandbox libraries), Local Sync, Remote Data Bank, Media Flow, Buttress (remote inference), and the verification toolchain (compile, harness-specific preview tool, on-device DevTools, definition-of-done gate). Triggers on multi-device sync, cloud data, media assets, AI offloading, E2E testing, canvas transitions, or verifying project work before declaring done.
4
4
  ---
5
5
 
6
6
  # BRICKS Ctor - Advanced Features
@@ -11,22 +11,28 @@ This skill covers advanced BRICKS features not in the main project instructions.
11
11
 
12
12
  | Rule | Description |
13
13
  |------|-------------|
14
- | [Architecture Patterns](rules/architecture-patterns.md) | **Read first** — decompose flows and select patterns |
15
- | [Animation](rules/animation.md) | Animation system for brick transforms and opacity |
16
- | [Standby Transition](rules/standby-transition.md) | Canvas enter/exit animations |
17
- | [Automations](rules/automations.md) | E2E testing and scheduled tasks |
18
- | [Data Calculation](rules/data-calculation.md) | JS sandbox libraries (25+ available) |
19
- | [Local Sync](rules/local-sync.md) | LAN device synchronization |
20
- | [Remote Data Bank](rules/remote-data-bank.md) | Cloud data sync and API access |
21
- | [Media Flow](rules/media-flow.md) | Media asset management |
22
- | [Buttress](rules/buttress.md) | Remote inference for AI generators |
14
+ | [Architecture Patterns](references/architecture-patterns.md) | **Read first** — decompose flows and select patterns |
15
+ | [Source-Editing Tools](references/source-editing-tools.md) | MCP tools for editing entries and data-calcs by AST (new/edit/remove, value grammar, verify) |
16
+ | [Animation](references/animation.md) | Animation system for brick transforms and opacity |
17
+ | [Standby Transition](references/standby-transition.md) | Canvas enter/exit animations |
18
+ | [Automations](references/automations.md) | E2E testing and scheduled tasks |
19
+ | [Data Calculation](references/data-calculation.md) | Wiring contract, trigger semantics, JS sandbox (25+ libraries) |
20
+ | [Local Sync](references/local-sync.md) | LAN device synchronization |
21
+ | [Remote Data Bank](references/remote-data-bank.md) | Cloud data sync and API access |
22
+ | [Media Flow](references/media-flow.md) | Media asset management |
23
+ | [Buttress](references/buttress.md) | Remote inference for AI generators |
24
+ | [Verification Toolchain](references/verification-toolchain.md) | Definition of done, compile, preview tool selection, on-device DevTools, Path 1/2/3 decision rule |
25
+ | [Simulator](references/simulator.md) | Path 1 fidelity — Simulator feature support, fallbacks (Camera/LLM/STT/Vector Store, Buttress disabled), when a green run is enough vs. Path 2 |
23
26
 
24
27
  ## Quick Reference
25
28
 
26
- - **Complex flows**: See [Architecture Patterns](rules/architecture-patterns.md) for decomposing multi-step workflows
27
- - **Multi-device**: See [Local Sync](rules/local-sync.md) for LAN coordination
28
- - **Cloud data**: See [Remote Data Bank](rules/remote-data-bank.md) for sync and API access
29
- - **Media assets**: See [Media Flow](rules/media-flow.md) for centralized asset management
30
- - **AI offloading**: See [Buttress](rules/buttress.md) for GPU server delegation
31
- - **E2E testing**: See [Automations](rules/automations.md) for test automation
32
- - **Enter animations**: See [Standby Transition](rules/standby-transition.md) for canvas transitions
29
+ - **Complex flows**: See [Architecture Patterns](references/architecture-patterns.md) for decomposing multi-step workflows
30
+ - **Editing entries/data-calcs**: See [Source-Editing Tools](references/source-editing-tools.md) prefer the MCP editing tools over hand-editing `subspaces/**`
31
+ - **Multi-device**: See [Local Sync](references/local-sync.md) for LAN coordination
32
+ - **Cloud data**: See [Remote Data Bank](references/remote-data-bank.md) for sync and API access
33
+ - **Media assets**: See [Media Flow](references/media-flow.md) for centralized asset management
34
+ - **AI offloading**: See [Buttress](references/buttress.md) for GPU server delegation
35
+ - **E2E testing**: See [Automations](references/automations.md) for test automation
36
+ - **Enter animations**: See [Standby Transition](references/standby-transition.md) for canvas transitions
37
+ - **Verification before done**: See [Verification Toolchain](references/verification-toolchain.md) for the definition-of-done gate and Path 1/2/3 decision rule
38
+ - **Simulator fidelity**: See [Simulator](references/simulator.md) for what the Simulator fakes (Camera, AI generators, Buttress) and when to escalate to a real device
@@ -41,12 +41,13 @@ const bounce: AnimationDef = {
41
41
  toValue: 1,
42
42
  friction: 7,
43
43
  tension: 40,
44
- speed: 12,
45
- bounciness: 8,
46
44
  },
47
45
  }
48
46
  ```
49
47
 
48
+ Use one spring parameter family per config: `tension/friction`, `speed/bounciness`, or
49
+ `stiffness/damping/mass`.
50
+
50
51
  ### Decay Animation
51
52
  Velocity-based deceleration animation.
52
53