@christianirack/brainllm-runtime 0.0.0-stage → 0.3.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/brain-ai.js +129 -0
  4. package/brain-anim.js +21 -0
  5. package/brain-ch.js +254 -0
  6. package/brain-consent.js +38 -0
  7. package/brain-data.js +549 -0
  8. package/brain-dev.js +100 -0
  9. package/brain-env.js +160 -0
  10. package/brain-err.js +242 -0
  11. package/brain-form.js +275 -0
  12. package/brain-guard.js +242 -0
  13. package/brain-hydrate.js +72 -0
  14. package/brain-i18n.js +111 -0
  15. package/brain-inspect.js +745 -0
  16. package/brain-island.js +106 -0
  17. package/brain-lazy.js +88 -0
  18. package/brain-mcp.js +73 -0
  19. package/brain-mock.js +80 -0
  20. package/brain-overlay.js +123 -0
  21. package/brain-persist.js +65 -0
  22. package/brain-router.js +503 -0
  23. package/brain-rpc.js +20 -0
  24. package/brain-schema.js +93 -0
  25. package/brain-wc.js +69 -0
  26. package/brain-why.js +198 -0
  27. package/brain-worker.js +66 -0
  28. package/brain.js +421 -0
  29. package/package.json +20 -4
  30. package/server/server.mjs +1046 -0
  31. package/shim/dom.js +448 -0
  32. package/shim/probe.js +111 -0
  33. package/sw/brain-sw.js +103 -0
  34. package/themes/contrast.cssbrain +39 -0
  35. package/themes/dark.cssbrain +39 -0
  36. package/themes/glass.cssbrain +39 -0
  37. package/themes/paper.cssbrain +39 -0
  38. package/ui/button.brain +31 -0
  39. package/ui/center.brain +11 -0
  40. package/ui/checkbox.brain +13 -0
  41. package/ui/container.brain +11 -0
  42. package/ui/dialog.brain +29 -0
  43. package/ui/error-state.brain +53 -0
  44. package/ui/field.brain +34 -0
  45. package/ui/grid.brain +11 -0
  46. package/ui/input.brain +23 -0
  47. package/ui/lang-switch.brain +38 -0
  48. package/ui/menu.brain +64 -0
  49. package/ui/row.brain +12 -0
  50. package/ui/select.brain +29 -0
  51. package/ui/sidebar.brain +17 -0
  52. package/ui/spacer.brain +12 -0
  53. package/ui/stack.brain +10 -0
  54. package/ui/table.brain +73 -0
  55. package/ui/tabs.brain +33 -0
  56. package/ui/toast.brain +45 -0
package/brain-wc.js ADDED
@@ -0,0 +1,69 @@
1
+ // BRAINLLM Web Components (SPEC §6.2): define(tag, Comp, css) exports a component as a Custom Element.
2
+ // Imports ./brain.js relatively. Props = attributes from Comp.p (string) or JS properties (any value).
3
+ import { sig, mount, _ev } from './brain.js'
4
+
5
+ const sheets = new Map()
6
+ // Events that do not cross the shadow root (composed: false): also delegated on each shadow root.
7
+ const LOCAL = ['change', 'submit', 'reset', 'select', 'toggle', 'invalid']
8
+
9
+ export function define(tag, C, css = '') {
10
+ const P = C.p || []
11
+ class E extends HTMLElement {
12
+ static observedAttributes = P
13
+ static formAssociated = !!C.form
14
+ constructor() {
15
+ super()
16
+ this.s = {}
17
+ for (const k of P) this.s[k] = sig(this.getAttribute(k))
18
+ this.i = C.form ? this.attachInternals() : null
19
+ this.x = null
20
+ }
21
+ attributeChangedCallback(k, _, v) {
22
+ this.s[k].v = v
23
+ }
24
+ connectedCallback() {
25
+ if (this.x) return
26
+ const r = this.shadowRoot || this.attachShadow({ mode: 'open' }) // reuses Declarative Shadow DOM
27
+ let sh = sheets.get(tag)
28
+ if (!sh) sheets.set(tag, (sh = new CSSStyleSheet())), sh.replaceSync(css)
29
+ r.adoptedStyleSheets = [sh]
30
+ if (!r.$w) {
31
+ r.$w = 1
32
+ for (const t of LOCAL) r.addEventListener(t, _ev)
33
+ }
34
+ r.textContent = '' // v0: DSD content is re-rendered, same as hydrate
35
+ const el = this
36
+ const $p = new Proxy(
37
+ {},
38
+ {
39
+ get: (_, k) =>
40
+ k in el.s
41
+ ? el.s[k].v
42
+ : k === '$internals'
43
+ ? el.i
44
+ : typeof k === 'string' && k.startsWith('on:')
45
+ ? d => el.dispatchEvent(new CustomEvent(k.slice(3), { detail: d, bubbles: true, composed: true }))
46
+ : undefined,
47
+ }
48
+ )
49
+ this.x = mount(C, r, $p)
50
+ }
51
+ disconnectedCallback() {
52
+ // in a microtask: moving the element (disconnect and reconnect) does not destroy it
53
+ queueMicrotask(() => {
54
+ if (!this.isConnected && this.x) this.x(), (this.x = null)
55
+ })
56
+ }
57
+ }
58
+ for (const k of P)
59
+ Object.defineProperty(E.prototype, k, {
60
+ get() {
61
+ return this.s[k].v
62
+ },
63
+ set(v) {
64
+ this.s[k].v = v
65
+ },
66
+ })
67
+ customElements.define(tag, E)
68
+ return E
69
+ }
package/brain-why.js ADDED
@@ -0,0 +1,198 @@
1
+ // BRAINLLM update provenance (INTERNALS §43.2-§43.1, #198). Development only: the dev client loads it next to
2
+ // brain-dev.js; nothing ships to production and the core carries no hook for it.
3
+ // - why(selector | node) → for a node that updated: which signal write caused each change (component, field,
4
+ // `file.brain:line` of the write) and the chain of drv between them. MCP `brain_why` (TOOLCHAIN #200) calls it
5
+ // through globalThis.__brainWhy.
6
+ // - interactions() → per user interaction (click, input, change, keydown, submit): how many DOM mutations it caused
7
+ // and on how many nodes, for the impact report (`brain_impact`, "updates per interaction").
8
+ // How: it wraps, in dev only, the signal setter (records the write and its stack), the node run (an effect run
9
+ // knows, before it re-runs, which of its sources were written: those are its causes) and the DOM write methods
10
+ // (each mutation made while an effect runs is attributed to that effect's causes).
11
+ import { sig } from './brain.js'
12
+ import { names, mounts as M } from './brain-dev.js'
13
+
14
+ const G = globalThis
15
+ const N = sig(0).constructor, P = N.prototype
16
+ const KEEP = 10, LAST = 50
17
+ const WR = new Map() // sig → write record, for the current flush (cleared once it settles)
18
+ const BY = new WeakMap() // node → [mutation records], newest first
19
+ const INT = [] // last interactions
20
+ let RUN = null // causes of the effect running now
21
+ let cur = null // interaction being measured
22
+ let clearing = 0
23
+
24
+ // ---------- signal writes ----------
25
+ const vd = Object.getOwnPropertyDescriptor(P, 'v')
26
+ Object.defineProperty(P, 'v', {
27
+ configurable: true,
28
+ get: vd.get,
29
+ set(x) {
30
+ if (!this.f && !Object.is(x, this._)) {
31
+ const info = names.get(this)
32
+ WR.set(this, { sig: this, name: (info && info.name) || null, comp: (info && info.comp) || null, from: this._, to: x, stack: new Error().stack || '' })
33
+ if (!clearing) (clearing = 1), setTimeout(() => ((clearing = 0), WR.clear()), 0) // after the flush microtasks
34
+ }
35
+ vd.set.call(this, x)
36
+ },
37
+ })
38
+
39
+ // causes of a node's next run: its sources that were written, or reached through a drv chain
40
+ function causesOf(n, via = [], seen = new Set()) {
41
+ const out = [], S = n.S
42
+ if (!S) return out
43
+ for (let i = 0; i < S.length; i += 2) {
44
+ const s = S[i]
45
+ if (seen.has(s)) continue
46
+ seen.add(s)
47
+ const w = WR.get(s)
48
+ if (w) out.push({ w, via })
49
+ else if (s.f && !s.e) out.push(...causesOf(s, [...via, (names.get(s) && names.get(s).name) || 'drv'], seen))
50
+ }
51
+ return out
52
+ }
53
+ const r0 = P.r
54
+ P.r = function () {
55
+ if (!this.e || !WR.size) return r0.call(this)
56
+ const prev = RUN
57
+ RUN = causesOf(this)
58
+ try {
59
+ return r0.call(this)
60
+ } finally {
61
+ RUN = prev
62
+ }
63
+ }
64
+
65
+ // ---------- DOM writes ----------
66
+ function note(node, kind) {
67
+ if (!node || !RUN || !RUN.length) return
68
+ const rec = { kind, t: Date.now(), causes: RUN }
69
+ for (const n of node.nodeType === 3 && node.parentNode ? [node, node.parentNode] : [node]) {
70
+ const a = BY.get(n) || (BY.set(n, []), BY.get(n))
71
+ a.unshift(rec)
72
+ a.length = Math.min(a.length, KEEP)
73
+ }
74
+ if (cur) cur.mutations++, cur.nodes.add(node)
75
+ }
76
+ const owner = (o, k) => {
77
+ for (; o; o = Object.getPrototypeOf(o)) {
78
+ const d = Object.getOwnPropertyDescriptor(o, k)
79
+ if (d) return [o, d]
80
+ }
81
+ return []
82
+ }
83
+ function wrap(sample, k, kind) {
84
+ const [o, d] = owner(sample, k)
85
+ if (!o || (d.value && d.value.$why) || (d.set && d.set.$why)) return
86
+ if (typeof d.value === 'function') {
87
+ const f = d.value
88
+ const g = function (...a) {
89
+ note(kind.node ? kind.node(this, a) : this, typeof kind === 'function' ? kind(a) : kind.k(a))
90
+ return f.apply(this, a)
91
+ }
92
+ g.$why = 1
93
+ Object.defineProperty(o, k, { ...d, value: g })
94
+ } else if (d.set) {
95
+ const set = function (v) {
96
+ note(this, kind)
97
+ d.set.call(this, v)
98
+ }
99
+ set.$why = 1
100
+ Object.defineProperty(o, k, { ...d, set })
101
+ }
102
+ }
103
+ function install() {
104
+ const d = G.document
105
+ if (!d || G.__brainWhy) return
106
+ const el = d.createElement('div'), tx = d.createTextNode(''), cm = d.createComment('')
107
+ wrap(el, 'setAttribute', a => 'attr:' + a[0])
108
+ wrap(el, 'removeAttribute', a => 'attr:' + a[0])
109
+ wrap(el, 'textContent', 'text')
110
+ wrap(tx, 'data', 'text')
111
+ wrap(tx, 'nodeValue', 'text')
112
+ wrap(el, 'insertBefore', { node: (_, a) => a[0], k: () => 'insert' })
113
+ wrap(el, 'appendChild', { node: (_, a) => a[0], k: () => 'insert' })
114
+ wrap(el, 'removeChild', { node: (_, a) => a[0], k: () => 'remove' })
115
+ for (const n of [el, tx, cm]) wrap(n, 'remove', () => 'remove')
116
+ for (const k of ['value', 'checked', 'selected']) wrap(d.createElement('input'), k, 'prop:' + k)
117
+ // classList / style have no link back to their element: map them through the element's getter (browsers)
118
+ for (const [k, m] of [['classList', 'toggle'], ['style', 'setProperty']]) {
119
+ const [o, dd] = owner(el, k)
120
+ if (!o || !dd.get) continue
121
+ const back = new WeakMap()
122
+ Object.defineProperty(o, k, { ...dd, get() { const v = dd.get.call(this); back.set(v, this); return v } })
123
+ const [po, pd] = owner(el[k], m)
124
+ if (po && typeof pd.value === 'function') {
125
+ const f = pd.value
126
+ Object.defineProperty(po, m, { ...pd, value(...a) { note(back.get(this), (k === 'style' ? 'style:' : 'class:') + a[0]); return f.apply(this, a) } })
127
+ }
128
+ }
129
+ // interactions: everything a user event causes until the updates settle
130
+ for (const t of ['click', 'input', 'change', 'keydown', 'submit'])
131
+ d.addEventListener(t, e => {
132
+ if (cur) return
133
+ cur = { type: t, target: describe(e.target), mutations: 0, nodes: new Set(), t0: G.performance ? performance.now() : Date.now() }
134
+ setTimeout(() => {
135
+ const c = cur
136
+ cur = null
137
+ INT.unshift({ type: c.type, target: c.target, mutations: c.mutations, nodes: c.nodes.size, ms: Math.round(((G.performance ? performance.now() : Date.now()) - c.t0) * 10) / 10 })
138
+ INT.length = Math.min(INT.length, LAST)
139
+ }, 0)
140
+ }, true)
141
+ G.__brainWhy = { why, interactions, mounts, clear }
142
+ }
143
+ const describe = n => {
144
+ if (!n || n.nodeType !== 1) return n && n.parentNode ? describe(n.parentNode) : null
145
+ const id = n.getAttribute('id'), c = n.getAttribute('class')
146
+ return n.tagName.toLowerCase() + (id ? '#' + id : '') + (c ? '.' + c.trim().split(/\s+/).join('.') : '')
147
+ }
148
+
149
+ // ---------- answers ----------
150
+ const RT = /\/(@brain|_b)\/brain[\w-]*\.js|\/runtime\/brain[\w-]*\.js|\/brain-why\.js/
151
+ const MAPS = new Map()
152
+ // the first app frame of the write's stack, as file.brain:line when the dev module carries its source map
153
+ async function where(stack) {
154
+ for (const l of String(stack).split('\n').slice(1)) {
155
+ const m = /\(?((?:https?|file):\/\/[^\s)]+?):(\d+):\d+\)?\s*$/.exec(l)
156
+ if (!m || RT.test(m[1])) continue
157
+ const url = m[1], line = +m[2]
158
+ if (!MAPS.has(url)) MAPS.set(url, load(url))
159
+ const lines = await MAPS.get(url)
160
+ const path = url.replace(/^\w+:\/\/[^/]*/, '').replace(/^\//, '').replace(/\?.*$/, '')
161
+ return lines && lines[line - 1] ? `${path}:${lines[line - 1]}` : `${path}:${line}`
162
+ }
163
+ return null
164
+ }
165
+ async function load(url) {
166
+ try {
167
+ if (!/^https?:/.test(url) || !G.fetch) return null
168
+ const t = await (await fetch(url)).text()
169
+ const m = /sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/.exec(t)
170
+ if (!m) return null
171
+ const { decode } = await import('./brain-err.js')
172
+ return decode(JSON.parse(atob(m[1])).mappings)
173
+ } catch {
174
+ return null
175
+ }
176
+ }
177
+ const val = v => (typeof v === 'function' ? 'fn' : v && typeof v === 'object' ? JSON.stringify(v).slice(0, 80) : v)
178
+
179
+ // why('#total') → [{ kind: 'text', at, causes: [{ signal: 'cart.total', comp: 'views/cart.brain', src:
180
+ // 'views/cart.brain:12', from, to, via: ['subtotal'] }] }], newest first; [] when that node never updated
181
+ export async function why(target) {
182
+ const n = typeof target === 'string' ? G.document && document.querySelector(target) : target
183
+ if (!n) return { error: `no element matches ${target}` }
184
+ const recs = [...(BY.get(n) || [])]
185
+ return Promise.all(
186
+ recs.map(async r => ({
187
+ kind: r.kind,
188
+ at: r.t,
189
+ causes: await Promise.all(r.causes.map(async c => ({ signal: c.w.name, comp: c.w.comp, src: await where(c.w.stack), from: val(c.w.from), to: val(c.w.to), via: c.via }))),
190
+ }))
191
+ )
192
+ }
193
+ export const interactions = () => INT.slice()
194
+ // last mounts, newest first: [{ comp: 'comp/x.brain', ms, at, hmr }] (brain-dev measures each one)
195
+ export const mounts = () => M.slice()
196
+ export const clear = () => (INT.length = 0)
197
+
198
+ install()
@@ -0,0 +1,66 @@
1
+ // BRAINLLM Web Worker bridge (INTERNALS §35.5). The compiler turns `*.worker.jsbrain` into a worker chunk that ends in
2
+ // `serve({ parseCsv, default: $default })`, and every importer into `const w = worker(new URL('…', import.meta.url))`
3
+ // plus `const parseCsv = (...a) => w.call('parseCsv', a)`. Messages use structured clone: { id, name, args } →
4
+ // { id, ok } | { id, err: { message, name, stack } }. The worker starts on the first call; a remote error is rethrown
5
+ // in the page as an Error with the same message and name (its stack in `remoteStack`).
6
+
7
+ // Worker side. `name` may be a path into the map ('default.mean'); the result may be a promise.
8
+ export function serve(map, scope = globalThis) {
9
+ scope.onmessage = async e => {
10
+ const { id, name, args } = e.data || {}
11
+ try {
12
+ let f = map, self
13
+ for (const k of String(name).split('.')) (self = f), (f = f == null ? undefined : f[k])
14
+ if (typeof f !== 'function') throw new TypeError(`worker: \`${name}\` is not an exported function`)
15
+ const ok = await f.apply(self, args || [])
16
+ scope.postMessage({ id, ok }) // throws DataCloneError for a result that cannot be cloned → reported below
17
+ } catch (x) {
18
+ scope.postMessage({ id, err: { message: (x && x.message) || String(x), name: (x && x.name) || 'Error', stack: x && x.stack } })
19
+ }
20
+ }
21
+ }
22
+
23
+ // Page side.
24
+ export function worker(url) {
25
+ let w = null, n = 0
26
+ const wait = new Map()
27
+ const failAll = err => {
28
+ for (const [, p] of wait) p[1](err)
29
+ wait.clear()
30
+ w = null // the next call starts a fresh worker
31
+ }
32
+ const start = () => {
33
+ w = new Worker(url, { type: 'module' })
34
+ w.onmessage = e => {
35
+ const { id, ok, err } = e.data || {}
36
+ const p = wait.get(id)
37
+ if (!p) return
38
+ wait.delete(id)
39
+ if (err) p[1](Object.assign(new Error(err.message), { name: err.name, remoteStack: err.stack }))
40
+ else p[0](ok)
41
+ }
42
+ w.onerror = e => {
43
+ if (e && e.preventDefault) e.preventDefault()
44
+ failAll(new Error('worker failed: ' + ((e && e.message) || url)))
45
+ }
46
+ }
47
+ return {
48
+ call(name, args) {
49
+ if (!w) start()
50
+ const id = ++n
51
+ return new Promise((ok, fail) => {
52
+ wait.set(id, [ok, fail])
53
+ try {
54
+ w.postMessage({ id, name, args })
55
+ } catch (x) {
56
+ wait.delete(id)
57
+ fail(x) // DataCloneError: an argument cannot cross to the worker (functions, DOM nodes…)
58
+ }
59
+ })
60
+ },
61
+ terminate() {
62
+ if (w) w.terminate()
63
+ failAll(new Error('worker terminated'))
64
+ },
65
+ }
66
+ }