@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.
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/brain-ai.js +129 -0
- package/brain-anim.js +21 -0
- package/brain-ch.js +254 -0
- package/brain-consent.js +38 -0
- package/brain-data.js +549 -0
- package/brain-dev.js +100 -0
- package/brain-env.js +160 -0
- package/brain-err.js +242 -0
- package/brain-form.js +275 -0
- package/brain-guard.js +242 -0
- package/brain-hydrate.js +72 -0
- package/brain-i18n.js +111 -0
- package/brain-inspect.js +745 -0
- package/brain-island.js +106 -0
- package/brain-lazy.js +88 -0
- package/brain-mcp.js +73 -0
- package/brain-mock.js +80 -0
- package/brain-overlay.js +123 -0
- package/brain-persist.js +65 -0
- package/brain-router.js +503 -0
- package/brain-rpc.js +20 -0
- package/brain-schema.js +93 -0
- package/brain-wc.js +69 -0
- package/brain-why.js +198 -0
- package/brain-worker.js +66 -0
- package/brain.js +421 -0
- package/package.json +20 -4
- package/server/server.mjs +1046 -0
- package/shim/dom.js +448 -0
- package/shim/probe.js +111 -0
- package/sw/brain-sw.js +103 -0
- package/themes/contrast.cssbrain +39 -0
- package/themes/dark.cssbrain +39 -0
- package/themes/glass.cssbrain +39 -0
- package/themes/paper.cssbrain +39 -0
- package/ui/button.brain +31 -0
- package/ui/center.brain +11 -0
- package/ui/checkbox.brain +13 -0
- package/ui/container.brain +11 -0
- package/ui/dialog.brain +29 -0
- package/ui/error-state.brain +53 -0
- package/ui/field.brain +34 -0
- package/ui/grid.brain +11 -0
- package/ui/input.brain +23 -0
- package/ui/lang-switch.brain +38 -0
- package/ui/menu.brain +64 -0
- package/ui/row.brain +12 -0
- package/ui/select.brain +29 -0
- package/ui/sidebar.brain +17 -0
- package/ui/spacer.brain +12 -0
- package/ui/stack.brain +10 -0
- package/ui/table.brain +73 -0
- package/ui/tabs.brain +33 -0
- package/ui/toast.brain +45 -0
package/brain-island.js
ADDED
|
@@ -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
|
+
}
|
package/brain-overlay.js
ADDED
|
@@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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
|
+
`
|
package/brain-persist.js
ADDED
|
@@ -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
|
+
}
|