@fugood/bricks-ctor 2.25.0-beta.23 → 2.25.0-beta.24

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.
@@ -243,14 +243,14 @@ describe('generateCalulationMap', () => {
243
243
  expect(result.map[aCmdId].in.value[0].disable_trigger_command).toBeUndefined()
244
244
  })
245
245
 
246
- test('snapshotMode forwards to makeId so generated ids are sequential', () => {
246
+ test('snapshotMode forwards to makeId so generated ids are deterministic v4 uuids', () => {
247
247
  const result = generateCalulationMap(baseConfig({ inputs: { a: 'foo' } }), {
248
248
  snapshotMode: true,
249
249
  })
250
250
  const sandboxIds = sandboxNodeIds(result.map).map((s) => s.id)
251
- // Snapshot-mode ids embed a 12-digit counter — stable suffix lets us assert order.
251
+ const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
252
252
  sandboxIds.forEach((id) => {
253
- expect(id).toMatch(/^PROPERTY_BANK_COMMAND_NODE_00000000-0000-0000-0000-\d{12}$/)
253
+ expect(id).toMatch(new RegExp(`^PROPERTY_BANK_COMMAND_NODE_${uuid}$`))
254
254
  })
255
255
  })
256
256
 
package/compile/index.ts CHANGED
@@ -1460,5 +1460,7 @@ export const compile = async (app: Application) => {
1460
1460
 
1461
1461
  export const checkConfig = async (configPath: string) => {
1462
1462
  const { sh } = await import('../tools/_shell')
1463
- await sh`bricks app check-config ${configPath}`
1463
+ // --validate-automation surfaces broken automation_map / test_map refs early,
1464
+ // which catches agent-authored automations that reference deleted bricks.
1465
+ await sh`bricks app check-config --validate-automation ${configPath}`
1464
1466
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.25.0-beta.23",
3
+ "version": "2.25.0-beta.24",
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.21",
10
+ "@fugood/bricks-cli": "^2.25.0-beta.24",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -25,5 +25,5 @@
25
25
  "peerDependencies": {
26
26
  "oxfmt": "^0.36.0"
27
27
  },
28
- "gitHead": "8b167250b57c5e9973e60de8266b880d15adcce6"
28
+ "gitHead": "529cad1e3d46ef2fbff4c8aaf759eec3d8ca289e"
29
29
  }
@@ -118,6 +118,16 @@ const testLoginFlow: AutomationTest = {
118
118
  | `match_screenshot` | `[name, threshold?, maxRetry?]` | Screenshot compare |
119
119
  | `delay` | `[subspace?, property?, defaultValue?]` | Delay execution |
120
120
 
121
+ In project TypeScript source, pass entity getters for BRICKS entities:
122
+
123
+ ```typescript
124
+ run: ['brick_press', () => mainSubspace, () => bricks.bSubmitButton]
125
+ run: ['wait_until_canvas_change', () => mainSubspace, () => canvases.cDone, 5000]
126
+ run: ['assert_property', () => mainSubspace, () => data.dStep, 'done']
127
+ ```
128
+
129
+ The compiler resolves these getters to the current generated IDs.
130
+
121
131
  ### execute_action Params
122
132
 
123
133
  The `params` object in `execute_action` uses **runtime event property keys** from `event-props.ts`, NOT the action config `input` names from type definitions.
@@ -208,6 +218,7 @@ Automations work with Modules. Use Manual Run in Preview mode for module testing
208
218
 
209
219
  - **Automation map key**: Always use `'AUTOMATION_MAP_DEFAULT'` as the automation map ID (not `makeId()`). The preview test runner reads from `automationMap['AUTOMATION_MAP_DEFAULT']?.map`.
210
220
  - **Valid makeId types**: Use `'test'` for AutomationTest, `'test_case'` for TestCase, `'test_var'` for TestVariable. Do NOT use `'automation_test'` or `'automation_test_map'`.
221
+ - **Entity references in run arrays**: Use getter references (`() => subspace`, `() => bricks.bButton`, `() => data.dValue`) in TypeScript source so compile resolves fresh IDs.
211
222
  - **handler in execute_action**: Pass the entity's `.id` string (e.g., `bricks.bInput.id`), not a getter function.
212
223
 
213
224
  ## Best Practices
@@ -1,8 +1,24 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
1
4
  import { makeId } from '../id'
2
5
 
3
- const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
6
+ const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
4
7
  const UUID_ONLY_RE = new RegExp(`^${UUID}$`)
5
8
 
9
+ const withProjectDir = (app, fn) => {
10
+ const previousCwd = process.cwd()
11
+ const dir = mkdtempSync(join(tmpdir(), 'make-id-'))
12
+ try {
13
+ if (app) writeFileSync(join(dir, 'application.json'), JSON.stringify(app))
14
+ process.chdir(dir)
15
+ return fn()
16
+ } finally {
17
+ process.chdir(previousCwd)
18
+ rmSync(dir, { recursive: true, force: true })
19
+ }
20
+ }
21
+
6
22
  describe('makeId', () => {
7
23
  describe('type prefixes', () => {
8
24
  const cases = [
@@ -25,8 +41,23 @@ describe('makeId', () => {
25
41
  expect(makeId(type)).toMatch(new RegExp(`^${prefix}${UUID}$`))
26
42
  })
27
43
 
28
- test('returns unique random uuids across calls', () => {
29
- expect(makeId('brick')).not.toBe(makeId('brick'))
44
+ test('uses generated count fallback when no alias is provided', () => {
45
+ const first = withProjectDir({ id: 'count-fallback-app' }, () => {
46
+ jest.resetModules()
47
+ const { makeId: makeFreshId } = require('../id')
48
+ return [makeFreshId('brick'), makeFreshId('brick')]
49
+ })
50
+ const second = withProjectDir({ id: 'count-fallback-app' }, () => {
51
+ jest.resetModules()
52
+ const { makeId: makeFreshId } = require('../id')
53
+ return [makeFreshId('brick'), makeFreshId('brick')]
54
+ })
55
+
56
+ expect(first[0]).toMatch(new RegExp(`^BRICK_${UUID}$`))
57
+ expect(first[1]).toMatch(new RegExp(`^BRICK_${UUID}$`))
58
+ expect(first[0]).not.toBe(first[1])
59
+ expect(second).toEqual(first)
60
+ jest.resetModules()
30
61
  })
31
62
 
32
63
  test('unknown type falls through to an unprefixed uuid', () => {
@@ -41,18 +72,83 @@ describe('makeId', () => {
41
72
  })
42
73
 
43
74
  describe('snapshotMode', () => {
44
- test('produces sequential, deterministic uuids', () => {
45
- const a = makeId('brick', { snapshotMode: true })
46
- const b = makeId('brick', { snapshotMode: true })
47
- expect(a).toMatch(/^BRICK_00000000-0000-0000-0000-\d{12}$/)
48
- expect(b).toMatch(/^BRICK_00000000-0000-0000-0000-\d{12}$/)
49
- expect(Number(b.slice(-12))).toBe(Number(a.slice(-12)) + 1)
75
+ test('produces deterministic v4 uuids from the generated count', () => {
76
+ const previousCwd = process.cwd()
77
+ const dir = mkdtempSync(join(tmpdir(), 'make-id-snapshot-'))
78
+ writeFileSync(join(dir, 'application.json'), JSON.stringify({ id: 'snapshot-app' }))
79
+ try {
80
+ process.chdir(dir)
81
+ jest.resetModules()
82
+ const { makeId: makeFreshId } = require('../id')
83
+ const a = makeFreshId('brick', { snapshotMode: true })
84
+ const b = makeFreshId('brick', { snapshotMode: true })
85
+
86
+ jest.resetModules()
87
+ const { makeId: makeFreshIdAgain } = require('../id')
88
+ expect(makeFreshIdAgain('brick', { snapshotMode: true })).toBe(a)
89
+ expect(makeFreshIdAgain('brick', { snapshotMode: true })).toBe(b)
90
+ expect(a).not.toBe(b)
91
+ expect(a).toMatch(new RegExp(`^BRICK_${UUID}$`))
92
+ expect(b).toMatch(new RegExp(`^BRICK_${UUID}$`))
93
+ } finally {
94
+ process.chdir(previousCwd)
95
+ rmSync(dir, { recursive: true, force: true })
96
+ jest.resetModules()
97
+ }
50
98
  })
51
99
 
52
100
  test('snapshotMode false falls back to random uuid', () => {
53
101
  const id = makeId('canvas', { snapshotMode: false })
54
102
  expect(id).toMatch(new RegExp(`^CANVAS_${UUID}$`))
55
- expect(id).not.toMatch(/CANVAS_00000000-0000-0000-0000-/)
103
+ })
104
+ })
105
+
106
+ describe('stable aliases', () => {
107
+ test('produces the same id for the same application id, type, and alias', () => {
108
+ const first = withProjectDir({ id: 'stable-app' }, () => {
109
+ jest.resetModules()
110
+ return require('../id').makeId('brick', 'stable-hero')
111
+ })
112
+ const second = withProjectDir({ id: 'stable-app' }, () => {
113
+ jest.resetModules()
114
+ return require('../id').makeId('brick', 'stable-hero')
115
+ })
116
+
117
+ expect(second).toBe(first)
118
+ expect(first).toMatch(new RegExp(`^BRICK_${UUID}$`))
119
+ jest.resetModules()
120
+ })
121
+
122
+ test('uses the application id when hashing stable aliases', () => {
123
+ const idA = withProjectDir({ id: 'app-a' }, () => makeId('brick', 'hero'))
124
+ const idB = withProjectDir({ id: 'app-b' }, () => makeId('brick', 'hero'))
125
+
126
+ expect(idA).toMatch(new RegExp(`^BRICK_${UUID}$`))
127
+ expect(idB).toMatch(new RegExp(`^BRICK_${UUID}$`))
128
+ expect(idA).not.toBe(idB)
129
+ })
130
+
131
+ test('rejects duplicate aliases for the same application id and entry type', () => {
132
+ withProjectDir({ id: 'duplicate-app' }, () => {
133
+ expect(makeId('brick', 'shared')).toMatch(new RegExp(`^BRICK_${UUID}$`))
134
+ expect(() => makeId('brick', 'shared')).toThrow(/Duplicate makeId alias 'shared'/)
135
+ })
136
+ })
137
+
138
+ test('allows the same alias for different entry types', () => {
139
+ withProjectDir({ id: 'shared-alias-app' }, () => {
140
+ const brick = makeId('brick', 'shared')
141
+ const canvas = makeId('canvas', 'shared')
142
+
143
+ expect(brick).toMatch(new RegExp(`^BRICK_${UUID}$`))
144
+ expect(canvas).toMatch(new RegExp(`^CANVAS_${UUID}$`))
145
+ expect(brick).not.toBe(canvas)
146
+ })
147
+ })
148
+
149
+ test('falls back when application.json is not present', () => {
150
+ const id = withProjectDir(null, () => makeId('brick', 'fallback-app-id'))
151
+ expect(id).toMatch(new RegExp(`^BRICK_${UUID}$`))
56
152
  })
57
153
  })
58
154
  })
package/utils/id.ts CHANGED
@@ -1,38 +1,83 @@
1
1
  import { v4 as uuid } from 'uuid'
2
+ import { createHash } from 'node:crypto'
3
+ import { readFileSync } from 'node:fs'
4
+ import { join } from 'node:path'
2
5
 
3
6
  let count = 0
4
7
 
5
- // Used for snapshot mode
6
- const countUUID = () => {
7
- const current = count
8
- count += 1
9
- return `00000000-0000-0000-0000-${current.toString().padStart(12, '0')}`
8
+ type IdType =
9
+ | 'animation'
10
+ | 'brick'
11
+ | 'canvas'
12
+ | 'generator'
13
+ | 'data'
14
+ | 'switch'
15
+ | 'property_bank_command'
16
+ | 'property_bank_calc'
17
+ | 'dynamic-brick'
18
+ | 'automation_map'
19
+ | 'test'
20
+ | 'test_case'
21
+ | 'test_var'
22
+ | 'subspace'
23
+
24
+ type IdOptions = {
25
+ snapshotMode?: boolean
26
+ }
27
+
28
+ const APPLICATION_ID_FALLBACK = 'unknown-application'
29
+
30
+ const usedStableAliases = new Set<string>()
31
+ let applicationIdCache: { cwd: string; id: string } | null = null
32
+
33
+ const readApplicationId = () => {
34
+ const cwd = process.cwd()
35
+ if (applicationIdCache?.cwd === cwd) return applicationIdCache.id
36
+
37
+ let id = APPLICATION_ID_FALLBACK
38
+ try {
39
+ const app = JSON.parse(readFileSync(join(cwd, 'application.json'), 'utf8'))
40
+ if (typeof app.id === 'string' && app.id) id = app.id
41
+ } catch {
42
+ // `makeId` is also used from tests and utilities outside project roots.
43
+ }
44
+
45
+ applicationIdCache = { cwd, id }
46
+ return id
47
+ }
48
+
49
+ const hashToRandomBytes = (parts: string[]) => {
50
+ const hash = createHash('sha256').update(JSON.stringify(parts)).digest()
51
+ return new Uint8Array(hash.subarray(0, 16))
10
52
  }
11
53
 
12
- // Make a random id if not want to use fixed id
13
- export const makeId = (
14
- type:
15
- | 'animation'
16
- | 'brick'
17
- | 'canvas'
18
- | 'generator'
19
- | 'data'
20
- | 'switch'
21
- | 'property_bank_command'
22
- | 'property_bank_calc'
23
- | 'dynamic-brick'
24
- | 'automation_map'
25
- | 'test'
26
- | 'test_case'
27
- | 'test_var'
28
- | 'subspace',
29
- opts?: {
30
- snapshotMode?: boolean
31
- },
32
- ) => {
54
+ const makeStableUuid = (type: string, alias?: string) => {
55
+ const applicationId = readApplicationId()
56
+ const seed = alias ?? String(count)
57
+ if (alias === undefined) count += 1
58
+
59
+ if (alias !== undefined) {
60
+ const aliasKey = JSON.stringify([applicationId, type, alias])
61
+ if (usedStableAliases.has(aliasKey)) {
62
+ throw new Error(`Duplicate makeId alias '${alias}' for type '${type}'`)
63
+ }
64
+ usedStableAliases.add(aliasKey)
65
+ }
66
+
67
+ return uuid({
68
+ random: hashToRandomBytes([applicationId, type, seed]),
69
+ })
70
+ }
71
+
72
+ // Make stable ids by default; explicit snapshotMode: false preserves the random escape hatch.
73
+ export const makeId = (type: IdType, aliasOrOpts?: string | IdOptions, opts?: IdOptions) => {
33
74
  if (type === 'subspace') {
34
75
  throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
35
76
  }
77
+
78
+ const alias = typeof aliasOrOpts === 'string' ? aliasOrOpts : undefined
79
+ const options = typeof aliasOrOpts === 'string' ? opts : (aliasOrOpts ?? opts)
80
+
36
81
  let prefix = ''
37
82
  switch (type) {
38
83
  case 'animation':
@@ -76,5 +121,11 @@ export const makeId = (
76
121
  break
77
122
  default:
78
123
  }
79
- return `${prefix}${opts?.snapshotMode ? countUUID() : uuid()}`
124
+
125
+ const useCountFallback = aliasOrOpts === undefined && opts === undefined
126
+ const id =
127
+ alias !== undefined || options?.snapshotMode || useCountFallback
128
+ ? makeStableUuid(type, alias)
129
+ : uuid()
130
+ return `${prefix}${id}`
80
131
  }