@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
@@ -0,0 +1,106 @@
1
+ // BRAINLLM islands (SPEC §6.1). A prerendered island is its root element (`data-b`) followed by
2
+ // `<script type="b/state">{"c": chunk, "x": export, "p": props}</script>`. hydrate() listens on the document:
3
+ // intent (pointerdown, focusin, keydown) only preloads the chunk and lets the native event act on the prerendered DOM,
4
+ // so typing and the press that starts a click are never lost. The first real event (click, submit, input, change)
5
+ // mounts the island in place, carrying over what the user already did there (values, checked, focus and caret), and
6
+ // replays that event once on the live node.
7
+ // No import of the core: brain.js loads this module with import() and passes { mount, _rt } (a static import back
8
+ // would make a cycle that content-hashed chunks cannot name).
9
+ import { adopter } from './brain-hydrate.js' // no imports of its own: no hash cycle
10
+
11
+ let mount, I
12
+ const load = (el, s) => el.$m || (el.$m = import(new URL(JSON.parse(s.textContent).c, document.baseURI).href).catch(_cf))
13
+ const at = (n, path) => {
14
+ for (const i of path) n = n && n.childNodes[i]
15
+ return n
16
+ }
17
+ const pathOf = (n, root, a = []) => {
18
+ for (; n && n !== root; n = n.parentNode) a.unshift([].indexOf.call(n.parentNode.childNodes, n))
19
+ return n ? a : null
20
+ }
21
+ async function resume(el, s) {
22
+ const { c, x = 'default', p = {} } = JSON.parse(s.textContent)
23
+ const m = await load(el, s)
24
+ // #285: a server render stamped for adoption (data-hk) resumes on its own nodes: nothing is replaced, so internal
25
+ // scroll, a playing video, focus and animations survive. Any mismatch falls back to rendering fresh below.
26
+ if (el.getAttribute('data-hk') && I.adopt) {
27
+ let d = null
28
+ try {
29
+ I.adopt(adopter(el))
30
+ const ns = I.root(z => ((d = z), I.nodes(m[x](p))), null)
31
+ if (ns[0] !== el) throw Error('adopt: another root')
32
+ I.adopt(null)
33
+ I.dev(null, c)
34
+ el.$d = d // the router disposes islands it swaps out
35
+ s.remove()
36
+ return el
37
+ } catch {
38
+ I.adopt(null)
39
+ if (d) d()
40
+ }
41
+ }
42
+ const f = document.createDocumentFragment()
43
+ const d = mount(m[x], f, p)
44
+ I.dev(null, c) // dev: hydrate event for brain-inspect
45
+ // dev: the prerendered HTML and the first client render must match (B0214, §31): else the page flashes on resume
46
+ if (globalThis.__brainDev && f.firstChild) {
47
+ const norm = h => h.replace(/<!--[\s\S]*?-->/g, '').replace(/>\s+</g, '><').replace(/\s+(data-b|nonce)="[^"]*"/g, '').trim()
48
+ const a0 = norm(el.outerHTML), b0 = norm(f.firstChild.outerHTML)
49
+ if (a0 !== b0) {
50
+ let i = 0
51
+ while (a0[i] === b0[i]) i++
52
+ console.warn(Object.assign(Error(`B0214: ${c} renders differently on the client than in the prerendered HTML near: server "${a0.slice(Math.max(0, i - 20), i + 40)}" · client "${b0.slice(Math.max(0, i - 20), i + 40)}"`), { code: 'B0214' }))
53
+ }
54
+ }
55
+ const n = f.firstChild, q = 'input,select,textarea', a = el.querySelectorAll(q), b = n.querySelectorAll(q)
56
+ for (let i = 0; i < a.length && i < b.length; i++)
57
+ /^(checkbox|radio)$/.test(a[i].type) ? (b[i].checked = a[i].checked) : a[i].value !== b[i].value && (b[i].value = a[i].value)
58
+ const act = document.activeElement, fp = act && el.contains(act) && pathOf(act, el)
59
+ n.$d = d // the router disposes islands it swaps out
60
+ el.replaceWith(f)
61
+ s.remove()
62
+ const g = fp && at(n, fp)
63
+ if (g && g.focus) {
64
+ g.focus({ preventScroll: true })
65
+ try {
66
+ g.setSelectionRange(act.selectionStart, act.selectionEnd, act.selectionDirection)
67
+ } catch {}
68
+ }
69
+ return n
70
+ }
71
+ // A chunk that cannot be imported (deleted by a newer deploy) reloads the page once per build, never in a loop.
72
+ export function _cf(e) {
73
+ const b = document.querySelector('meta[name=brain-build]'), k = 'brain:reload:' + (b ? b.getAttribute('content') : '')
74
+ try {
75
+ if (!sessionStorage.getItem(k)) sessionStorage.setItem(k, 1), location.reload()
76
+ } catch {}
77
+ throw e
78
+ }
79
+ export function hydrate(r = document, rt) {
80
+ ;({ mount } = rt)
81
+ I = rt._rt()
82
+ if (r.$h) return // idempotent: the router re-runs page scripts on every navigation
83
+ r.$h = 1
84
+ for (const t of ['click', 'submit', 'input', 'change', 'pointerdown', 'focusin', 'keydown'])
85
+ r.addEventListener(
86
+ t,
87
+ e => {
88
+ const path = []
89
+ let el = e.target, s
90
+ for (; el && el.nodeType === 1; el = el.parentNode) {
91
+ s = el.nextSibling
92
+ if (s && s.type === 'b/state' && el.hasAttribute('data-b')) break
93
+ path.unshift([].indexOf.call(el.parentNode.childNodes, el))
94
+ }
95
+ if (!el || el.nodeType !== 1) return
96
+ if ('pointerdown focusin keydown'.includes(t)) return void load(el, s) // intent: preload only, the native event goes on
97
+ e.preventDefault()
98
+ e.stopPropagation()
99
+ ;(el.$b || (el.$b = resume(el, s))).then(
100
+ n => (n = at(n, path)) && n.dispatchEvent(new e.constructor(t, e)),
101
+ x => I.oops(x, null, 'island') // reported once to the catcher, never an unhandled rejection
102
+ )
103
+ },
104
+ true
105
+ )
106
+ }
package/brain-lazy.js ADDED
@@ -0,0 +1,88 @@
1
+ // BRAINLLM lazy components (INTERNALS §22): `<Chart lazy />` (visible), `lazy="idle"`, `lazy="interaction"`.
2
+ // The codegen renders a placeholder element in the template (its fallback content, or an empty sized box) and emits
3
+ // `lazy(el, () => import('/_b/<h>'), mode, $props)`. When the trigger fires, the chunk is imported and the component
4
+ // is built under the owner that called lazy (context, disposal and the error catcher work as if it were inline);
5
+ // its nodes replace the placeholder. If the owner is disposed first, the trigger is cancelled; afterwards, the
6
+ // component is disposed with it. An import failure is reported (reportError → brain-err) and the placeholder stays.
7
+ // `el.$l` is the load promise (prefetch: `el.$l || el.$go()`).
8
+ import { _rt, fx } from './brain.js'
9
+
10
+ const G = globalThis
11
+ const I = _rt()
12
+ const ACT = ['pointerover', 'pointerdown', 'focusin', 'touchstart', 'click', 'keydown']
13
+ // A chunk deleted by a newer deploy: reload the page once per build (sessionStorage guard), never in a loop (§35.7).
14
+ function reloadOnce() {
15
+ try {
16
+ const b = document.querySelector('meta[name=brain-build]'), k = 'brain:reload:' + (b ? b.getAttribute('content') : '')
17
+ if (sessionStorage.getItem(k)) return false
18
+ sessionStorage.setItem(k, 1)
19
+ location.reload()
20
+ return true
21
+ } catch {
22
+ return false
23
+ }
24
+ }
25
+
26
+ export function lazy(el, imp, mode, props = {}) {
27
+ const owner = I.owner()
28
+ let dead = false, stop = () => {}, retry = () => {}, clicked = null, built
29
+ const go = () => {
30
+ if (dead) return
31
+ stop()
32
+ el.setAttribute('aria-busy', 'true')
33
+ return (el.$l ||= Promise.resolve()
34
+ .then(imp)
35
+ .then(m => {
36
+ if (dead) return
37
+ const C = (m && m.default) || m
38
+ built = I.root(() => I.nodes(C(props)), owner) // adopted by owner: disposed with it
39
+ el.replaceWith(...built)
40
+ // the click that triggered the load landed on the placeholder: replay it on the real component
41
+ const t = clicked && built.find(n => n.nodeType === 1)
42
+ if (t) t.dispatchEvent(new clicked.constructor('click', clicked))
43
+ return built
44
+ })
45
+ .catch(e => {
46
+ el.$l = null
47
+ if (dead) return
48
+ el.setAttribute('aria-busy', 'false')
49
+ retry()
50
+ if (!reloadOnce()) G.reportError ? reportError(e) : queueMicrotask(() => { throw e })
51
+ }))
52
+ }
53
+ el.$go = go
54
+ if (mode === 'idle') {
55
+ if (G.requestIdleCallback) {
56
+ const h = requestIdleCallback(go, { timeout: 2000 })
57
+ stop = () => cancelIdleCallback(h)
58
+ } else {
59
+ const h = setTimeout(go, 1)
60
+ stop = () => clearTimeout(h)
61
+ }
62
+ } else if (mode === 'interaction') {
63
+ const f = e => {
64
+ if (e.type === 'keydown') {
65
+ if (e.key !== 'Enter' && e.key !== ' ') return
66
+ e.preventDefault()
67
+ }
68
+ if (e.type === 'click') (clicked = e), e.preventDefault(), e.stopPropagation()
69
+ go()
70
+ }
71
+ retry = () => { for (const t of ACT) el.addEventListener(t, f, t === 'click') }
72
+ retry()
73
+ stop = () => {
74
+ for (const t of ACT) el.removeEventListener(t, f, t === 'click')
75
+ }
76
+ } else if (G.IntersectionObserver) {
77
+ const io = new IntersectionObserver(es => es.some(e => e.isIntersecting) && go(), { rootMargin: '200px' })
78
+ io.observe(el)
79
+ stop = () => io.disconnect()
80
+ } else go()
81
+ if (owner)
82
+ fx(() => () => {
83
+ dead = true
84
+ stop()
85
+ if (built) I.rm(built)
86
+ })
87
+ return el
88
+ }
package/brain-mcp.js ADDED
@@ -0,0 +1,73 @@
1
+ // BRAINLLM agent tools (WebMCP, INTERNALS §32). A component exposes functions to the browser's AI agent:
2
+ // tool('search_products', 'Search the catalog by text', { q: 'str!', max: 'num<=50' }, ({ q, max }) => …)
3
+ // Registered when the component mounts, unregistered when it unmounts; a no-op where the browser has no model context
4
+ // (feature-detected: document.modelContext, the newer name, or navigator.modelContext, Chrome's origin trial). The
5
+ // parameters use form()'s mini-language and become a JSON Schema; arguments are validated before `run` is called and
6
+ // a bad call answers with the field's message instead of running. Tools act with the page's permissions only:
7
+ // anything destructive should still ask for confirmation in the UI.
8
+ // Optional 5th argument (WebMCP draft 2026-10-27): { title, readOnly, consequential, untrusted } → `title` and
9
+ // `annotations: { readOnlyHint, consequentialHint, untrustedContentHint }` on the registration; only the keys given.
10
+ import { fx } from './brain.js'
11
+ import { rule, check } from './brain-schema.js'
12
+
13
+ const G = globalThis
14
+ export const context = () => (G.document && G.document.modelContext) || (G.navigator && G.navigator.modelContext) || null
15
+
16
+ const TYPE = { str: 'string', email: 'string', url: 'string', date: 'string', num: 'number', bool: 'boolean' }
17
+ // { q: 'str!', tags: '[str]' } → JSON Schema (object, properties, required)
18
+ export function schema(params = {}) {
19
+ const properties = {}, required = []
20
+ for (const [k, src] of Object.entries(params)) {
21
+ const r = rule(src), item = { type: TYPE[r.t] || 'string' }
22
+ if (r.t === 'email') item.format = 'email'
23
+ if (r.t === 'url') item.format = 'uri'
24
+ if (r.t === 'date') item.format = 'date'
25
+ if (r.t === 'num' && r.min != null) item.minimum = r.min
26
+ if (r.t === 'num' && r.max != null) item.maximum = r.max
27
+ if (r.t === 'str' && r.min != null) item.minLength = r.min
28
+ if (r.t === 'str' && r.max != null) item.maxLength = r.max
29
+ properties[k] = r.list ? { type: 'array', items: item } : item
30
+ if (r.req) required.push(k)
31
+ }
32
+ return { type: 'object', properties, required }
33
+ }
34
+
35
+ const text = v => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v ?? null) }] })
36
+
37
+ const HINTS = [['readOnly', 'readOnlyHint'], ['consequential', 'consequentialHint'], ['untrusted', 'untrustedContentHint']]
38
+
39
+ // The registration dictionary: name, description, inputSchema, then title/annotations only when given.
40
+ export function definition(name, description, params, opts) {
41
+ const def = { name, description, inputSchema: schema(params) }
42
+ if (!opts) return def
43
+ if (typeof opts.title === 'string' && opts.title) def.title = opts.title
44
+ let annotations = null
45
+ for (let i = 0; i < HINTS.length; i++) {
46
+ const v = opts[HINTS[i][0]]
47
+ if (v != null) (annotations || (annotations = {}))[HINTS[i][1]] = !!v
48
+ }
49
+ if (annotations) def.annotations = annotations
50
+ return def
51
+ }
52
+
53
+ export function tool(name, description, params, run, opts) {
54
+ const mc = context()
55
+ if (!mc || !mc.registerTool || G.__brainSSR || G.__brainRoute) return
56
+ const rules = Object.fromEntries(Object.entries(params || {}).map(([k, s]) => [k, rule(s)]))
57
+ fx(() => {
58
+ const def = definition(name, description, params, opts)
59
+ def.execute = async (args = {}) => {
60
+ for (const k in rules) {
61
+ const m = check(rules[k], args[k])
62
+ if (m) return text(`${k}: ${m}`)
63
+ }
64
+ try {
65
+ return text(await run(args))
66
+ } catch (e) {
67
+ return text(`Error: ${(e && e.message) || e}`)
68
+ }
69
+ }
70
+ const reg = mc.registerTool(def)
71
+ return () => (reg && reg.unregister ? reg.unregister() : mc.unregisterTool && mc.unregisterTool(name))
72
+ })
73
+ }
package/brain-mock.js ADDED
@@ -0,0 +1,80 @@
1
+ // BRAINLLM fake data (brain run --mock, INTERNALS §15, #159). Development only: brain-data never imports it, so no page
2
+ // in production carries it. `fake(schema, seed)` → data that matches an api() schema (form()'s mini-language, nested
3
+ // objects, [Schema] lists), deterministic: the same schema and seed give the same JSON on any machine. Field names steer
4
+ // the values (email, name, title, city, url, phone, price, date, id…) so screens look real; bounds and required hold.
5
+ // `mocker(seed)` is what `http({ mock })` takes: the same seed and URL give the same answer; a write answers its input
6
+ // merged over a fake `out`. TOOLCHAIN #266 wires it: `http({ mock: mocker(seed) })`.
7
+ import { rule } from './brain-schema.js'
8
+
9
+ const hashOf = s => {
10
+ let h = 2166136261
11
+ for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619)
12
+ return h >>> 0
13
+ }
14
+ const prng = seed => {
15
+ let a = hashOf(seed)
16
+ return () => {
17
+ a = (a + 0x6d2b79f5) | 0
18
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
19
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
20
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
21
+ }
22
+ }
23
+ const FIRST = ['Ada', 'Grace', 'Linus', 'Margaret', 'Alan', 'Barbara', 'Ken', 'Radia', 'Dennis', 'Frances', 'Tim', 'Katherine']
24
+ const LAST = ['Lovelace', 'Hopper', 'Torvalds', 'Hamilton', 'Turing', 'Liskov', 'Thompson', 'Perlman', 'Ritchie', 'Allen', 'Berners-Lee', 'Johnson']
25
+ const WORDS = ['quiet', 'bright', 'river', 'signal', 'garden', 'orbit', 'paper', 'stone', 'harbor', 'window', 'field', 'summer', 'north', 'cedar', 'lantern', 'meadow']
26
+ const CITIES = ['Lisbon', 'Kyoto', 'Montreal', 'Oslo', 'Puebla', 'Cape Town', 'Melbourne', 'Lima', 'Seoul', 'Vienna']
27
+ function fakeField(r, key, rnd, i, who) {
28
+ const k = String(key || '').toLowerCase(), pick = a => a[Math.floor(rnd() * a.length)]
29
+ const words = n => Array.from({ length: n }, () => pick(WORDS)).join(' ')
30
+ const cap = w => w.charAt(0).toUpperCase() + w.slice(1)
31
+ if (r.t === 'bool') return rnd() < 0.5
32
+ if (r.t === 'num') {
33
+ const lo = r.min ?? (/(^|_)id$|count|qty|quantity|age/.test(k) ? 1 : 0), hi = r.max ?? (/age/.test(k) ? 90 : /price|amount|total|cost/.test(k) ? 500 : /(^|_)id$/.test(k) ? 10000 : 100)
34
+ if (/(^|_)id$/.test(k) && i != null) return i + 1 // ids of a list are unique and stable
35
+ const v = lo + rnd() * (hi - lo)
36
+ return /price|amount|total|cost|rate/.test(k) ? Math.round(v * 100) / 100 : Math.round(v)
37
+ }
38
+ if (r.t === 'date') return new Date(Date.UTC(2026, 0, 1) + Math.floor(rnd() * 365) * 864e5).toISOString().slice(0, 10)
39
+ const { first, last } = who || { first: pick(FIRST), last: pick(LAST) } // one person per object: name and email agree
40
+ let v
41
+ if (r.t === 'email' || /email/.test(k)) v = `${first}.${last}`.toLowerCase().replace(/[^a-z.]/g, '') + '@example.com'
42
+ else if (r.t === 'url' || /url|link|website|href/.test(k)) v = `https://example.com/${pick(WORDS)}`
43
+ else if (/(^|_)id$|slug/.test(k)) v = (i != null ? `${pick(WORDS)}-${i + 1}` : `${pick(WORDS)}-${Math.floor(rnd() * 1000)}`)
44
+ else if (/^name$|full_?name|author|user/.test(k)) v = `${first} ${last}`
45
+ else if (/first/.test(k)) v = first
46
+ else if (/last|surname/.test(k)) v = last
47
+ else if (/city|town/.test(k)) v = pick(CITIES)
48
+ else if (/phone|tel/.test(k)) v = `+1 555 01${String(Math.floor(rnd() * 100)).padStart(2, '0')}`
49
+ else if (/title|subject|headline/.test(k)) v = cap(words(3))
50
+ else if (/body|text|content|description|summary|bio|note/.test(k)) v = cap(words(12)) + '.'
51
+ else if (/color|colour/.test(k)) v = '#' + Math.floor(rnd() * 0xffffff).toString(16).padStart(6, '0')
52
+ else if (/status|state/.test(k)) v = pick(['active', 'pending', 'done'])
53
+ else v = cap(words(2))
54
+ if (r.max != null && v.length > r.max) v = v.slice(0, r.max).trim()
55
+ if (r.min != null && v.length < r.min) v = v.padEnd(r.min, 'x')
56
+ return v
57
+ }
58
+ function gen(s, key, rnd, i, who) {
59
+ if (Array.isArray(s)) return Array.from({ length: 3 + Math.floor(rnd() * 4) }, (_, j) => gen(s[0], key, rnd, j))
60
+ if (s && typeof s === 'object') {
61
+ const o = {}, me = { first: FIRST[Math.floor(rnd() * FIRST.length)], last: LAST[Math.floor(rnd() * LAST.length)] }
62
+ for (const k of Object.keys(s)) o[k] = gen(s[k], k, rnd, i, me)
63
+ return o
64
+ }
65
+ let r
66
+ try {
67
+ r = rule(s)
68
+ } catch {
69
+ return null // a type name the compiler could not resolve: nothing to imitate
70
+ }
71
+ if (r.t === 'obj') return gen(r.s, key, rnd, i)
72
+ if (r.list) return Array.from({ length: Math.max(r.min || 0, 3) }, (_, j) => fakeField(r, key, rnd, j, who))
73
+ return fakeField(r, key, rnd, i, who)
74
+ }
75
+ export const fake = (schema, seed = 'brain') => gen(schema, '', prng(String(seed)), null)
76
+
77
+ export const mocker = (seed = 'brain') => (method, url, def, body) => {
78
+ const out = def && def.out ? fake(def.out, seed + ' ' + method + ' ' + url) : null
79
+ return method === 'GET' || !out || typeof out !== 'object' || Array.isArray(out) ? out : { ...out, ...body }
80
+ }
@@ -0,0 +1,123 @@
1
+ // BRAINLLM dev overlay (INTERNALS §20, #267). Development only: the dev client (crates/brain-serve/src/client.js)
2
+ // imports it; no page in production does. What it shows:
3
+ // - errors (a file that does not compile, or a runtime error from brain-err): a panel at the bottom that does not cover
4
+ // the page, with the code, the snippet with the line marked, the fix and "Copy for the LLM";
5
+ // - warnings and info: a chip in a corner ("3 warnings") that opens the same list.
6
+ // API (format agreed with TOOLCHAIN #47): show(diags, { file }) replaces the diagnostics of that file (an empty list =
7
+ // it compiles again) · error(record) adds a runtime error (brain-err: { code, msg, src, brainStack }) · clear().
8
+ // A diagnostic has the shape of `brain check --json` plus `snippet: { line, text: [...] }` (a few lines around `line`).
9
+ const G = globalThis
10
+ const byFile = new Map() // file → diagnostics
11
+ let runtime = [] // runtime errors, newest last
12
+ let host = null, root = null, open = false
13
+
14
+ const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c])
15
+ const all = () => [...byFile.values()].flat()
16
+
17
+ // The text a model needs to fix it without opening anything else: location, message, snippet, fix and the JSON
18
+ export function forLLM(d) {
19
+ const out = [`${d.code} ${d.file || d.src || ''}${d.line ? `:${d.line}${d.col ? ':' + d.col : ''}` : ''} (${d.level || 'error'})`, d.msg]
20
+ if (d.snippet && d.snippet.text)
21
+ d.snippet.text.forEach((t, i) => {
22
+ const n = d.snippet.line + i
23
+ out.push(`${n === d.line ? '>' : ' '} ${String(n).padStart(4)} | ${t}`)
24
+ })
25
+ if (d.fix) out.push(`fix: ${d.fix}`)
26
+ if (d.brainStack) out.push('stack:', ...[].concat(d.brainStack))
27
+ if (d.file) out.push('brain check --json:', JSON.stringify((byFile.get(d.file) || [d]).map(({ snippet, ...x }) => x)))
28
+ return out.join('\n')
29
+ }
30
+
31
+ function item(d, i) {
32
+ const loc = d.file ? `${d.file}:${d.line || ''}${d.col ? ':' + d.col : ''}` : d.src || ''
33
+ const snip = d.snippet && d.snippet.text
34
+ ? `<pre class="snip">${d.snippet.text.map((t, j) => `<span class="${d.snippet.line + j === d.line ? 'hit' : ''}">${String(d.snippet.line + j).padStart(4)} │ ${esc(t)}</span>`).join('\n')}</pre>`
35
+ : ''
36
+ return `<li class="${d.level === 'warning' || d.level === 'info' ? d.level : 'error'}"><p class="t"><b>${esc(d.code || 'Error')}</b> <span>${esc(loc)}</span></p>` +
37
+ `<p>${esc(d.msg)}</p>${snip}${d.fix ? `<p class="fix">fix: ${esc(d.fix)}</p>` : ''}` +
38
+ `<button type="button" data-copy="${i}">Copy for the LLM</button></li>`
39
+ }
40
+
41
+ function render() {
42
+ const d = G.document
43
+ if (!d || !d.body) return
44
+ const list = [...all(), ...runtime.map(r => ({ level: 'error', ...r }))]
45
+ const errors = list.filter(x => x.level !== 'warning' && x.level !== 'info'), notes = list.filter(x => x.level === 'warning' || x.level === 'info')
46
+ if (!list.length) {
47
+ if (host) host.remove()
48
+ return
49
+ }
50
+ if (!host) {
51
+ host = d.createElement('brain-overlay')
52
+ root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host
53
+ root.addEventListener('click', e => {
54
+ let b = e.target
55
+ while (b && b.tagName !== 'BUTTON') b = b.parentNode
56
+ if (!b) return
57
+ if (b.hasAttribute('data-chip')) return void ((open = !open), render())
58
+ const x = shown[+b.getAttribute('data-copy')]
59
+ if (x) copy(forLLM(x), b)
60
+ })
61
+ }
62
+ const panel = errors.length || open
63
+ shown = panel ? (errors.length ? [...errors, ...(open ? notes : [])] : notes) : []
64
+ root.innerHTML = `<style>${CSS}</style>` +
65
+ (panel ? `<section class="panel" role="alert" aria-label="Problems"><ul>${shown.map(item).join('')}</ul></section>` : '') +
66
+ (notes.length ? `<button type="button" class="chip" data-chip aria-expanded="${open}">${notes.length} ${notes.length === 1 ? 'warning' : 'warnings'}</button>` : '')
67
+ if (!host.isConnected) d.body.appendChild(host)
68
+ }
69
+ let shown = []
70
+
71
+ function copy(text, btn) {
72
+ const done = () => ((btn.textContent = 'Copied'), setTimeout(() => (btn.textContent = 'Copy for the LLM'), 1500))
73
+ const nav = G.navigator
74
+ if (nav && nav.clipboard && nav.clipboard.writeText) nav.clipboard.writeText(text).then(done, () => {})
75
+ else done()
76
+ G.__brainOverlayCopied = text // tests and tools read what was copied
77
+ }
78
+
79
+ export function show(diags, o = {}) {
80
+ // show(all, 'comp/x.brain') (the dev client) or show(all, { file })
81
+ const file = (typeof o === 'string' ? o : o.file) || (diags[0] && diags[0].file) || ''
82
+ if (diags.length) byFile.set(file, diags)
83
+ else byFile.delete(file)
84
+ render()
85
+ }
86
+ export function error(r) {
87
+ if (!r) return
88
+ // the same error again (brain-err notifies twice: once more when the source map resolves) merges into the first
89
+ const same = x => x.code === r.code && x.msg === r.msg && x.src === r.src
90
+ const old = runtime.find(same)
91
+ runtime = [...runtime.filter(x => !same(x)), { ...old, ...Object.fromEntries(Object.entries(r).filter(([, v]) => v != null)) }].slice(-20)
92
+ render()
93
+ }
94
+ export function clear() {
95
+ byFile.clear()
96
+ runtime = []
97
+ open = false
98
+ render()
99
+ }
100
+
101
+ const CSS = `
102
+ :host,.panel,.chip{all:initial}
103
+ .panel{position:fixed;left:12px;right:12px;bottom:12px;max-height:45vh;overflow:auto;z-index:2147483647;padding:14px 18px;
104
+ border-radius:14px;background:rgba(22,22,26,.95);color:#f3f3f5;font:13px/1.5 ui-sans-serif,system-ui,sans-serif;
105
+ box-shadow:0 1px 2px rgba(0,0,0,.2),0 18px 40px -12px rgba(0,0,0,.45)}
106
+ ul{margin:0;padding:0;list-style:none;display:grid;gap:14px}
107
+ li{border-left:3px solid #ff6b5f;padding-left:12px}
108
+ li.warning{border-color:#f5b54a}
109
+ li.info{border-color:#6aa8ff}
110
+ p{margin:2px 0}
111
+ .t b{color:#ff8a80}
112
+ li.warning .t b{color:#f5b54a}
113
+ .t span{color:#b4b4bd;font:12px ui-monospace,monospace}
114
+ .snip{margin:6px 0;padding:8px 10px;border-radius:8px;background:rgba(255,255,255,.06);font:12px/1.45 ui-monospace,monospace;overflow:auto;white-space:pre}
115
+ .snip .hit{background:rgba(255,107,95,.22);display:inline-block;min-width:100%}
116
+ .fix{color:#9fe0b0}
117
+ button{font:12px ui-sans-serif,system-ui,sans-serif;margin-top:6px;padding:4px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.2);
118
+ background:rgba(255,255,255,.08);color:#f3f3f5;cursor:pointer}
119
+ .chip{position:fixed;right:16px;bottom:16px;z-index:2147483647;padding:6px 12px;border-radius:999px;border:0;cursor:pointer;
120
+ font:600 12px ui-sans-serif,system-ui,sans-serif;color:#1a1a1a;background:#f5b54a;box-shadow:0 6px 18px -6px rgba(0,0,0,.35)}
121
+ .panel ~ .chip{top:16px;bottom:auto}
122
+ @media (prefers-reduced-motion:no-preference){button{transition:background .15s}}
123
+ `
@@ -0,0 +1,65 @@
1
+ // BRAINLLM store persistence (INTERNALS §35.4), loaded on demand by `store(init, { persist: true })` (brain-ch.js).
2
+ // brain-ch restores small states from localStorage synchronously; this module restores large ones from IndexedDB and
3
+ // writes every change (debounced 100 ms): below 4 KB to localStorage, above to IndexedDB with a marker in localStorage.
4
+ // Records carry the shape version `v`; another shape is discarded. Quota or private-mode errors switch that store to
5
+ // memory and are reported once (reportError → brain-err), never thrown.
6
+ import { _rt, fx } from './brain.js'
7
+
8
+ const G = globalThis, I = _rt(), LIMIT = 4096, WAIT = 100
9
+ let warned = false
10
+ const report = e => {
11
+ if (warned) return
12
+ warned = true
13
+ const x = Object.assign(new Error('persist: ' + ((e && e.message) || e) + '; the store stays in memory'), { code: 'persist', cause: e })
14
+ G.reportError ? G.reportError(x) : console.warn(x.message)
15
+ }
16
+
17
+ let dbp = null
18
+ const req = r => new Promise((ok, fail) => ((r.onsuccess = () => ok(r.result)), (r.onerror = () => fail(r.error))))
19
+ const db = () =>
20
+ (dbp ||= new Promise((ok, fail) => {
21
+ const r = G.indexedDB.open('brain', 1)
22
+ r.onupgradeneeded = () => r.result.createObjectStore('kv')
23
+ r.onsuccess = () => ok(r.result)
24
+ r.onerror = () => fail(r.error)
25
+ }).catch(e => ((dbp = null), Promise.reject(e))))
26
+ const idb = async (mode, f) => req(f((await db()).transaction('kv', mode).objectStore('kv')))
27
+
28
+ export async function persist({ key, v, big, apply, snap, S, base }) {
29
+ let mem = false, last = base
30
+ if (big)
31
+ try {
32
+ const r = G.indexedDB ? await idb('readonly', s => s.get(key)) : null
33
+ if (r && r.v === v) apply(r.d), (last = JSON.stringify(snap()))
34
+ } catch (e) {
35
+ report(e)
36
+ }
37
+ let t = null, inIdb = big
38
+ const write = async j => {
39
+ if (mem) return
40
+ try {
41
+ if (j.length < LIMIT || !G.indexedDB) {
42
+ G.localStorage.setItem(key, `{"v":${JSON.stringify(v)},"d":${j}}`)
43
+ if (inIdb) (inIdb = false), await idb('readwrite', s => s.delete(key))
44
+ } else {
45
+ await idb('readwrite', s => s.put({ v, d: JSON.parse(j) }, key))
46
+ inIdb = true
47
+ G.localStorage.setItem(key, JSON.stringify({ v, idb: 1 }))
48
+ }
49
+ } catch (e) {
50
+ mem = true
51
+ report(e)
52
+ }
53
+ }
54
+ I.root(() =>
55
+ fx(() => {
56
+ const d = {}
57
+ for (const k in S) d[k] = S[k].v // tracks every field
58
+ const j = JSON.stringify(d)
59
+ if (j === last) return
60
+ last = j
61
+ clearTimeout(t)
62
+ t = setTimeout(() => write(last), WAIT)
63
+ })
64
+ )
65
+ }