@fugood/bricks-project 2.24.2 → 2.24.4
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.
- package/compile/index.ts +195 -13
- package/package.json +2 -2
- package/package.json.bak +2 -2
- package/skills/bricks-ctor/SKILL.md +3 -1
- package/skills/bricks-ctor/rules/animation.md +3 -2
- package/skills/bricks-ctor/rules/buttress.md +97 -8
- package/skills/bricks-ctor/rules/verification-toolchain.md +170 -0
- package/skills/bricks-design/SKILL.md +160 -45
- package/skills/bricks-design/references/architecture-truths.md +125 -0
- package/skills/bricks-design/references/avoiding-complexity.md +91 -0
- package/skills/bricks-design/references/design-critique.md +195 -0
- package/skills/bricks-design/references/design-languages.md +265 -0
- package/skills/bricks-design/references/performance.md +116 -0
- package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
- package/skills/bricks-design/references/translating-inputs.md +152 -0
- package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
- package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
- package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
- package/skills/bricks-design/references/workflow.md +134 -0
- package/skills/bricks-ux/SKILL.md +120 -0
- package/skills/bricks-ux/references/accessibility.md +162 -0
- package/skills/bricks-ux/references/flow-states.md +175 -0
- package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
- package/skills/bricks-ux/references/monitoring-screens.md +153 -0
- package/skills/bricks-ux/references/pressable-composition.md +126 -0
- package/skills/bricks-ux/references/user-journey.md +168 -0
- package/skills/bricks-ux/references/ux-critique.md +256 -0
- package/tools/_git-author.ts +10 -2
- package/tools/_last-pushed-commit.ts +28 -0
- package/tools/deploy.ts +15 -0
- package/tools/preview-main.mjs +18 -5
- package/tools/pull.ts +91 -16
- package/tools/update-config.ts +118 -0
- package/types/animation.ts +16 -5
- package/types/automation.ts +1 -0
- package/types/bricks/Image.ts +12 -0
- package/types/data-calc.ts +1 -0
- package/types/data.ts +1 -1
- package/types/generators/Assistant.ts +18 -0
- package/types/generators/LlmGgml.ts +1 -0
- package/types/generators/LlmMlx.ts +1 -0
- package/types/generators/SpeechToTextGgml.ts +1 -0
- package/types/subspace.ts +1 -1
- package/utils/data.ts +1 -1
- package/skills/bricks-design/LICENSE.txt +0 -180
package/compile/index.ts
CHANGED
|
@@ -301,6 +301,162 @@ const animationTypeMap = {
|
|
|
301
301
|
AnimationTimingConfig: 'timing',
|
|
302
302
|
AnimationSpringConfig: 'spring',
|
|
303
303
|
AnimationDecayConfig: 'decay',
|
|
304
|
+
} as const
|
|
305
|
+
|
|
306
|
+
type CompiledAnimationType = (typeof animationTypeMap)[keyof typeof animationTypeMap]
|
|
307
|
+
type WarningMetadata = Record<string, unknown>
|
|
308
|
+
|
|
309
|
+
const animationProperties = new Set([
|
|
310
|
+
'transform.translateX',
|
|
311
|
+
'transform.translateY',
|
|
312
|
+
'transform.scale',
|
|
313
|
+
'transform.scaleX',
|
|
314
|
+
'transform.scaleY',
|
|
315
|
+
'transform.rotate',
|
|
316
|
+
'transform.rotateX',
|
|
317
|
+
'transform.rotateY',
|
|
318
|
+
'opacity',
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
const animationComposeTypes = new Set(['parallel', 'sequence'])
|
|
322
|
+
const springConfigFamilies = [
|
|
323
|
+
['stiffness', 'damping', 'mass'],
|
|
324
|
+
['tension', 'friction'],
|
|
325
|
+
['bounciness', 'speed'],
|
|
326
|
+
]
|
|
327
|
+
const springConfigFamilyKeys = new Set(springConfigFamilies.flat())
|
|
328
|
+
|
|
329
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
330
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
331
|
+
|
|
332
|
+
const hasDefinedConfigValue = (config: Record<string, unknown>, key: string) =>
|
|
333
|
+
config[key] !== undefined
|
|
334
|
+
|
|
335
|
+
const assertConfigValue = (
|
|
336
|
+
config: Record<string, unknown>,
|
|
337
|
+
key: string,
|
|
338
|
+
errorReference: string,
|
|
339
|
+
) => {
|
|
340
|
+
if (!hasDefinedConfigValue(config, key)) {
|
|
341
|
+
throw new Error(`Invalid animation config ${errorReference}: missing "${key}"`)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const assertAnimationProperty = (property: unknown, errorReference: string) => {
|
|
346
|
+
if (typeof property !== 'string' || !animationProperties.has(property)) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Invalid animation property${errorReference ? ` ${errorReference}` : ''}: ${String(
|
|
349
|
+
property,
|
|
350
|
+
)}`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return property
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const getAnimationType = (config: unknown, errorReference: string): CompiledAnimationType => {
|
|
357
|
+
if (!isRecord(config)) {
|
|
358
|
+
throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const animationType = animationTypeMap[config.__type as keyof typeof animationTypeMap]
|
|
362
|
+
if (!animationType) {
|
|
363
|
+
throw new Error(`Invalid animation config type ${errorReference}: ${String(config.__type)}`)
|
|
364
|
+
}
|
|
365
|
+
return animationType
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const assertAnimationComposeType = (composeType: unknown, errorReference: string) => {
|
|
369
|
+
if (typeof composeType !== 'string' || !animationComposeTypes.has(composeType)) {
|
|
370
|
+
throw new Error(`Invalid animation compose type ${errorReference}: ${String(composeType)}`)
|
|
371
|
+
}
|
|
372
|
+
return composeType
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const pickDefinedConfigValues = (config: Record<string, unknown>, keys: string[]) =>
|
|
376
|
+
keys.reduce((acc, key) => {
|
|
377
|
+
if (hasDefinedConfigValue(config, key)) acc[key] = config[key]
|
|
378
|
+
return acc
|
|
379
|
+
}, {})
|
|
380
|
+
|
|
381
|
+
const getDefinedConfigKeys = (config: Record<string, unknown>, keys: string[]) =>
|
|
382
|
+
keys.filter((key) => hasDefinedConfigValue(config, key))
|
|
383
|
+
|
|
384
|
+
const formatWarningMetadata = (metadata: WarningMetadata = {}) =>
|
|
385
|
+
Object.entries(metadata)
|
|
386
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
387
|
+
.map(([key, value]) => `${key}: ${String(value)}`)
|
|
388
|
+
.join(', ')
|
|
389
|
+
|
|
390
|
+
const formatWarningReference = (metadata?: WarningMetadata) => {
|
|
391
|
+
const metadataText = formatWarningMetadata(metadata)
|
|
392
|
+
return metadataText ? ` [${metadataText}]` : ''
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const normalizeSpringConfig = (
|
|
396
|
+
config: Record<string, unknown>,
|
|
397
|
+
errorReference: string,
|
|
398
|
+
warningMetadata?: WarningMetadata,
|
|
399
|
+
): Record<string, unknown> => {
|
|
400
|
+
assertConfigValue(config, 'toValue', errorReference)
|
|
401
|
+
|
|
402
|
+
const usedFamilies = springConfigFamilies.filter((keys) =>
|
|
403
|
+
keys.some((key) => hasDefinedConfigValue(config, key)),
|
|
404
|
+
)
|
|
405
|
+
if (usedFamilies.length <= 1) return config
|
|
406
|
+
|
|
407
|
+
const configWithoutSpringFamily = Object.entries(config).reduce((acc, [key, value]) => {
|
|
408
|
+
if (!springConfigFamilyKeys.has(key)) acc[key] = value
|
|
409
|
+
return acc
|
|
410
|
+
}, {})
|
|
411
|
+
|
|
412
|
+
// Match runtime normalization: physical spring values are most explicit,
|
|
413
|
+
// otherwise preserve BRICKS' historical tension/friction controls.
|
|
414
|
+
const resolvedFamily =
|
|
415
|
+
usedFamilies.find((keys) => keys.includes('stiffness')) ||
|
|
416
|
+
usedFamilies.find((keys) => keys.includes('tension')) ||
|
|
417
|
+
usedFamilies[0]
|
|
418
|
+
const resolvedFamilyKeys = getDefinedConfigKeys(config, resolvedFamily)
|
|
419
|
+
const droppedFamilyKeys = usedFamilies
|
|
420
|
+
.filter((keys) => keys !== resolvedFamily)
|
|
421
|
+
.flatMap((keys) => getDefinedConfigKeys(config, keys))
|
|
422
|
+
|
|
423
|
+
console.warn(
|
|
424
|
+
`[Warning] Resolved animation spring config${formatWarningReference(
|
|
425
|
+
warningMetadata,
|
|
426
|
+
)}: using ${resolvedFamilyKeys.join('/')}, dropping ${droppedFamilyKeys.join('/')}`,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
...configWithoutSpringFamily,
|
|
431
|
+
...pickDefinedConfigValues(config, resolvedFamily),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const compileAnimationConfig = (
|
|
436
|
+
animationType: CompiledAnimationType,
|
|
437
|
+
config: unknown,
|
|
438
|
+
errorReference: string,
|
|
439
|
+
warningMetadata?: WarningMetadata,
|
|
440
|
+
) => {
|
|
441
|
+
if (!isRecord(config)) {
|
|
442
|
+
throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const compiledConfig = compileProperty(omit(config, '__type'), errorReference)
|
|
446
|
+
|
|
447
|
+
if (!isRecord(compiledConfig)) {
|
|
448
|
+
throw new Error(`Invalid animation config ${errorReference}: config must compile to an object`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (animationType === 'timing') {
|
|
452
|
+
assertConfigValue(compiledConfig, 'toValue', errorReference)
|
|
453
|
+
} else if (animationType === 'spring') {
|
|
454
|
+
return normalizeSpringConfig(compiledConfig, errorReference, warningMetadata)
|
|
455
|
+
} else if (animationType === 'decay') {
|
|
456
|
+
assertConfigValue(compiledConfig, 'velocity', errorReference)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return compiledConfig
|
|
304
460
|
}
|
|
305
461
|
|
|
306
462
|
const compileFrame = (frame: Canvas['items'][number]['frame']) => ({
|
|
@@ -485,6 +641,7 @@ const compileAutomationTest = (
|
|
|
485
641
|
|
|
486
642
|
return {
|
|
487
643
|
id: testId,
|
|
644
|
+
alias: test.alias,
|
|
488
645
|
title: test.title,
|
|
489
646
|
hide_short_ref: test.hideShortRef,
|
|
490
647
|
timeout: test.timeout,
|
|
@@ -629,39 +786,63 @@ export const compile = async (app: Application) => {
|
|
|
629
786
|
`(animation index: ${animationIndex}, subspace: ${subspaceId})`,
|
|
630
787
|
)
|
|
631
788
|
|
|
632
|
-
|
|
789
|
+
const animationTypename = animation.__typename
|
|
790
|
+
if (animationTypename === 'Animation') {
|
|
633
791
|
const animationDef = animation as AnimationDef
|
|
792
|
+
const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
|
|
793
|
+
const animationWarningMetadata = {
|
|
794
|
+
animationIndex,
|
|
795
|
+
animationTitle: animationDef.title,
|
|
796
|
+
animationAlias: animationDef.alias,
|
|
797
|
+
animationProperty: animationDef.property,
|
|
798
|
+
subspaceIndex,
|
|
799
|
+
subspaceTitle: subspace.title,
|
|
800
|
+
}
|
|
801
|
+
const animationType = getAnimationType(animationDef.config, animationErrorReference)
|
|
634
802
|
map[animationId] = {
|
|
635
803
|
alias: animationDef.alias,
|
|
636
804
|
title: animationDef.title,
|
|
637
805
|
description: animationDef.description,
|
|
638
806
|
hide_short_ref: animationDef.hideShortRef,
|
|
639
807
|
animationRunType: animationDef.runType,
|
|
640
|
-
property: animationDef.property,
|
|
641
|
-
type:
|
|
642
|
-
config:
|
|
643
|
-
|
|
644
|
-
|
|
808
|
+
property: assertAnimationProperty(animationDef.property, animationErrorReference),
|
|
809
|
+
type: animationType,
|
|
810
|
+
config: compileAnimationConfig(
|
|
811
|
+
animationType,
|
|
812
|
+
animationDef.config,
|
|
813
|
+
animationErrorReference,
|
|
814
|
+
animationWarningMetadata,
|
|
645
815
|
),
|
|
646
816
|
}
|
|
647
|
-
} else if (
|
|
817
|
+
} else if (animationTypename === 'AnimationCompose') {
|
|
648
818
|
const animationDef = animation as AnimationComposeDef
|
|
819
|
+
const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
|
|
649
820
|
map[animationId] = {
|
|
650
821
|
alias: animationDef.alias,
|
|
651
822
|
title: animationDef.title,
|
|
652
823
|
description: animationDef.description,
|
|
653
824
|
hide_short_ref: animationDef.hideShortRef,
|
|
654
825
|
animationRunType: animationDef.runType,
|
|
655
|
-
compose_type:
|
|
826
|
+
compose_type: assertAnimationComposeType(
|
|
827
|
+
animationDef.composeType,
|
|
828
|
+
animationErrorReference,
|
|
829
|
+
),
|
|
656
830
|
item_list: animationDef.items.map((item, index) => {
|
|
657
831
|
const innerAnimation = item()
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
)
|
|
662
|
-
|
|
832
|
+
const innerAnimationId = assertEntryId(
|
|
833
|
+
innerAnimation?.id,
|
|
834
|
+
'ANIMATION',
|
|
835
|
+
`(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
|
|
836
|
+
)
|
|
837
|
+
return { animation_id: innerAnimationId }
|
|
663
838
|
}),
|
|
664
839
|
}
|
|
840
|
+
} else {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
|
|
843
|
+
animationTypename,
|
|
844
|
+
)}`,
|
|
845
|
+
)
|
|
665
846
|
}
|
|
666
847
|
return map
|
|
667
848
|
}, {}),
|
|
@@ -1029,6 +1210,7 @@ export const compile = async (app: Application) => {
|
|
|
1029
1210
|
)
|
|
1030
1211
|
|
|
1031
1212
|
const calc: any = {
|
|
1213
|
+
alias: dataCalc.alias,
|
|
1032
1214
|
title: dataCalc.title,
|
|
1033
1215
|
description: dataCalc.description,
|
|
1034
1216
|
hide_short_ref: dataCalc.hideShortRef,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-project",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.4",
|
|
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.24.
|
|
10
|
+
"@fugood/bricks-cli": "^2.24.4",
|
|
11
11
|
"@huggingface/gguf": "^0.3.2",
|
|
12
12
|
"@iarna/toml": "^3.0.0",
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
package/package.json.bak
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.4",
|
|
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.24.
|
|
10
|
+
"@fugood/bricks-cli": "^2.24.4",
|
|
11
11
|
"@huggingface/gguf": "^0.3.2",
|
|
12
12
|
"@iarna/toml": "^3.0.0",
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -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,
|
|
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/preview MCP, 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
|
|
@@ -20,6 +20,7 @@ This skill covers advanced BRICKS features not in the main project instructions.
|
|
|
20
20
|
| [Remote Data Bank](rules/remote-data-bank.md) | Cloud data sync and API access |
|
|
21
21
|
| [Media Flow](rules/media-flow.md) | Media asset management |
|
|
22
22
|
| [Buttress](rules/buttress.md) | Remote inference for AI generators |
|
|
23
|
+
| [Verification Toolchain](rules/verification-toolchain.md) | Definition of done, compile/preview MCP, on-device DevTools, Path 1/2/3 decision rule |
|
|
23
24
|
|
|
24
25
|
## Quick Reference
|
|
25
26
|
|
|
@@ -30,3 +31,4 @@ This skill covers advanced BRICKS features not in the main project instructions.
|
|
|
30
31
|
- **AI offloading**: See [Buttress](rules/buttress.md) for GPU server delegation
|
|
31
32
|
- **E2E testing**: See [Automations](rules/automations.md) for test automation
|
|
32
33
|
- **Enter animations**: See [Standby Transition](rules/standby-transition.md) for canvas transitions
|
|
34
|
+
- **Verification before done**: See [Verification Toolchain](rules/verification-toolchain.md) for the definition-of-done gate and Path 1/2/3 decision rule
|
|
@@ -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
|
|
|
@@ -20,16 +20,26 @@ When mobile devices or embedded systems lack hardware for local AI inference (LL
|
|
|
20
20
|
|
|
21
21
|
## Client Configuration
|
|
22
22
|
|
|
23
|
-
In generator properties, configure
|
|
23
|
+
In generator properties, configure `buttressConnectionSettings`:
|
|
24
24
|
|
|
25
25
|
| Setting | Description |
|
|
26
26
|
|---------|-------------|
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
27
|
+
| `enabled` | Toggle Buttress offloading |
|
|
28
|
+
| `autoDiscoverType` | `auto` (LAN auto-discovery, recommended) or `manual` (typed URL) |
|
|
29
|
+
| `url` | Server URL (`manual` mode only — leave empty for `auto`) |
|
|
30
|
+
| `fallbackType` | If Buttress is unreachable: `use-local` runs locally, `no-op` blocks the load |
|
|
31
|
+
| `strategy` | Execution preference (see below) |
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
The launcher provides the access token automatically — there is **no user-facing access-token setting**. In `auto` mode the launcher discovers servers bound to the device's workspace and uses its workspace-scoped JWT; in `manual` mode it sends the same JWT only when the typed URL belongs to a server bound to the same workspace (verified against the server's `/buttress/info`).
|
|
34
|
+
|
|
35
|
+
### `autoDiscoverType` options
|
|
36
|
+
|
|
37
|
+
| Mode | When to use | What the device does |
|
|
38
|
+
|------|-------------|----------------------|
|
|
39
|
+
| `auto` | LAN deployment with a workspace-bound buttress-server | Listens for UDP announcements and picks the strongest server bound to its workspace; reconnects automatically when the workspace flips |
|
|
40
|
+
| `manual` | Internet-reachable server, mixed networks, or a public/unbound server | Connects to the typed `url` directly; sends a token only when the server is bound to the same workspace |
|
|
41
|
+
|
|
42
|
+
### Strategy options
|
|
33
43
|
|
|
34
44
|
| Strategy | Description |
|
|
35
45
|
|----------|-------------|
|
|
@@ -37,7 +47,11 @@ In generator properties, configure Buttress settings:
|
|
|
37
47
|
| `prefer-buttress` | Use Buttress if available, fallback to local |
|
|
38
48
|
| `prefer-best` | Auto-select based on capability comparison |
|
|
39
49
|
|
|
40
|
-
## Generator Configuration
|
|
50
|
+
## Generator Configuration Examples
|
|
51
|
+
|
|
52
|
+
### Auto-discovery (recommended)
|
|
53
|
+
|
|
54
|
+
Workspace-bound launcher + workspace-bound buttress-server on the same LAN. No URL needed — discovery picks the highest-scoring server that runs the requested backend.
|
|
41
55
|
|
|
42
56
|
```typescript
|
|
43
57
|
import { makeId } from 'bricks-ctor'
|
|
@@ -53,7 +67,7 @@ const llmGenerator: GeneratorLLM = {
|
|
|
53
67
|
contextSize: 8192,
|
|
54
68
|
buttressConnectionSettings: {
|
|
55
69
|
enabled: true,
|
|
56
|
-
|
|
70
|
+
autoDiscoverType: 'auto',
|
|
57
71
|
fallbackType: 'use-local',
|
|
58
72
|
strategy: 'prefer-best',
|
|
59
73
|
},
|
|
@@ -63,6 +77,44 @@ const llmGenerator: GeneratorLLM = {
|
|
|
63
77
|
}
|
|
64
78
|
```
|
|
65
79
|
|
|
80
|
+
### Manual URL
|
|
81
|
+
|
|
82
|
+
Use when the server isn't on the launcher's broadcast domain (cross-subnet, internet, behind a reverse proxy) or you want to point at a specific public/unbound server.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
buttressConnectionSettings: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
autoDiscoverType: 'manual',
|
|
88
|
+
url: 'http://192.168.1.100:2080',
|
|
89
|
+
fallbackType: 'use-local',
|
|
90
|
+
strategy: 'prefer-best',
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> ⚠️ With `fallbackType: 'no-op'` the generator refuses to load locally if Buttress is unreachable. Use `'use-local'` whenever the device can also run the model standalone.
|
|
95
|
+
|
|
96
|
+
## Integrating a discovered buttress-server
|
|
97
|
+
|
|
98
|
+
The ctor-desktop "Local Devices" panel (and the `bricks buttress scan` CLI) can hand you a server's identity, workspace, and announced generator caps. When the user asks to integrate one:
|
|
99
|
+
|
|
100
|
+
1. **Confirm workspace match.** The discovery message says whether the server's workspace matches this project's. If it doesn't, do NOT add the integration — instruct the user to run `bricks buttress unbind && bricks buttress bind` from this workspace's owner CLI on the server host. The launcher won't trust a cross-workspace server.
|
|
101
|
+
|
|
102
|
+
2. **Map announced types to generator templates.** For each `generators[].type` in the announce, target the matching templateKey (create the generator if absent):
|
|
103
|
+
|
|
104
|
+
| Announced type | templateKey | Default model |
|
|
105
|
+
|----------------|------------------------------|---------------|
|
|
106
|
+
| `ggml-llm` | `GENERATOR_LLM` | `ggml-org/gemma-3-12b-it-qat-GGUF` (≥20 GB usable) or `ggml-org/gpt-oss-20b-GGUF` (~12 GB usable) |
|
|
107
|
+
| `mlx-llm` | `GENERATOR_MLX_LLM` | `mlx-community/Qwen3-4B-4bit` (or a larger 4-bit quant if `usableBytes` allows) |
|
|
108
|
+
| `ggml-stt` | `GENERATOR_SPEECH_INFERENCE` | `BricksDisplay/whisper-ggml` (filename `ggml-small-q8_0.bin`) |
|
|
109
|
+
|
|
110
|
+
Pick a model that fits the announced `usableBytes`. Set `contextSize` to match.
|
|
111
|
+
|
|
112
|
+
3. **Use the canonical auto-discovery config** from "Auto-discovery (recommended)" above. Don't switch to manual mode just because the server is on the LAN — auto mode picks the workspace-bound server automatically and rebinds when the device's workspace flips.
|
|
113
|
+
|
|
114
|
+
### Real-device caveat
|
|
115
|
+
|
|
116
|
+
Buttress LAN auto-discovery uses native UDP via `react-native-jsi-udp`. The iOS Simulator silently fails to bind UDP, so auto mode is a no-op there. With `fallbackType: 'use-local'` the generator falls back to local inference in the simulator — dev/test stays functional. **Validate the buttress path itself only when deploying to a real Foundation device; don't gate the build on simulator validation.**
|
|
117
|
+
|
|
66
118
|
## Server Setup
|
|
67
119
|
|
|
68
120
|
### Requirements
|
|
@@ -98,6 +150,43 @@ bricks-buttress --config ./config.toml
|
|
|
98
150
|
|----------|-------------|
|
|
99
151
|
| `HF_TOKEN` | Hugging Face token for model downloads |
|
|
100
152
|
| `ENABLE_OPENAI_COMPAT_ENDPOINT` | Set to `1` for OpenAI-compatible API |
|
|
153
|
+
| `BRICKS_BUTTRESS_STATE_DIR` | Override the workspace state directory (default `~/.bricks-cli/buttress`) |
|
|
154
|
+
|
|
155
|
+
## Bind the Server to a Workspace
|
|
156
|
+
|
|
157
|
+
To use `autoDiscoverType: 'auto'` — and to require workspace-scoped JWT auth — pair the buttress-server with a BRICKS workspace using the `bricks buttress` CLI commands. An unbound server stays in legacy public mode and accepts any LAN client.
|
|
158
|
+
|
|
159
|
+
### One-time bind
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Log into the cloud bricks-server with the workspace owner's account
|
|
163
|
+
bricks auth login
|
|
164
|
+
|
|
165
|
+
# Pair the buttress-server running on this machine with the active workspace
|
|
166
|
+
bricks buttress bind
|
|
167
|
+
|
|
168
|
+
# Restart bricks-buttress so it picks up the new state.json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`bricks buttress bind` writes `~/.bricks-cli/buttress/state.json` containing the workspace id, the issuer's Ed25519 public key, and a stable `serverId` (defaults to `buttress-<machineId>`). Override location with `--state-dir` or `$BRICKS_BUTTRESS_STATE_DIR`. For headless setups, use `bricks buttress bind --print` to emit state.json to stdout.
|
|
172
|
+
|
|
173
|
+
### Verify and manage
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Show local binding + workspace-side bound list
|
|
177
|
+
bricks buttress status
|
|
178
|
+
|
|
179
|
+
# Discover buttress-servers on the LAN (with version, auth state, generator caps)
|
|
180
|
+
bricks buttress scan
|
|
181
|
+
|
|
182
|
+
# Issue a long-lived workspace access token (CI / ctor agents that already hold a workspace token)
|
|
183
|
+
bricks buttress issue-token --ttl 86400
|
|
184
|
+
|
|
185
|
+
# Detach this server from the workspace
|
|
186
|
+
bricks buttress unbind
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
After binding, launchers in the same workspace will auto-discover this server; manual-mode generators will only send their access token if the server's reported workspace matches their own.
|
|
101
190
|
|
|
102
191
|
## Server Configuration (TOML)
|
|
103
192
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Verification Toolchain
|
|
2
|
+
|
|
3
|
+
Three runtime targets, one decision rule. Verify against the cheapest path that proves what you need to prove.
|
|
4
|
+
|
|
5
|
+
## Definition of done — the hard gate
|
|
6
|
+
|
|
7
|
+
Before the agent is allowed to claim work is complete, **all** of the following must be true:
|
|
8
|
+
|
|
9
|
+
1. **Compile clean.** Latest source passes `compile` with no errors.
|
|
10
|
+
2. **Every Canvas screenshot captured.** A rendered screenshot of every Canvas in the deliverable, captured via `preview` MCP (`responseImage: true`) at the target hardware resolution and orientation. Canvases gated behind events are reached via Automation runs (`testId` / `testTitleLike` with `brick_press` / `wait_until_canvas_change` cases). Single-Canvas Subspaces still require a captured screenshot.
|
|
11
|
+
3. **Every screenshot reviewed by the agent.** The agent must read each captured screenshot back through the host's image-reading capability — saving the file is not the same as seeing it. The model that produced the Canvas must also have seen the rendered result, or the verification gate has not passed.
|
|
12
|
+
4. **Reference comparison report.** If the user supplied any reference material (Figma / website / HTML / screenshot / PDF / brand book / competitor), produce a short delta report per Canvas:
|
|
13
|
+
- What matches the reference.
|
|
14
|
+
- What intentionally diverges, and why (deployment constraint, BRICKS-runtime semantic, system commitment).
|
|
15
|
+
- What unintentionally diverges, and the planned fix.
|
|
16
|
+
5. **Path appropriate to deployment executed.** Path 1 by default. Path 2 (on-device) when the deployment depends on real hardware behaviour, or whenever the brief touches payment, identity, peripherals, or LocalSync.
|
|
17
|
+
6. **Console clean.** No 404s on media, no Data-key-undefined warnings, no Generator init failures, no React-style warnings — every console line is either zero or accept-with-a-comment.
|
|
18
|
+
|
|
19
|
+
What does **not** count as done:
|
|
20
|
+
|
|
21
|
+
- A single hero-Canvas screenshot.
|
|
22
|
+
- "Looks roughly like the reference" with no per-Canvas comparison.
|
|
23
|
+
- "Compiled successfully" with no rendered preview.
|
|
24
|
+
- A claim of done with no screenshots in evidence.
|
|
25
|
+
- Captured screenshots saved to disk but never read back by the agent.
|
|
26
|
+
|
|
27
|
+
If any item is unmet, the work is mid-iteration. Say so explicitly to the user; offer a precise list of what remains.
|
|
28
|
+
|
|
29
|
+
## Path 1 — Electron preview (no device required)
|
|
30
|
+
|
|
31
|
+
The default loop. Always available; deterministic; no device wear; safe for side-effecting flows because nothing real fires.
|
|
32
|
+
|
|
33
|
+
### `bricks-ctor` MCP — `compile`
|
|
34
|
+
|
|
35
|
+
Typecheck + compile the project. Gate every iteration on this; everything below assumes a clean compile.
|
|
36
|
+
|
|
37
|
+
Agent invocation: call the MCP tool `compile` exposed by the `bricks-ctor` MCP server registered for the project. No arguments.
|
|
38
|
+
|
|
39
|
+
### `bricks-ctor` MCP — `preview`
|
|
40
|
+
|
|
41
|
+
Launches headless Electron, takes a screenshot, optionally runs a named Automation test by id or partial title.
|
|
42
|
+
|
|
43
|
+
Arguments:
|
|
44
|
+
- `delay` (ms before screenshot) — default 3000. Increase if Standby Transitions are still in flight.
|
|
45
|
+
- `width`, `height` — screenshot dimensions in px.
|
|
46
|
+
- `responseImage: true` — return base64 jpeg as MCP image content (recommended for visual sanity).
|
|
47
|
+
- `testId` or `testTitleLike` — run a project Automation before the screenshot. Use `testTitleLike` for fuzzy matches (case-insensitive partial title).
|
|
48
|
+
|
|
49
|
+
Returns text log + (if `responseImage`) base64 image content.
|
|
50
|
+
|
|
51
|
+
### `bun preview` (project script)
|
|
52
|
+
|
|
53
|
+
Sustained dev session — watches `subspaces/`, recompiles on save, exposes CDP at `localhost:19852` (configurable via `--cdp-port`), writes `.bricks/devtools.json` with port/pid for downstream tooling.
|
|
54
|
+
|
|
55
|
+
Useful flags:
|
|
56
|
+
- `--screenshot` + `--screenshot-delay/width/height/path` — drive screenshot capture without keeping the window open.
|
|
57
|
+
- `--show-menu` — surface the Electron menu (debugging).
|
|
58
|
+
- `--test-id` / `--test-title-like` — run an Automation in this preview.
|
|
59
|
+
- `--no-keep-open` — exit after one screenshot.
|
|
60
|
+
- `--no-cdp` — disable CDP server (rare; default is to expose it).
|
|
61
|
+
- `--clear-cache` — reset cached state between runs.
|
|
62
|
+
|
|
63
|
+
For ad-hoc CDP inspection against this local preview, connect any CDP client to `localhost:19852` — Chrome DevTools front-end works directly. For an agent-friendly CLI over CDP (screenshot, brick tree/query, input emulation, storage reads, runtime eval, network capture), the `bricks-cli` skill documents the `bricks devtools` command surface — read that skill if it is installed in this workspace. If it is not installed, run `bricks --help` and `bricks devtools --help`; the CLI's own help output is authoritative.
|
|
64
|
+
|
|
65
|
+
### Project Automations
|
|
66
|
+
|
|
67
|
+
E2E tests authored in TypeScript inside the project (`AutomationTest` / `TestCase`). Test cases include:
|
|
68
|
+
|
|
69
|
+
- `brick_press` — synthesize a press on a Brick.
|
|
70
|
+
- `wait_until_canvas_change` — assert navigation.
|
|
71
|
+
- `assert_property` — assert a Data value equals expected.
|
|
72
|
+
- `wait_property_update` — wait for a Data value to change.
|
|
73
|
+
- `match_screenshot` — visual regression with stored reference image.
|
|
74
|
+
|
|
75
|
+
Trigger types: `launch` (runs on app start), `anytime` (manual), `cron` (scheduled).
|
|
76
|
+
|
|
77
|
+
Important: the automation map id must be `'AUTOMATION_MAP_DEFAULT'` (not a generated id) — the preview test runner reads from `automationMap['AUTOMATION_MAP_DEFAULT']?.map`.
|
|
78
|
+
|
|
79
|
+
Run a single test from the agent: `bricks-ctor` MCP `preview` with `testId` or `testTitleLike`.
|
|
80
|
+
|
|
81
|
+
## Path 2 — Real device with DevTools enabled
|
|
82
|
+
|
|
83
|
+
Required when the deployment depends on hardware behaviour the Electron preview cannot reproduce: physical orientation/DPI, real touch hardware (IR overlays, multi-touch), peripherals (camera, BLE, MQTT, NFC, payment, printer, sensors), watchdog cycles on the actual launcher build, fleet-managed reboot semantics, LocalSync across multiple devices, the actual color/luminance of the panel.
|
|
84
|
+
|
|
85
|
+
**Always Path 2** when the brief touches: payment, identity capture, real-time peripheral data, multi-device LocalSync, certified hardware.
|
|
86
|
+
|
|
87
|
+
### One-time manual setup (the agent cannot do this)
|
|
88
|
+
|
|
89
|
+
Ask the user to:
|
|
90
|
+
|
|
91
|
+
1. On the device, open **Settings** → advanced settings.
|
|
92
|
+
2. Toggle **Chrome DevTools** on. The device starts a DevTools server on the local network on port `19851` (auto-increments if taken).
|
|
93
|
+
3. Confirm **Enable LAN Discovery** is on (default) so the device is discoverable.
|
|
94
|
+
4. Note the device passcode if displayed.
|
|
95
|
+
|
|
96
|
+
Requires BRICKS Foundation **≥ 2.24** for CDP commands.
|
|
97
|
+
|
|
98
|
+
### Endpoints exposed once enabled
|
|
99
|
+
|
|
100
|
+
| Endpoint | URL shape | Use |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Web UI | `http://<ip>:19851` | DevTools landing in a browser |
|
|
103
|
+
| CDP | `http://<ip>:19851/devtools-frontend/inspector.html?ws=<ip>:19851/ws/<passcode>` | Chrome DevTools front-end |
|
|
104
|
+
| MCP | `http://<ip>:19851/mcp` | MCP endpoint (Bearer-token auth with passcode) |
|
|
105
|
+
| MCP SSE | `http://<ip>:19851/sse` | Same, SSE transport |
|
|
106
|
+
| Info | `http://<ip>:19851/devtools/info` | Device / server metadata |
|
|
107
|
+
|
|
108
|
+
### Driving the device
|
|
109
|
+
|
|
110
|
+
Once DevTools is on, ask the user how they want to drive the verification — Chrome DevTools front-end, an MCP client they already run, or screenshot export from their own tooling — and follow their lead. Capture every Canvas screenshot through whichever path the user picks; the verification gate is about the evidence, not the tool.
|
|
111
|
+
|
|
112
|
+
For agent-driven CDP/MCP work against the device (`bricks devtools …` with `-a <ip> --passcode <pc>`, plus bridging the device MCP endpoint into an MCP client), the same `bricks-cli` skill referenced in Path 1 covers the on-device case — read it if installed. If not installed, run `bricks --help` and `bricks devtools --help` for the authoritative command listing.
|
|
113
|
+
|
|
114
|
+
### Real-device side-effects warning
|
|
115
|
+
|
|
116
|
+
Real devices fire real peripherals. Payment terminals charge. MQTT broadcasts on shared topics. BLE advertises to bystanders. Use a **staging** device for verification cycles; never iterate on a production-deployed device unless the user explicitly approves.
|
|
117
|
+
|
|
118
|
+
## Path 3 — Remote debugging via BRICKS Controller (off-LAN)
|
|
119
|
+
|
|
120
|
+
Out of scope for the standard verification loop. It exists for ops scenarios where the device is not on the same network. If the user asks for it, point them at the BRICKS Controller documentation rather than attempting it as part of verification.
|
|
121
|
+
|
|
122
|
+
## Decision rule
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
default Path 1.
|
|
126
|
+
|
|
127
|
+
if deployment depends on:
|
|
128
|
+
physical orientation / DPI / touch HW / peripherals /
|
|
129
|
+
watchdog on real launcher / LocalSync / certified hardware
|
|
130
|
+
→ escalate to Path 2 before declaring done.
|
|
131
|
+
|
|
132
|
+
if brief touches payment, identity, peripherals, LocalSync
|
|
133
|
+
→ Path 2 is mandatory, not optional.
|
|
134
|
+
|
|
135
|
+
if user is off-LAN
|
|
136
|
+
→ Path 3 (out of scope here).
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## What to actually verify (per shape)
|
|
140
|
+
|
|
141
|
+
### Static signage (single-canvas glance loop)
|
|
142
|
+
- Path 1 screenshot captures the loop frame.
|
|
143
|
+
- Watch one full rotation in `bun preview` to see all queue items.
|
|
144
|
+
- Cut network mid-loop; confirm cached media plays.
|
|
145
|
+
- Path 2 only if the panel is OLED / has burn-in concerns or specific color profile.
|
|
146
|
+
|
|
147
|
+
### Multi-canvas state machine (kiosk)
|
|
148
|
+
- Path 1: Automation walking the happy path with `brick_press` + `wait_until_canvas_change` + `assert_property` + `match_screenshot` per Canvas.
|
|
149
|
+
- Cancel + inactivity reset paths verified as Automations.
|
|
150
|
+
- Watchdog reset: kill the runtime, restart, confirm boot Canvas resets transient flow Data.
|
|
151
|
+
- Path 2 mandatory if payment / identity is in the flow — fire a real test transaction on staging hardware.
|
|
152
|
+
|
|
153
|
+
### Subspace-driven composition
|
|
154
|
+
- Path 1: open the Subspace standalone if the preview supports it; verify in isolation.
|
|
155
|
+
- Embedded test: walk the host's flow; assert Outlets fire as expected via `wait_property_update`.
|
|
156
|
+
- Re-mount test: cause the host to re-bind the Subspace; confirm internal state resets.
|
|
157
|
+
|
|
158
|
+
### Generator-driven reactive
|
|
159
|
+
- Path 1: drive the source by writing to Data from the runtime (whatever CDP path the user prefers); observe Brick re-render.
|
|
160
|
+
- Throttle / firehose tests: write 100 updates rapidly; confirm the canvas doesn't choke (frame budget intact).
|
|
161
|
+
- Disconnect: simulate source disconnect; confirm UI surfaces stale state, not blank.
|
|
162
|
+
- Threshold transitions: cross every Switch boundary; confirm visual reaction.
|
|
163
|
+
- Path 2 mandatory if the source is a real peripheral — Electron cannot fake the timing or the failure modes.
|
|
164
|
+
|
|
165
|
+
### Multi-device LocalSync
|
|
166
|
+
- Path 2 mandatory. Two devices on the same LAN with DevTools on. Drive one, observe the other.
|
|
167
|
+
|
|
168
|
+
## Browser / runtime log discipline
|
|
169
|
+
|
|
170
|
+
Throughout: the DevTools console must be clean. 404s on media, "Data key not defined" warnings, Generator init failures, React-style warnings — every one is a defect or accept-with-comment. Don't ship around them.
|