@fugood/bricks-project 2.25.0-beta.43 → 2.25.0-beta.46
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/config-diff.ts +64 -17
- package/compile/index.ts +30 -20
- package/compile/util.ts +10 -7
- package/package.json +2 -2
- package/package.json.bak +2 -2
- package/skills/bricks-ctor/references/simulator.md +3 -2
- package/skills/bricks-ctor/references/verification-toolchain.md +2 -0
- package/tools/_edits-log.ts +41 -0
- package/tools/mcp-env.ts +1 -1
- package/tools/mcp-tools/_verify.ts +6 -1
- package/tools/mcp-tools/compile.ts +2 -2
- package/tools/mcp-tools/data-calc-editing.ts +3 -33
- package/tools/mcp-tools/entry-editing.ts +3 -33
- package/types/data-calc-command/color.d.ts +1 -1
- package/types/data-calc-command/datetime.d.ts +2 -2
- package/utils/calc.ts +5 -1
- package/utils/id.ts +39 -37
package/compile/config-diff.ts
CHANGED
|
@@ -27,17 +27,60 @@ export type ConfigChange =
|
|
|
27
27
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
28
28
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
29
29
|
|
|
30
|
+
const isJsonDroppedValue = (value: unknown) =>
|
|
31
|
+
value === undefined || typeof value === 'function' || typeof value === 'symbol'
|
|
32
|
+
|
|
33
|
+
const toJsonComparableScalar = (value: unknown) => {
|
|
34
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return null
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const toJsonCompatibleValue = (value: unknown): unknown => {
|
|
39
|
+
if (isJsonDroppedValue(value)) return undefined
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((item) => (isJsonDroppedValue(item) ? null : toJsonCompatibleValue(item)))
|
|
42
|
+
}
|
|
43
|
+
if (isRecord(value)) {
|
|
44
|
+
return Object.entries(value).reduce((acc, [key, item]) => {
|
|
45
|
+
if (!isJsonDroppedValue(item)) acc[key] = toJsonCompatibleValue(item)
|
|
46
|
+
return acc
|
|
47
|
+
}, {})
|
|
48
|
+
}
|
|
49
|
+
return toJsonComparableScalar(value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hasJsonObjectKey = (value: Record<string, unknown>, key: string) =>
|
|
53
|
+
Object.prototype.hasOwnProperty.call(value, key) && !isJsonDroppedValue(value[key])
|
|
54
|
+
|
|
55
|
+
const getJsonArrayItem = (value: unknown[], index: number) => {
|
|
56
|
+
const item = value[index]
|
|
57
|
+
return isJsonDroppedValue(item) ? null : item
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
const deepEqual = (a: unknown, b: unknown): boolean => {
|
|
61
|
+
a = toJsonComparableScalar(a)
|
|
62
|
+
b = toJsonComparableScalar(b)
|
|
31
63
|
if (a === b) return true
|
|
32
64
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
33
|
-
|
|
65
|
+
if (a.length !== b.length) return false
|
|
66
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
67
|
+
if (!deepEqual(getJsonArrayItem(a, index), getJsonArrayItem(b, index))) return false
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
34
70
|
}
|
|
35
71
|
if (isRecord(a) && isRecord(b)) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
let comparableAKeys = 0
|
|
73
|
+
for (const key of Object.keys(a)) {
|
|
74
|
+
if (isJsonDroppedValue(a[key])) continue
|
|
75
|
+
comparableAKeys += 1
|
|
76
|
+
if (!hasJsonObjectKey(b, key) || !deepEqual(a[key], b[key])) return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let comparableBKeys = 0
|
|
80
|
+
for (const key of Object.keys(b)) {
|
|
81
|
+
if (!isJsonDroppedValue(b[key])) comparableBKeys += 1
|
|
82
|
+
}
|
|
83
|
+
return comparableAKeys === comparableBKeys
|
|
41
84
|
}
|
|
42
85
|
return false
|
|
43
86
|
}
|
|
@@ -64,22 +107,32 @@ const diffInto = (
|
|
|
64
107
|
if (isRecord(before) && isRecord(after)) {
|
|
65
108
|
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
66
109
|
for (const key of keys) {
|
|
110
|
+
const beforeHasKey = hasJsonObjectKey(before, key)
|
|
111
|
+
const afterHasKey = hasJsonObjectKey(after, key)
|
|
112
|
+
if (!beforeHasKey && !afterHasKey) continue
|
|
113
|
+
|
|
67
114
|
const nextPath = [...currentPath, key]
|
|
68
|
-
if (!
|
|
69
|
-
else if (!
|
|
70
|
-
|
|
115
|
+
if (!afterHasKey) ops.push({ op: 'unset', path: nextPath })
|
|
116
|
+
else if (!beforeHasKey) {
|
|
117
|
+
ops.push({ op: 'set', path: nextPath, value: toJsonCompatibleValue(after[key]) })
|
|
118
|
+
} else diffInto(before[key], after[key], nextPath, ops)
|
|
71
119
|
}
|
|
72
120
|
return
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
|
|
76
124
|
for (let index = 0; index < after.length; index += 1) {
|
|
77
|
-
diffInto(
|
|
125
|
+
diffInto(
|
|
126
|
+
getJsonArrayItem(before, index),
|
|
127
|
+
getJsonArrayItem(after, index),
|
|
128
|
+
[...currentPath, index],
|
|
129
|
+
ops,
|
|
130
|
+
)
|
|
78
131
|
}
|
|
79
132
|
return
|
|
80
133
|
}
|
|
81
134
|
|
|
82
|
-
ops.push({ op: 'set', path: currentPath, value: after })
|
|
135
|
+
ops.push({ op: 'set', path: currentPath, value: toJsonCompatibleValue(after) })
|
|
83
136
|
}
|
|
84
137
|
|
|
85
138
|
// Diff two compiled configs. `before == null` means there was no prior build to compare
|
|
@@ -100,9 +153,3 @@ export const readBuildConfig = async (projectDir: string): Promise<unknown> => {
|
|
|
100
153
|
return null
|
|
101
154
|
}
|
|
102
155
|
}
|
|
103
|
-
|
|
104
|
-
export const summarizeConfigChange = (change: ConfigChange): string => {
|
|
105
|
-
if (change.status === 'no_baseline') return 'config change: no prior build to compare'
|
|
106
|
-
if (change.status === 'unavailable') return 'config change: build artifact unavailable'
|
|
107
|
-
return change.opCount === 0 ? 'config change: none' : `config change: ${change.opCount} op(s)`
|
|
108
|
-
}
|
package/compile/index.ts
CHANGED
|
@@ -8,10 +8,12 @@ import type { ExportNamedDeclaration, FunctionDeclaration } from 'acorn'
|
|
|
8
8
|
import escodegen from 'escodegen'
|
|
9
9
|
import { makeId } from '../utils/id'
|
|
10
10
|
import { generateCalulationMap } from './util'
|
|
11
|
-
import { computeConfigChange, readBuildConfig, summarizeConfigChange } from './config-diff'
|
|
12
11
|
import { templateActionNameMap } from './action-name-map'
|
|
13
12
|
import { templateEventPropsMap } from '../utils/event-props'
|
|
14
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'
|
|
15
17
|
import type {
|
|
16
18
|
Application,
|
|
17
19
|
Data,
|
|
@@ -702,24 +704,37 @@ const compileAutomation = (automationMap: AutomationMap) =>
|
|
|
702
704
|
}),
|
|
703
705
|
)
|
|
704
706
|
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
|
|
707
|
+
// Record the minimal compiled-config delta this compile produced to the shared audit
|
|
708
|
+
// log (`.bricks/edits.jsonl`), so editing files directly and running `bun compile`
|
|
709
|
+
// leaves the same trail as the MCP source-editing tools. Maintained only in the
|
|
710
|
+
// editing-tools context (`BRICKS_CTOR_ENABLE_EDITING_TOOLS`); the source-editing tools
|
|
711
|
+
// turn it off for their verify compiles (see _verify.ts) so a tool edit records one
|
|
712
|
+
// richer entry instead of an extra generic compile entry. Also silent when there is no
|
|
713
|
+
// prior build to diff against (fresh projects, package tests, tooling outside a project).
|
|
714
|
+
const recordConfigChange = async (previousConfig: unknown, config: unknown) => {
|
|
709
715
|
if (previousConfig == null) return
|
|
710
|
-
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
const change = computeConfigChange(previousConfig,
|
|
716
|
+
if (!isTruthyEnv(process.env.BRICKS_CTOR_ENABLE_EDITING_TOOLS)) return
|
|
717
|
+
// The baseline was parsed from JSON; `computeConfigChange` applies the same
|
|
718
|
+
// JSON-omitted-field rules lazily so compile avoids cloning the full config.
|
|
719
|
+
const change = computeConfigChange(previousConfig, config)
|
|
714
720
|
if (change.status !== 'ok') return
|
|
715
|
-
|
|
716
|
-
|
|
721
|
+
await appendEditRecord(process.cwd(), {
|
|
722
|
+
ts: new Date().toISOString(),
|
|
723
|
+
tool: 'compile',
|
|
724
|
+
provenance: editProvenance(),
|
|
725
|
+
outcome: 'ok',
|
|
726
|
+
summary:
|
|
727
|
+
change.opCount === 0
|
|
728
|
+
? 'compile: no config change'
|
|
729
|
+
: `compile: ${change.opCount} config op(s)`,
|
|
730
|
+
configChange: change,
|
|
731
|
+
}).catch(() => undefined)
|
|
717
732
|
}
|
|
718
733
|
|
|
719
734
|
export const compile = async (app: Application) => {
|
|
720
735
|
await new Promise((resolve) => setImmediate(resolve, 0))
|
|
721
|
-
// Snapshot the
|
|
722
|
-
//
|
|
736
|
+
// Snapshot the prior build artifact before the caller's compile.ts overwrites it, so
|
|
737
|
+
// the config change introduced by this compile can be recorded on return.
|
|
723
738
|
const previousConfig = await readBuildConfig(process.cwd())
|
|
724
739
|
const timestamp = Date.now()
|
|
725
740
|
// Pre-index subspace ids so the canvas-item validation below stays O(1).
|
|
@@ -1433,12 +1448,7 @@ export const compile = async (app: Application) => {
|
|
|
1433
1448
|
: null,
|
|
1434
1449
|
}
|
|
1435
1450
|
|
|
1436
|
-
Object.assign(
|
|
1437
|
-
calc,
|
|
1438
|
-
generateCalulationMap(calc.script_config, {
|
|
1439
|
-
snapshotMode: process.env.BRICKS_SNAPSHOT_MODE === '1',
|
|
1440
|
-
}),
|
|
1441
|
-
)
|
|
1451
|
+
Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
|
|
1442
1452
|
}
|
|
1443
1453
|
map[dataCalcId] = calc
|
|
1444
1454
|
return map
|
|
@@ -1475,7 +1485,7 @@ export const compile = async (app: Application) => {
|
|
|
1475
1485
|
automation_map: compiledAutomationMap || app.metadata?.TEMP_automation_map || {},
|
|
1476
1486
|
update_timestamp: timestamp,
|
|
1477
1487
|
}
|
|
1478
|
-
|
|
1488
|
+
await recordConfigChange(previousConfig, config)
|
|
1479
1489
|
return config
|
|
1480
1490
|
}
|
|
1481
1491
|
|
package/compile/util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeSeededId } from '../utils/id'
|
|
2
2
|
|
|
3
3
|
type ScriptConfig = {
|
|
4
4
|
title?: string
|
|
@@ -33,15 +33,18 @@ const padding = 15
|
|
|
33
33
|
const layerXInterval = 300
|
|
34
34
|
const layerYInterval = 150
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// `calcId` (the owning script calc's stable id) seeds every derived command-node id, so an
|
|
37
|
+
// unchanged calc recompiles to identical ids and editing one calc never shifts another's —
|
|
38
|
+
// keeping the compiled config byte-stable for change detection. See makeSeededId in utils/id.
|
|
39
|
+
export const generateCalulationMap = (config: ScriptConfig, calcId: string) => {
|
|
37
40
|
validateConfig(config)
|
|
38
|
-
const sandboxId =
|
|
39
|
-
const sandboxErrorId =
|
|
40
|
-
const sandboxResultId =
|
|
41
|
+
const sandboxId = makeSeededId('property_bank_command', `${calcId}:sandbox-run`)
|
|
42
|
+
const sandboxErrorId = makeSeededId('property_bank_command', `${calcId}:sandbox-error`)
|
|
43
|
+
const sandboxResultId = makeSeededId('property_bank_command', `${calcId}:sandbox-result`)
|
|
41
44
|
|
|
42
45
|
const inputs = Object.entries(config.inputs).reduce(
|
|
43
46
|
(acc, [key, value], index) => {
|
|
44
|
-
const commandId =
|
|
47
|
+
const commandId = makeSeededId('property_bank_command', `${calcId}:input:${key}`)
|
|
45
48
|
acc.map[key] = {
|
|
46
49
|
type: 'data-node',
|
|
47
50
|
properties: {},
|
|
@@ -123,7 +126,7 @@ export const generateCalulationMap = (config: ScriptConfig, opts?: { snapshotMod
|
|
|
123
126
|
let y = 0
|
|
124
127
|
const outputs = Object.entries(config.outputs).reduce(
|
|
125
128
|
(acc, [key, pbList], index) => {
|
|
126
|
-
const commandId =
|
|
129
|
+
const commandId = makeSeededId('property_bank_command', `${calcId}:output:${key}`)
|
|
127
130
|
acc.commandIdList.push(commandId)
|
|
128
131
|
acc.map[commandId] = {
|
|
129
132
|
type: 'command-node-object',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-project",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.46",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@babel/parser": "7.28.5",
|
|
12
12
|
"@babel/traverse": "7.28.5",
|
|
13
13
|
"@babel/types": "7.28.5",
|
|
14
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
14
|
+
"@fugood/bricks-cli": "^2.25.0-beta.46",
|
|
15
15
|
"@huggingface/gguf": "^0.3.2",
|
|
16
16
|
"@iarna/toml": "^3.0.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
package/package.json.bak
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.46",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@babel/parser": "7.28.5",
|
|
12
12
|
"@babel/traverse": "7.28.5",
|
|
13
13
|
"@babel/types": "7.28.5",
|
|
14
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
14
|
+
"@fugood/bricks-cli": "^2.25.0-beta.46",
|
|
15
15
|
"@huggingface/gguf": "^0.3.2",
|
|
16
16
|
"@iarna/toml": "^3.0.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -46,7 +46,7 @@ These work, but with browser caveats:
|
|
|
46
46
|
| WebView / WebCrawler | Subject to browser CORS — a load/fetch that works on device may be blocked |
|
|
47
47
|
| On-device AI (LLM / STT / VAD / Vector Store / Reranker) | Runs **single-threaded** — far slower than the device, not representative of real latency. Also subject to the model fallbacks below |
|
|
48
48
|
| On-device database (SQLite — `GENERATOR_SQLITE`) | Runs for real on the in-memory WASM `sqlite-vec` build — `execute` / `query` / `transaction` / batch all work. `storageType: file` is transparently treated as in-memory, so nothing persists across reloads (see above) |
|
|
49
|
-
| Scene3D /
|
|
49
|
+
| Scene3D / Sketch / WebRTC | Supported |
|
|
50
50
|
|
|
51
51
|
Feature availability also varies across the device platforms themselves (iOS / tvOS / Android / the desktop OSes). When a deployment targets a specific platform's capability, confirm it on that platform.
|
|
52
52
|
|
|
@@ -89,6 +89,7 @@ So that camera and AI features are usable without device permissions, multi-giga
|
|
|
89
89
|
| Brick / Generator | In the Simulator | Does NOT prove |
|
|
90
90
|
|-------------------|------------------|----------------|
|
|
91
91
|
| Camera (`BRICK_CAMERA`) | A 3D mock canvas, no camera permission prompt. `takePicture` snapshots the canvas; recording produces a placeholder clip | Real camera feed, focus, recording, permission flow |
|
|
92
|
+
| Maps (`BRICK_MAPS`) | A real interactive map on free OpenStreetMap-based tiles — no Google Maps API key needed. Markers, path polyline, the six themes / map types (approximated with free tile sets + CSS tints), and the zoom / pan / navigate / focus / reset / fit actions all work | Google / Apple Maps rendering, exact `customMapStyle` / theme styling (approximated), traffic / buildings / indoors layers, real device geolocation |
|
|
92
93
|
| Thermal Printer (`GENERATOR_THERMAL_PRINTER`) | A simulated printer — `init` / `checkStatus` / `scan` fake per-driver status and discovered devices (ESC/POS, Star, TSC, Castles); `print` renders an approximate on-screen receipt. A bottom-left bubble shows live status with a fault toggle to exercise error wiring. Print results can be exported as PNG via `bricks-cli` (see below) | Real device connection, actual paper output, exact native driver status codes |
|
|
93
94
|
| LLM (`GENERATOR_LLM`) | Swapped to a tiny local stand-in model | Output quality / latency of your real model |
|
|
94
95
|
| Reranker — GGML (`GENERATOR_RERANKER`) | Swapped to a small local multilingual reranker model | Ranking quality / latency of your real model |
|
|
@@ -119,7 +120,7 @@ The PNG is the same approximate receipt the on-screen preview shows (rendered fr
|
|
|
119
120
|
|
|
120
121
|
### Running the real implementation instead
|
|
121
122
|
|
|
122
|
-
Each substituted brick/generator can be switched back to its real implementation per item: open the **gear (Simulator settings)** in the editor's preview toolbar, uncheck the item, and **Apply**. Apply persists the choice and reloads the preview so it takes effect (a plain refresh won't). Use this to, e.g., point a Vector Store at a real API key in the preview. The browser limits above still apply, and **Buttress stays disabled regardless** — there's no backend for it here.
|
|
123
|
+
Each substituted brick/generator can be switched back to its real implementation per item: open the **gear (Simulator settings)** in the editor's preview toolbar, uncheck the item, and **Apply**. Apply persists the choice and reloads the preview so it takes effect (a plain refresh won't). Use this to, e.g., point a Vector Store at a real API key in the preview, or render the real Google/Apple Maps brick (which needs a Maps API key on web). The browser limits above still apply, and **Buttress stays disabled regardless** — there's no backend for it here.
|
|
123
124
|
|
|
124
125
|
The Thermal Printer is the exception: it has no real web implementation to switch to (the native drivers can't run in a browser), so it is **always simulated** and is not in the gear list.
|
|
125
126
|
|
|
@@ -71,6 +71,8 @@ Useful flags:
|
|
|
71
71
|
|
|
72
72
|
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.
|
|
73
73
|
|
|
74
|
+
To inspect Data / Property Bank or storage state, prefer the dedicated `bricks devtools storage` subcommands — `storage data-bank get <S_xxxx>` (saved Data values), `storage system persist|memory`, `storage system get <key>` — over hand-written `runtime eval`. Reach for `runtime eval` only for *live* store internals that aren't persisted to a data bank (e.g. a current transient value: `runtime eval "system.data.property('S_xxxx', '<alias>')"`); don't reverse-engineer the `system.*` globals.
|
|
75
|
+
|
|
74
76
|
### Project Automations
|
|
75
77
|
|
|
76
78
|
E2E tests authored in TypeScript inside the project (`AutomationTest` / `TestCase`). Test cases include:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const auditLogIgnoreEntry = '.bricks/edits.jsonl'
|
|
5
|
+
|
|
6
|
+
// Ensure the project's .gitignore excludes the audit log. No-op when it is already
|
|
7
|
+
// ignored directly or via a broader `.bricks` rule.
|
|
8
|
+
const ensureAuditLogIgnored = async (projectDir: string) => {
|
|
9
|
+
const gitignorePath = path.join(projectDir, '.gitignore')
|
|
10
|
+
const content = await readFile(gitignorePath, 'utf8').catch((err: any) => {
|
|
11
|
+
if (err?.code === 'ENOENT') return ''
|
|
12
|
+
throw err
|
|
13
|
+
})
|
|
14
|
+
const ignored = content
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.map((line) => line.trim())
|
|
17
|
+
.some((line) => line === auditLogIgnoreEntry || line === '.bricks/' || line === '.bricks')
|
|
18
|
+
if (ignored) return
|
|
19
|
+
|
|
20
|
+
const prefix = content && !content.endsWith('\n') ? '\n' : ''
|
|
21
|
+
await writeFile(
|
|
22
|
+
gitignorePath,
|
|
23
|
+
`${content}${prefix}\n# MCP entry-editing audit log\n${auditLogIgnoreEntry}\n`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Append one JSON record to `.bricks/edits.jsonl`, creating the directory and the
|
|
28
|
+
// gitignore entry as needed. Shared by the source-editing tools and `compile()` so
|
|
29
|
+
// every project mutation lands in the same audit log.
|
|
30
|
+
export const appendEditRecord = async (projectDir: string, record: Record<string, unknown>) => {
|
|
31
|
+
const bricksDir = path.join(projectDir, '.bricks')
|
|
32
|
+
await mkdir(bricksDir, { recursive: true })
|
|
33
|
+
await ensureAuditLogIgnored(projectDir)
|
|
34
|
+
await appendFile(path.join(bricksDir, 'edits.jsonl'), `${JSON.stringify(record)}\n`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Provenance stamped on every audit record: which agent/session produced the change.
|
|
38
|
+
export const editProvenance = () => ({
|
|
39
|
+
session: process.env.BRICKS_CTOR_SESSION_ID || process.env.CODEX_SESSION_ID,
|
|
40
|
+
agent: process.env.BRICKS_CTOR_AGENT_ID || process.env.USER,
|
|
41
|
+
})
|
package/tools/mcp-env.ts
CHANGED
|
@@ -9,5 +9,5 @@ export function isTruthyEnv(value: string | undefined) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function shouldRegisterEditingTools(env: Record<string, string | undefined> = process.env) {
|
|
12
|
-
return isTruthyEnv(env.
|
|
12
|
+
return isTruthyEnv(env.BRICKS_CTOR_ENABLE_EDITING_TOOLS)
|
|
13
13
|
}
|
|
@@ -17,7 +17,12 @@ export const compileAndDiff = async (projectDir: string): Promise<VerificationRe
|
|
|
17
17
|
// Snapshot the prior compiled config before recompiling (the new artifact overwrites it).
|
|
18
18
|
const before = await readBuildConfig(projectDir)
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
// Turn off the editing-tools audit for this spawned compile so compile() doesn't write
|
|
21
|
+
// a duplicate generic entry — the calling editing tool records its own richer entry.
|
|
22
|
+
await sh`bun compile`
|
|
23
|
+
.cwd(projectDir)
|
|
24
|
+
.env({ ...noColorEnv, BRICKS_CTOR_ENABLE_EDITING_TOOLS: '0' })
|
|
25
|
+
.text()
|
|
21
26
|
} catch (err: any) {
|
|
22
27
|
const stdout = err.stdout?.toString() ?? ''
|
|
23
28
|
const stderr = err.stderr?.toString() ?? ''
|
|
@@ -10,9 +10,9 @@ const noColorEnv = { FORCE_COLOR: '0', NO_COLOR: '1' }
|
|
|
10
10
|
export function register(server: McpServer, projectDir: string) {
|
|
11
11
|
const { dirname } = import.meta
|
|
12
12
|
|
|
13
|
+
// `bun compile` records the resulting config delta to `.bricks/edits.jsonl` itself
|
|
14
|
+
// (see compile() in compile/index.ts), so the spawned output below already reflects it.
|
|
13
15
|
server.tool('compile', {}, async () => {
|
|
14
|
-
// The config-change report is printed by compile() itself (compile/config-diff.ts),
|
|
15
|
-
// so the spawned `bun compile` output below already carries it.
|
|
16
16
|
let log = 'Type checking & Compiling...\n'
|
|
17
17
|
try {
|
|
18
18
|
log += await sh`bun compile`.cwd(projectDir).env(noColorEnv).text()
|
|
@@ -4,16 +4,15 @@ import * as t from '@babel/types'
|
|
|
4
4
|
import { parse as parseSandboxModule } from 'acorn'
|
|
5
5
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
6
6
|
import { format as formatWithOxfmt } from 'oxfmt'
|
|
7
|
-
import {
|
|
7
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { z } from 'zod'
|
|
10
10
|
|
|
11
11
|
import { verifyProject } from './_verify'
|
|
12
|
+
import { appendEditRecord, editProvenance } from '../_edits-log'
|
|
12
13
|
|
|
13
14
|
const generate = (generateModule as any).default || generateModule
|
|
14
15
|
|
|
15
|
-
const auditLogIgnoreEntry = '.bricks/edits.jsonl'
|
|
16
|
-
|
|
17
16
|
const oxfmtOptions = {
|
|
18
17
|
trailingComma: 'all',
|
|
19
18
|
tabWidth: 2,
|
|
@@ -646,32 +645,6 @@ const writeParsedFile = async (parsed: ParsedFile) => {
|
|
|
646
645
|
return code
|
|
647
646
|
}
|
|
648
647
|
|
|
649
|
-
const ensureAuditLogIgnored = async (projectDir: string) => {
|
|
650
|
-
const gitignorePath = path.join(projectDir, '.gitignore')
|
|
651
|
-
const content = await readFile(gitignorePath, 'utf8').catch((err: any) => {
|
|
652
|
-
if (err?.code === 'ENOENT') return ''
|
|
653
|
-
throw err
|
|
654
|
-
})
|
|
655
|
-
const ignored = content
|
|
656
|
-
.split(/\r?\n/)
|
|
657
|
-
.map((line) => line.trim())
|
|
658
|
-
.some((line) => line === auditLogIgnoreEntry || line === '.bricks/' || line === '.bricks')
|
|
659
|
-
if (ignored) return
|
|
660
|
-
|
|
661
|
-
const prefix = content && !content.endsWith('\n') ? '\n' : ''
|
|
662
|
-
await writeFile(
|
|
663
|
-
gitignorePath,
|
|
664
|
-
`${content}${prefix}\n# MCP entry-editing audit log\n${auditLogIgnoreEntry}\n`,
|
|
665
|
-
)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const appendEditRecord = async (projectDir: string, record: Record<string, unknown>) => {
|
|
669
|
-
const bricksDir = path.join(projectDir, '.bricks')
|
|
670
|
-
await mkdir(bricksDir, { recursive: true })
|
|
671
|
-
await ensureAuditLogIgnored(projectDir)
|
|
672
|
-
await appendFile(path.join(bricksDir, 'edits.jsonl'), `${JSON.stringify(record)}\n`)
|
|
673
|
-
}
|
|
674
|
-
|
|
675
648
|
const responseFor = (result: any): any => ({
|
|
676
649
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
677
650
|
isError: result.isError || undefined,
|
|
@@ -692,10 +665,7 @@ const runOperation = async (
|
|
|
692
665
|
operation: () => Promise<any>,
|
|
693
666
|
) => {
|
|
694
667
|
const startedAt = new Date().toISOString()
|
|
695
|
-
const provenance =
|
|
696
|
-
session: process.env.BRICKS_CTOR_SESSION_ID || process.env.CODEX_SESSION_ID,
|
|
697
|
-
agent: process.env.BRICKS_CTOR_AGENT_ID || process.env.USER,
|
|
698
|
-
}
|
|
668
|
+
const provenance = editProvenance()
|
|
699
669
|
try {
|
|
700
670
|
const result = await operation()
|
|
701
671
|
await appendEditRecord(projectDir, {
|
|
@@ -5,17 +5,16 @@ import * as t from '@babel/types'
|
|
|
5
5
|
import { parse as parseRuntimeExpression } from 'acorn'
|
|
6
6
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
7
7
|
import { format as formatWithOxfmt } from 'oxfmt'
|
|
8
|
-
import {
|
|
8
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import { z } from 'zod'
|
|
11
11
|
|
|
12
12
|
import { verifyProject } from './_verify'
|
|
13
|
+
import { appendEditRecord, editProvenance } from '../_edits-log'
|
|
13
14
|
|
|
14
15
|
const generate = (generateModule as any).default || generateModule
|
|
15
16
|
const traverse = (traverseModule as any).default || traverseModule
|
|
16
17
|
|
|
17
|
-
const auditLogIgnoreEntry = '.bricks/edits.jsonl'
|
|
18
|
-
|
|
19
18
|
const oxfmtOptions = {
|
|
20
19
|
trailingComma: 'all',
|
|
21
20
|
tabWidth: 2,
|
|
@@ -894,32 +893,6 @@ const writeParsedFile = async (parsed: ParsedFile) => {
|
|
|
894
893
|
return code
|
|
895
894
|
}
|
|
896
895
|
|
|
897
|
-
const appendEditRecord = async (projectDir: string, record: Record<string, unknown>) => {
|
|
898
|
-
const bricksDir = path.join(projectDir, '.bricks')
|
|
899
|
-
await mkdir(bricksDir, { recursive: true })
|
|
900
|
-
await ensureAuditLogIgnored(projectDir)
|
|
901
|
-
await appendFile(path.join(bricksDir, 'edits.jsonl'), `${JSON.stringify(record)}\n`)
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const ensureAuditLogIgnored = async (projectDir: string) => {
|
|
905
|
-
const gitignorePath = path.join(projectDir, '.gitignore')
|
|
906
|
-
const content = await readFile(gitignorePath, 'utf8').catch((err: any) => {
|
|
907
|
-
if (err?.code === 'ENOENT') return ''
|
|
908
|
-
throw err
|
|
909
|
-
})
|
|
910
|
-
const ignored = content
|
|
911
|
-
.split(/\r?\n/)
|
|
912
|
-
.map((line) => line.trim())
|
|
913
|
-
.some((line) => line === auditLogIgnoreEntry || line === '.bricks/' || line === '.bricks')
|
|
914
|
-
if (ignored) return
|
|
915
|
-
|
|
916
|
-
const prefix = content && !content.endsWith('\n') ? '\n' : ''
|
|
917
|
-
await writeFile(
|
|
918
|
-
gitignorePath,
|
|
919
|
-
`${content}${prefix}\n# MCP entry-editing audit log\n${auditLogIgnoreEntry}\n`,
|
|
920
|
-
)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
896
|
const summarizeTarget = (parsed?: ParsedFile, entry?: ExportEntry) => {
|
|
924
897
|
if (!parsed) return undefined
|
|
925
898
|
const subspace = getSubspaceLabelFromPath(parsed.absPath)
|
|
@@ -941,10 +914,7 @@ const runOperation = async (
|
|
|
941
914
|
entry: input?.entry,
|
|
942
915
|
id: input?.id,
|
|
943
916
|
},
|
|
944
|
-
provenance:
|
|
945
|
-
session: process.env.BRICKS_CTOR_SESSION_ID || process.env.CODEX_SESSION_ID,
|
|
946
|
-
agent: process.env.BRICKS_CTOR_AGENT_ID || process.env.USER,
|
|
947
|
-
},
|
|
917
|
+
provenance: editProvenance(),
|
|
948
918
|
}
|
|
949
919
|
|
|
950
920
|
try {
|
|
@@ -250,7 +250,7 @@ export type DataCommandColorRandom = DataCommand & {
|
|
|
250
250
|
outputs?: Array<DataCalcOutput<'result'> /* target: string */>
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/* RGBA — Generate color
|
|
253
|
+
/* RGBA — Generate a color from RGBA channels (red/green/blue: 0-255, alpha: 0-1) */
|
|
254
254
|
export type DataCommandColorRgba = DataCommand & {
|
|
255
255
|
__commandName: 'COLOR_RGBA'
|
|
256
256
|
inputs?: Array<
|
|
@@ -22,7 +22,7 @@ export type DataCommandDatetimeDate = DataCommand & {
|
|
|
22
22
|
outputs?: Array<DataCalcOutput<'result'> /* target: number */>
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/* Day — Get day (Sunday is
|
|
25
|
+
/* Day — Get day of week (0-based: Sunday is 0, Saturday is 6) */
|
|
26
26
|
export type DataCommandDatetimeDay = DataCommand & {
|
|
27
27
|
__commandName: 'DATETIME_DAY'
|
|
28
28
|
inputs?: Array<
|
|
@@ -64,7 +64,7 @@ export type DataCommandDatetimeMinute = DataCommand & {
|
|
|
64
64
|
outputs?: Array<DataCalcOutput<'result'> /* target: number */>
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/* Month — Get month (January is
|
|
67
|
+
/* Month — Get month (0-based: January is 0, December is 11) */
|
|
68
68
|
export type DataCommandDatetimeMonth = DataCommand & {
|
|
69
69
|
__commandName: 'DATETIME_MONTH'
|
|
70
70
|
inputs?: Array<
|
package/utils/calc.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const generateDataCalculationMapEditorInfo = (
|
|
|
28
28
|
DataCalculationData | DataCommand,
|
|
29
29
|
Set<DataCalculationData | DataCommand>
|
|
30
30
|
>()
|
|
31
|
+
const nodeById = new Map<string, DataCalculationData | DataCommand>()
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
if ('id' in node) nodeById.set(node.id, node)
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
// Analyze node connections
|
|
33
37
|
nodes.forEach((node) => {
|
|
@@ -48,7 +52,7 @@ export const generateDataCalculationMapEditorInfo = (
|
|
|
48
52
|
if (!connectedTo.has(node)) {
|
|
49
53
|
connectedTo.set(node, new Set())
|
|
50
54
|
}
|
|
51
|
-
const sourceNode =
|
|
55
|
+
const sourceNode = nodeById.get(conn.id)
|
|
52
56
|
if (sourceNode) {
|
|
53
57
|
connectedTo.get(node)!.add(sourceNode)
|
|
54
58
|
}
|
package/utils/id.ts
CHANGED
|
@@ -69,63 +69,65 @@ const makeStableUuid = (type: string, alias?: string) => {
|
|
|
69
69
|
})
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
export const makeId = (type: IdType, aliasOrOpts?: string | IdOptions, opts?: IdOptions) => {
|
|
74
|
-
if (type === 'subspace') {
|
|
75
|
-
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const alias = typeof aliasOrOpts === 'string' ? aliasOrOpts : undefined
|
|
79
|
-
const options = typeof aliasOrOpts === 'string' ? opts : (aliasOrOpts ?? opts)
|
|
80
|
-
|
|
81
|
-
let prefix = ''
|
|
72
|
+
const idPrefix = (type: IdType): string => {
|
|
82
73
|
switch (type) {
|
|
83
74
|
case 'animation':
|
|
84
|
-
|
|
85
|
-
break
|
|
75
|
+
return 'ANIMATION_'
|
|
86
76
|
case 'brick':
|
|
87
|
-
|
|
88
|
-
break
|
|
77
|
+
return 'BRICK_'
|
|
89
78
|
case 'dynamic-brick':
|
|
90
|
-
|
|
91
|
-
break
|
|
79
|
+
return 'DYNAMIC_BRICK_'
|
|
92
80
|
case 'canvas':
|
|
93
|
-
|
|
94
|
-
break
|
|
81
|
+
return 'CANVAS_'
|
|
95
82
|
case 'generator':
|
|
96
|
-
|
|
97
|
-
break
|
|
83
|
+
return 'GENERATOR_'
|
|
98
84
|
case 'data':
|
|
99
|
-
|
|
100
|
-
break
|
|
85
|
+
return 'PROPERTY_BANK_DATA_NODE_'
|
|
101
86
|
case 'switch':
|
|
102
|
-
|
|
103
|
-
break
|
|
87
|
+
return 'BRICK_STATE_GROUP_'
|
|
104
88
|
case 'property_bank_command':
|
|
105
|
-
|
|
106
|
-
break
|
|
89
|
+
return 'PROPERTY_BANK_COMMAND_NODE_'
|
|
107
90
|
case 'property_bank_calc':
|
|
108
|
-
|
|
109
|
-
break
|
|
91
|
+
return 'PROPERTY_BANK_COMMAND_MAP_'
|
|
110
92
|
case 'automation_map':
|
|
111
|
-
|
|
112
|
-
break
|
|
93
|
+
return 'AUTOMATION_MAP_'
|
|
113
94
|
case 'test':
|
|
114
|
-
|
|
115
|
-
break
|
|
95
|
+
return 'TEST_'
|
|
116
96
|
case 'test_case':
|
|
117
|
-
|
|
118
|
-
break
|
|
97
|
+
return 'TEST_CASE_'
|
|
119
98
|
case 'test_var':
|
|
120
|
-
|
|
121
|
-
break
|
|
99
|
+
return 'TEST_VAR_'
|
|
122
100
|
default:
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Make stable ids by default; explicit snapshotMode: false preserves the random escape hatch.
|
|
106
|
+
export const makeId = (type: IdType, aliasOrOpts?: string | IdOptions, opts?: IdOptions) => {
|
|
107
|
+
if (type === 'subspace') {
|
|
108
|
+
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
123
109
|
}
|
|
124
110
|
|
|
111
|
+
const alias = typeof aliasOrOpts === 'string' ? aliasOrOpts : undefined
|
|
112
|
+
const options = typeof aliasOrOpts === 'string' ? opts : (aliasOrOpts ?? opts)
|
|
113
|
+
|
|
125
114
|
const useCountFallback = aliasOrOpts === undefined && opts === undefined
|
|
126
115
|
const id =
|
|
127
116
|
alias !== undefined || options?.snapshotMode || useCountFallback
|
|
128
117
|
? makeStableUuid(type, alias)
|
|
129
118
|
: uuid()
|
|
130
|
-
return `${
|
|
119
|
+
return `${idPrefix(type)}${id}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Deterministic id derived solely from a caller-supplied seed. Unlike makeId it keeps no
|
|
123
|
+
// global state — no incrementing counter, no alias registry — so the same (type, seed)
|
|
124
|
+
// always maps to the same id on every compile and in any process. Compiled artifacts seed
|
|
125
|
+
// their ids this way (e.g. generateCalulationMap's command nodes, seeded by their calc id +
|
|
126
|
+
// structural role) so unchanged source recompiles byte-identically and editing one calc
|
|
127
|
+
// never shifts another's ids. The seed must be unique within a single config.
|
|
128
|
+
export const makeSeededId = (type: IdType, seed: string) => {
|
|
129
|
+
if (type === 'subspace') {
|
|
130
|
+
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
131
|
+
}
|
|
132
|
+
return `${idPrefix(type)}${uuid({ random: hashToRandomBytes([readApplicationId(), type, seed]) })}`
|
|
131
133
|
}
|