@mindmakr/reins 0.0.0-stage → 0.1.0

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/adapters.d.ts +25 -0
  4. package/dist/adapters.js +24 -0
  5. package/dist/bridge/args.d.ts +15 -0
  6. package/dist/bridge/args.js +59 -0
  7. package/dist/bridge/audit.d.ts +13 -0
  8. package/dist/bridge/audit.js +13 -0
  9. package/dist/bridge/clipboard.d.ts +2 -0
  10. package/dist/bridge/clipboard.js +31 -0
  11. package/dist/bridge/hub.d.ts +56 -0
  12. package/dist/bridge/hub.js +162 -0
  13. package/dist/bridge/index.d.ts +11 -0
  14. package/dist/bridge/index.js +11 -0
  15. package/dist/bridge/mcp.d.ts +5 -0
  16. package/dist/bridge/mcp.js +22 -0
  17. package/dist/bridge/origin.d.ts +7 -0
  18. package/dist/bridge/origin.js +23 -0
  19. package/dist/bridge/report-cli.d.ts +13 -0
  20. package/dist/bridge/report-cli.js +82 -0
  21. package/dist/bridge/session.d.ts +24 -0
  22. package/dist/bridge/session.js +116 -0
  23. package/dist/bridge/snap.d.ts +28 -0
  24. package/dist/bridge/snap.js +65 -0
  25. package/dist/bridge/token.d.ts +10 -0
  26. package/dist/bridge/token.js +35 -0
  27. package/dist/bridge/tools.d.ts +18 -0
  28. package/dist/bridge/tools.js +174 -0
  29. package/dist/cli.d.ts +2 -0
  30. package/dist/cli.js +67 -0
  31. package/dist/dom.d.ts +2 -0
  32. package/dist/dom.js +48 -0
  33. package/dist/env.d.ts +1 -0
  34. package/dist/env.js +6 -0
  35. package/dist/expo.d.ts +27 -0
  36. package/dist/expo.js +48 -0
  37. package/dist/index.d.ts +70 -0
  38. package/dist/index.js +267 -0
  39. package/dist/metro.d.ts +16 -0
  40. package/dist/metro.js +45 -0
  41. package/dist/network.d.ts +14 -0
  42. package/dist/network.js +50 -0
  43. package/dist/next-token-route.d.ts +9 -0
  44. package/dist/next-token-route.js +17 -0
  45. package/dist/next.d.ts +13 -0
  46. package/dist/next.js +27 -0
  47. package/dist/node.d.ts +80 -0
  48. package/dist/node.js +414 -0
  49. package/dist/react.d.ts +281 -0
  50. package/dist/react.js +33 -0
  51. package/dist/record-stub.d.ts +18 -0
  52. package/dist/record-stub.js +6 -0
  53. package/dist/record.d.ts +28 -0
  54. package/dist/record.js +104 -0
  55. package/dist/redact.d.ts +7 -0
  56. package/dist/redact.js +30 -0
  57. package/dist/report.d.ts +121 -0
  58. package/dist/report.js +353 -0
  59. package/dist/server.d.ts +13 -0
  60. package/dist/server.js +27 -0
  61. package/dist/stub.d.ts +88 -0
  62. package/dist/stub.js +38 -0
  63. package/dist/types.d.ts +248 -0
  64. package/dist/types.js +1 -0
  65. package/dist/verify-cli.d.ts +2 -0
  66. package/dist/verify-cli.js +3 -0
  67. package/dist/verify.d.ts +17 -0
  68. package/dist/verify.js +60 -0
  69. package/dist/vite.d.ts +32 -0
  70. package/dist/vite.js +33 -0
  71. package/dist/webmcp.d.ts +3 -0
  72. package/dist/webmcp.js +20 -0
  73. package/package.json +59 -4
package/dist/report.js ADDED
@@ -0,0 +1,353 @@
1
+ export const SCHEMA_VERSION = 1;
2
+ /** Thresholds behind the Findings section. */
3
+ export const LIMITS = { apiMs: 500, renders: 10, fcpMs: 1800, dupCalls: 3, dupWindowMs: 1000, spanShare: 0.5, spanMinMs: 100, nPlusOne: 3 };
4
+ const R = Math.round;
5
+ const path = (url) => url.split(/[?#]/)[0];
6
+ const callKey = (c) => `${c.method} ${path(c.url)}`;
7
+ const sum = (a) => a.reduce((x, y) => x + y, 0);
8
+ const avg = (a) => (a.length ? R(sum(a) / a.length) : 0);
9
+ const groupBy = (list, key) => {
10
+ const m = new Map();
11
+ for (const x of list) {
12
+ const k = key(x);
13
+ const bucket = m.get(k);
14
+ bucket ? bucket.push(x) : m.set(k, [x]);
15
+ }
16
+ return m;
17
+ };
18
+ const trimCall = ({ method, url, status, startMs = 0, ms, traceId }) => ({ method, url, status, startMs, ms, traceId });
19
+ /** Per-route stats for server requests, slowest average first. */
20
+ export function routeStats(requests) {
21
+ return [...groupBy(requests, (r) => `${r.method} ${r.route}`).values()]
22
+ .map((rs) => ({
23
+ method: rs[0].method,
24
+ route: rs[0].route,
25
+ count: rs.length,
26
+ avgMs: avg(rs.map((r) => r.ms)),
27
+ maxMs: Math.max(...rs.map((r) => r.ms)),
28
+ avgDbMs: avg(rs.map((r) => r.dbMs)),
29
+ }))
30
+ .sort((a, b) => b.avgMs - a.avgMs);
31
+ }
32
+ /** Merges the reports of several paired servers into one. */
33
+ export function mergeServers(reports) {
34
+ if (!reports.length)
35
+ return null;
36
+ const requests = reports.flatMap((r) => r.requests).sort((a, b) => a.at - b.at);
37
+ return {
38
+ runtime: reports[0].runtime,
39
+ requests,
40
+ slowest: routeStats(requests),
41
+ spans: reports.flatMap((r) => r.spans),
42
+ outbound: reports.flatMap((r) => r.outbound),
43
+ actions: reports.flatMap((r) => r.actions || []),
44
+ };
45
+ }
46
+ /**
47
+ * Adds the unified timeline to a perf report: lifecycle marks, panel mounts, API calls and actions,
48
+ * plus the server half of each API call, joined by its `x-reins-trace` id. Calls are trimmed to timing fields.
49
+ */
50
+ export function withTimeline(perf, server) {
51
+ const timeline = [];
52
+ const add = (e) => timeline.push(e);
53
+ const calls = perf.api.calls.map(trimCall);
54
+ const byTrace = new Map((server || []).filter((r) => r.traceId).map((r) => [r.traceId, r]));
55
+ for (const [k, v] of Object.entries(perf.lifecycle))
56
+ if (v != null && !/firstPaint|domNodes|nowMs/.test(k))
57
+ add({ kind: 'lifecycle', name: k.slice(0, -2), atMs: v });
58
+ for (const c of perf.components)
59
+ add({ kind: 'component-mount', name: c.id, atMs: c.mountedAtMs });
60
+ for (const c of calls) {
61
+ add({ kind: 'api', name: `${c.method} ${c.url}`, atMs: c.startMs || 0, durationMs: c.ms, status: c.status, traceId: c.traceId });
62
+ const s = c.traceId && byTrace.get(c.traceId);
63
+ if (s) {
64
+ // Clocks differ between page and server; centre the server time inside the browser call.
65
+ const at = R((c.startMs || 0) + Math.max(0, (c.ms - s.ms) / 2));
66
+ add({
67
+ kind: 'server',
68
+ name: `${s.method} ${s.route}`,
69
+ atMs: at,
70
+ durationMs: s.ms,
71
+ status: s.status,
72
+ traceId: s.traceId,
73
+ dbMs: s.dbMs,
74
+ spans: s.spans.map((p) => ({ name: p.name, atMs: R(at + p.startMs), durationMs: p.ms, error: p.error })),
75
+ });
76
+ }
77
+ }
78
+ for (const a of perf.actions)
79
+ add({ kind: 'action', name: a.name, atMs: a.startMs, durationMs: a.durationMs });
80
+ timeline.sort((a, b) => a.atMs - b.atMs);
81
+ const slowest = perf.api.slowest && trimCall(perf.api.slowest);
82
+ return { ...perf, api: { ...perf.api, slowest, calls }, timeline };
83
+ }
84
+ /** Flattens a `reins_dom` tree: node count and the nodes that have layout issues. */
85
+ export function summarizeDom(panel, root) {
86
+ let nodes = 0;
87
+ const issues = [];
88
+ const walk = (n) => {
89
+ nodes++;
90
+ if (n.issues?.length)
91
+ issues.push({ panel, tag: n.tag, id: n.id, text: n.text, issues: n.issues });
92
+ n.children?.forEach(walk);
93
+ };
94
+ if (root)
95
+ walk(root);
96
+ return { nodes, issues };
97
+ }
98
+ /** Puts the gathered parts into one session record. */
99
+ export function buildSession(p) {
100
+ const perf = p.perf ? withTimeline(p.perf, p.server?.requests) : null;
101
+ const comps = new Map((perf?.components || []).map((c) => [c.id, c]));
102
+ const panels = [];
103
+ const domIssues = [];
104
+ for (const [id, tree] of Object.entries(p.dom || {})) {
105
+ const d = summarizeDom(id, tree);
106
+ const c = comps.get(id);
107
+ panels.push({ id, renders: c?.renders, mountedAtMs: c?.mountedAtMs, lastRenderMs: c?.lastRenderMs, nodes: d.nodes, issues: d.issues.length });
108
+ domIssues.push(...d.issues);
109
+ }
110
+ for (const c of comps.values())
111
+ if (!p.dom || !(c.id in p.dom))
112
+ panels.push({ id: c.id, renders: c.renders, mountedAtMs: c.mountedAtMs, lastRenderMs: c.lastRenderMs, nodes: 0, issues: 0 });
113
+ return {
114
+ schemaVersion: 1,
115
+ title: p.title || 'session',
116
+ createdAt: p.createdAt || new Date().toISOString(),
117
+ reinsVersion: p.reinsVersion,
118
+ source: p.source,
119
+ app: p.app || {},
120
+ actions: p.actions || [],
121
+ state: p.state ?? null,
122
+ network: p.network || [],
123
+ errors: p.errors || [],
124
+ perf,
125
+ panels,
126
+ domIssues,
127
+ server: p.server || null,
128
+ snaps: p.snaps || [],
129
+ audit: p.audit || [],
130
+ };
131
+ }
132
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
133
+ /** Plain-language flags computed by simple rules. See `LIMITS`. */
134
+ export function findings(s) {
135
+ const out = [];
136
+ const calls = s.perf?.api.calls || [];
137
+ for (const [k, cs] of groupBy(calls, callKey)) {
138
+ const max = Math.max(...cs.map((c) => c.ms));
139
+ if (max > LIMITS.apiMs)
140
+ out.push({ key: `slow-api ${k}`, level: 'warn', text: `Slow API: \`${k}\` took ${max} ms (over ${LIMITS.apiMs} ms)${cs.length > 1 ? `, slowest of ${cs.length} calls` : ''}.` });
141
+ const failed = cs.filter((c) => !c.status || c.status >= 400);
142
+ if (failed.length)
143
+ out.push({ key: `failed-api ${k}`, level: 'error', text: `Failed API call: \`${k}\` ${failed[0].status ? `returned ${failed[0].status}` : 'did not complete (network error)'}${failed.length > 1 ? ` (${failed.length} times)` : ''}.` });
144
+ const starts = cs.map((c) => c.startMs || 0).sort((a, b) => a - b);
145
+ for (let i = 0; i + LIMITS.dupCalls - 1 < starts.length; i++)
146
+ if (starts[i + LIMITS.dupCalls - 1] - starts[i] <= LIMITS.dupWindowMs) {
147
+ out.push({ key: `duplicate-calls ${k}`, level: 'warn', text: `Possible duplicate calls: \`${k}\` was called ${LIMITS.dupCalls} or more times within 1 s.` });
148
+ break;
149
+ }
150
+ }
151
+ for (const c of s.perf?.components || [])
152
+ if (c.renders > LIMITS.renders)
153
+ out.push({ key: `renders ${c.id}`, level: 'warn', text: `Panel \`${c.id}\` rendered ${c.renders} times (over ${LIMITS.renders}).` });
154
+ const fcp = s.perf?.lifecycle.firstContentfulPaintMs;
155
+ if (fcp != null && fcp > LIMITS.fcpMs)
156
+ out.push({ key: 'slow-fcp', level: 'warn', text: `Slow first paint: FCP was ${fcp} ms (over ${LIMITS.fcpMs} ms).` });
157
+ if (s.errors.length) {
158
+ const kinds = [...groupBy(s.errors, (e) => e.kind)].map(([k, es]) => plural(es.length, k === 'console' ? 'console error' : k === 'error' ? 'uncaught error' : 'unhandled rejection'));
159
+ out.push({ key: 'errors', level: 'error', text: `${kinds.join(', ')}. First: "${cut(s.errors[0].message, 120)}".` });
160
+ }
161
+ for (const [panel, is] of groupBy(s.domIssues, (d) => d.panel)) {
162
+ const counts = [...groupBy(is.flatMap((d) => d.issues), (x) => x)].map(([k, v]) => `${k} ×${v.length}`);
163
+ out.push({ key: `layout ${panel}`, level: 'warn', text: `Layout: ${plural(is.length, 'element')} in \`${panel}\` ${is.length === 1 ? 'is' : 'are'} clipped or zero-size (${counts.join(', ')}).` });
164
+ }
165
+ const worst = new Map();
166
+ for (const req of s.server?.requests || []) {
167
+ if (req.ms < LIMITS.spanMinMs)
168
+ continue;
169
+ for (const [name, sp] of groupBy(req.spans, (x) => x.name)) {
170
+ const ms = sum(sp.map((x) => x.ms));
171
+ const key = `${req.method} ${req.route} ${name}`;
172
+ if (ms >= req.ms * LIMITS.spanShare && ms > (worst.get(key)?.ms ?? -1))
173
+ worst.set(key, { req, name, ms, n: sp.length });
174
+ }
175
+ }
176
+ for (const [key, w] of worst) {
177
+ const n1 = w.n >= LIMITS.nPlusOne ? `, called ${w.n} times in one request: looks like an N+1 query, batch it` : '';
178
+ out.push({ key: `server-span ${key}`, level: 'warn', text: `Server: \`${w.name}\` took ${R(w.ms)} of ${w.req.ms} ms (${R((w.ms / w.req.ms) * 100)}%) in \`${w.req.method} ${w.req.route}\`${n1}.` });
179
+ }
180
+ return out;
181
+ }
182
+ const cut = (v, n) => {
183
+ const t = String(v ?? '').replace(/\s+/g, ' ');
184
+ return t.length > n ? t.slice(0, n - 1) + '…' : t;
185
+ };
186
+ const cell = (v) => (v === undefined || v === null || v === '' ? '' : cut(v, 160).replace(/\|/g, '\\|'));
187
+ const table = (head, rows) => rows.length ? [`| ${head.join(' | ')} |`, `|${head.map(() => '---').join('|')}|`, ...rows.map((r) => `| ${r.map(cell).join(' | ')} |`)].join('\n') : '_None._';
188
+ const ms = (v) => (v == null ? '' : `${R(v)} ms`);
189
+ /** Renders a session as plain Markdown for models and people. */
190
+ export function renderMarkdown(s, o = {}) {
191
+ const max = o.maxRows ?? 60;
192
+ const top = (a) => a.slice(0, max);
193
+ const more = (a) => (a.length > max ? `\n\n_${a.length - max} more not shown; see the JSON file._` : '');
194
+ const p = s.perf;
195
+ const l = p?.lifecycle;
196
+ const api = p?.api;
197
+ const srv = s.server;
198
+ const f = findings(s);
199
+ const out = [`# reins session: ${s.title}`, ''];
200
+ out.push(table(['Field', 'Value'], [
201
+ ['App', s.app.url],
202
+ ['Title', s.app.title],
203
+ ['Platform', s.app.platform || 'web'],
204
+ ['Recorded', s.createdAt],
205
+ ['Source', s.source === 'record' ? 'record mode (tester export)' : 'bridge (agent)'],
206
+ ['reins', s.reinsVersion],
207
+ ]));
208
+ const rows = [
209
+ ['TTFB', ms(l?.ttfbMs)],
210
+ ['First contentful paint', ms(l?.firstContentfulPaintMs)],
211
+ ['Load', ms(l?.loadMs)],
212
+ ['DOM nodes', l?.domNodes],
213
+ ['Panels', s.panels.length],
214
+ ['API calls', api ? `${api.totalCalls} (${api.failedCalls} failed)` : ''],
215
+ ['API avg / max', api ? `${api.avgMs} ms / ${api.maxMs} ms` : ''],
216
+ ['Errors', s.errors.length],
217
+ ['Layout issues', s.domIssues.length],
218
+ ];
219
+ if (srv) {
220
+ const slow = srv.slowest[0];
221
+ rows.push(['Server requests', `${srv.requests.length}${slow ? ` (slowest route \`${slow.method} ${slow.route}\`, avg ${slow.avgMs} ms, db ${slow.avgDbMs} ms)` : ''}`]);
222
+ if (srv.runtime)
223
+ rows.push(['Event loop p99', ms(srv.runtime.eventLoop.p99Ms)]);
224
+ }
225
+ out.push('', '## Summary', '', table(['Metric', 'Value'], rows));
226
+ out.push('', '## Findings', '');
227
+ out.push(f.length ? f.map((x) => `- ${x.level === 'error' ? '❌' : '⚠️'} ${x.text}`).join('\n') : '- ✅ Nothing crossed the thresholds.');
228
+ if (p?.timeline) {
229
+ const tl = p.timeline.map((e) => {
230
+ const spans = e.spans?.length
231
+ ? ' · ' + [...groupBy(e.spans, (x) => x.name)].map(([n, sp]) => `${n}${sp.length > 1 ? ` ×${sp.length}` : ''} ${R(sum(sp.map((x) => x.durationMs)))} ms`).join(', ')
232
+ : '';
233
+ return [e.atMs, e.kind, (e.kind === 'server' ? '↳ ' : '') + e.name + spans, ms(e.durationMs) + (e.dbMs ? ` (db ${R(e.dbMs)} ms)` : ''), e.status];
234
+ });
235
+ out.push('', '## Timeline', '', 'Milliseconds since the page started. Server rows sit inside the browser call they answer (joined by trace id).', '', table(['At', 'Kind', 'What', 'Duration', 'Status'], top(tl)) + more(tl));
236
+ }
237
+ const cs = api?.calls || [];
238
+ out.push('', '## API calls', '', table(['Method', 'URL', 'Status', 'Duration', 'Start', 'Trace'], top(cs).map((c) => [c.method, c.url, c.status, ms(c.ms), c.startMs, c.traceId])) + more(cs));
239
+ if (srv) {
240
+ const rt = srv.runtime;
241
+ out.push('', '## Server', '');
242
+ if (rt)
243
+ out.push(`Node ${rt.node}, up ${rt.uptimeS} s, RSS ${rt.rssMb} MB, heap ${rt.heapUsedMb} MB, event loop p50 ${rt.eventLoop.p50Ms} ms / p99 ${rt.eventLoop.p99Ms} ms / max ${rt.eventLoop.maxMs} ms.`, '');
244
+ out.push(table(['Route', 'Calls', 'Avg', 'Max', 'Avg db'], srv.slowest.map((r) => [`${r.method} ${r.route}`, r.count, ms(r.avgMs), ms(r.maxMs), ms(r.avgDbMs)])));
245
+ const reqs = [...srv.requests].sort((a, b) => b.ms - a.ms);
246
+ out.push('', '### Slowest requests', '', table(['Request', 'Status', 'Duration', 'db', 'Spans', 'Outbound', 'Trace'], top(reqs.slice(0, 15)).map((r) => [
247
+ `${r.method} ${r.url}`,
248
+ r.status,
249
+ ms(r.ms),
250
+ ms(r.dbMs),
251
+ [...groupBy(r.spans, (x) => x.name)].map(([n, sp]) => `${n}${sp.length > 1 ? ` ×${sp.length}` : ''}`).join(', '),
252
+ r.outbound.map((x) => `${x.method} ${path(x.url)} ${x.status} ${x.ms} ms`).join(', '),
253
+ r.traceId,
254
+ ])));
255
+ }
256
+ out.push('', '## Panels', '', table(['Panel', 'Renders', 'Mounted at', 'Last render', 'DOM nodes', 'Issues'], s.panels.map((x) => [x.id, x.renders, ms(x.mountedAtMs), ms(x.lastRenderMs), x.nodes, x.issues])));
257
+ out.push('', '## Errors', '', table(['Kind', 'Message'], top(s.errors).map((e) => [e.kind, e.message])) + more(s.errors));
258
+ out.push('', '## Layout issues', '', table(['Panel', 'Element', 'Issues', 'Text'], top(s.domIssues).map((d) => [d.panel, d.tag + (d.id ? '#' + d.id : ''), d.issues.join(', '), d.text])) + more(s.domIssues));
259
+ const runs = s.audit.filter((a) => a.event === 'reins_run_action' || (a.source === 'page' && a.event === 'run'));
260
+ out.push('', '## Actions run', '', table(['At', 'By', 'Action', 'Result', 'Duration'], runs.map((a) => [a.at, a.source === 'agent' ? 'agent' : a.via || a.source, a.name, a.ok === false ? `failed: ${a.error}` : 'ok', ms(a.ms)])));
261
+ if (!runs.length && p?.actions.length)
262
+ out.push('', table(['At', 'Action', 'Duration'], p.actions.map((a) => [a.startMs, a.name.replace('[reins] ', ''), ms(a.durationMs)])));
263
+ out.push('', '## Screenshots', '', s.snaps.length ? s.snaps.map((x) => `- [${x.split('/').pop()}](${o.link ? o.link(x) : x})`).join('\n') : '_None._', '');
264
+ return out.join('\n');
265
+ }
266
+ /** A file-name-safe base: `2026-11-10T06-30-00-123Z-checkout-flow`. */
267
+ export function sessionBase(createdAt, title) {
268
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'session';
269
+ return `${createdAt.replace(/[:.]/g, '-')}-${slug}`;
270
+ }
271
+ const fmtDelta = (b, a, unit) => {
272
+ if (b == null || a == null)
273
+ return '';
274
+ const d = R(a - b);
275
+ if (!d)
276
+ return '= same';
277
+ const pct = b ? ` (${d > 0 ? '+' : '−'}${R((Math.abs(d) / b) * 100)}%)` : '';
278
+ return `${d > 0 ? '▲' : '▼'} ${Math.abs(d)}${unit}${pct} ${d > 0 ? 'worse' : 'better'}`;
279
+ };
280
+ /** A Markdown diff of two sessions: metrics, API time per URL, panel renders, and findings. Lower is better for every metric. */
281
+ export function compareSessions(before, after) {
282
+ const m = (s) => {
283
+ const reqs = s.server?.requests || [];
284
+ return {
285
+ 'TTFB (ms)': s.perf?.lifecycle.ttfbMs,
286
+ 'First contentful paint (ms)': s.perf?.lifecycle.firstContentfulPaintMs,
287
+ 'Load (ms)': s.perf?.lifecycle.loadMs,
288
+ 'DOM nodes': s.perf?.lifecycle.domNodes,
289
+ 'API calls': s.perf?.api.totalCalls,
290
+ 'Failed API calls': s.perf?.api.failedCalls,
291
+ 'API avg (ms)': s.perf?.api.avgMs,
292
+ 'API max (ms)': s.perf?.api.maxMs,
293
+ Errors: s.errors.length,
294
+ 'Layout issues': s.domIssues.length,
295
+ 'Server avg (ms)': s.server ? avg(reqs.map((r) => r.ms)) : null,
296
+ 'Server db avg (ms)': s.server ? avg(reqs.map((r) => r.dbMs)) : null,
297
+ 'Event loop p99 (ms)': s.server?.runtime?.eventLoop.p99Ms ?? null,
298
+ };
299
+ };
300
+ const mb = m(before);
301
+ const ma = m(after);
302
+ const unit = (k) => (k.includes('(ms)') ? ' ms' : '');
303
+ const metricRows = Object.keys(mb)
304
+ .filter((k) => mb[k] != null || ma[k] != null)
305
+ .map((k) => [k, mb[k] ?? '', ma[k] ?? '', fmtDelta(mb[k], ma[k], unit(k))]);
306
+ const per = (s) => new Map([...groupBy(s.perf?.api.calls || [], callKey)].map(([k, cs]) => [k, avg(cs.map((c) => c.ms))]));
307
+ const pb = per(before);
308
+ const pa = per(after);
309
+ const apiRows = [...new Set([...pb.keys(), ...pa.keys()])].sort().map((k) => [k, pb.get(k) ?? '', pa.get(k) ?? '', fmtDelta(pb.get(k), pa.get(k), ' ms')]);
310
+ const rb = new Map(before.panels.map((x) => [x.id, x.renders]));
311
+ const ra = new Map(after.panels.map((x) => [x.id, x.renders]));
312
+ const panelRows = [...new Set([...rb.keys(), ...ra.keys()])].sort().map((k) => [k, rb.get(k) ?? '', ra.get(k) ?? '', fmtDelta(rb.get(k), ra.get(k), '')]);
313
+ const fb = findings(before);
314
+ const fa = findings(after);
315
+ const keys = (l) => new Set(l.map((x) => x.key));
316
+ const newFindings = fa.filter((x) => !keys(fb).has(x.key));
317
+ const resolved = fb.filter((x) => !keys(fa).has(x.key));
318
+ const still = fa.filter((x) => keys(fb).has(x.key));
319
+ const list = (l) => (l.length ? l.map((x) => `- ${x.text}`).join('\n') : '- None.');
320
+ const markdown = [
321
+ `# reins compare: ${before.title} → ${after.title}`,
322
+ '',
323
+ `Before: ${before.createdAt}. After: ${after.createdAt}. ▼ means the number went down; for every row here, lower is better.`,
324
+ '',
325
+ '## Metrics',
326
+ '',
327
+ table(['Metric', 'Before', 'After', 'Change'], metricRows),
328
+ '',
329
+ '## API time per URL (avg)',
330
+ '',
331
+ table(['Call', 'Before (ms)', 'After (ms)', 'Change'], apiRows),
332
+ '',
333
+ '## Panel renders',
334
+ '',
335
+ table(['Panel', 'Before', 'After', 'Change'], panelRows),
336
+ '',
337
+ '## Findings',
338
+ '',
339
+ '### Resolved',
340
+ '',
341
+ list(resolved),
342
+ '',
343
+ '### New',
344
+ '',
345
+ list(newFindings),
346
+ '',
347
+ '### Still open',
348
+ '',
349
+ list(still),
350
+ '',
351
+ ].join('\n');
352
+ return { markdown, newFindings, resolved };
353
+ }
@@ -0,0 +1,13 @@
1
+ export declare const TOKEN_ROUTE = "/__reins/token";
2
+ type Headers = Record<string, string | string[] | undefined>;
3
+ /**
4
+ * The dev server's answer to a token request. Reads `.reins/token` fresh each time,
5
+ * because the bridge renews it after every pairing. Cross-site browser requests are refused.
6
+ */
7
+ export declare function tokenResponse(projectDir: string, headers?: Headers): {
8
+ status: number;
9
+ body: string;
10
+ };
11
+ /** True when REINS_ENABLE=1 asks to keep reins in a non-development build (staging only). */
12
+ export declare function keepInBuild(env?: Record<string, string | undefined>): boolean;
13
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,27 @@
1
+ // Node-side helpers shared by the build plugins (@mindmakr/reins/vite, @mindmakr/reins/next, @mindmakr/reins/metro).
2
+ // Never imported by app code.
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ export const TOKEN_ROUTE = '/__reins/token';
6
+ /**
7
+ * The dev server's answer to a token request. Reads `.reins/token` fresh each time,
8
+ * because the bridge renews it after every pairing. Cross-site browser requests are refused.
9
+ */
10
+ export function tokenResponse(projectDir, headers = {}) {
11
+ const site = headers['sec-fetch-site'];
12
+ if (site && site !== 'same-origin' && site !== 'none')
13
+ return { status: 403, body: 'cross-site request refused' };
14
+ try {
15
+ const token = readFileSync(join(projectDir, '.reins', 'token'), 'utf8').trim();
16
+ if (token)
17
+ return { status: 200, body: token };
18
+ }
19
+ catch {
20
+ // no bridge yet
21
+ }
22
+ return { status: 404, body: 'no reins bridge running: start `npx reins bridge`' };
23
+ }
24
+ /** True when REINS_ENABLE=1 asks to keep reins in a non-development build (staging only). */
25
+ export function keepInBuild(env = process.env) {
26
+ return ['REINS_ENABLE', 'VITE_REINS_ENABLE', 'NEXT_PUBLIC_REINS_ENABLE', 'EXPO_PUBLIC_REINS_ENABLE'].some((k) => env[k] === '1' || env[k] === 'true');
27
+ }
package/dist/stub.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ export * from './types.js';
2
+ export declare const VERSION = "0.1.0";
3
+ export declare const devEnabled: () => boolean;
4
+ export declare function createReins(_g?: unknown): {
5
+ marker: string;
6
+ action: (def: unknown) => () => void;
7
+ state: (name: string, get: () => unknown) => () => void;
8
+ registerPanel: (id: string, measure: unknown) => () => void;
9
+ markComponent: (id: string, phase?: "mount" | "render") => void;
10
+ panel: (_id: string, _o?: unknown) => Promise<null>;
11
+ panels: () => string[];
12
+ perf: () => {
13
+ lifecycle: {
14
+ ttfbMs: null;
15
+ domInteractiveMs: null;
16
+ domContentLoadedMs: null;
17
+ loadMs: null;
18
+ firstPaintMs: null;
19
+ firstContentfulPaintMs: null;
20
+ domNodes: number;
21
+ nowMs: number;
22
+ };
23
+ components: never[];
24
+ api: {
25
+ totalCalls: number;
26
+ failedCalls: number;
27
+ avgMs: number;
28
+ maxMs: number;
29
+ slowest: null;
30
+ calls: never[];
31
+ };
32
+ actions: never[];
33
+ timeline: never[];
34
+ };
35
+ dom: (_o?: unknown) => Promise<null>;
36
+ connect: (_o?: unknown) => Promise<boolean>;
37
+ disconnect: () => void;
38
+ isPaired: () => boolean;
39
+ record: (_o?: unknown) => boolean;
40
+ actions: () => unknown[];
41
+ run: (_name: string, _input?: unknown, _o?: unknown) => Promise<never>;
42
+ snapshot: (_name?: string) => Record<string, unknown>;
43
+ network: (_limit?: number) => unknown[];
44
+ errors: (_limit?: number) => unknown[];
45
+ };
46
+ export declare const reins: {
47
+ marker: string;
48
+ action: (def: unknown) => () => void;
49
+ state: (name: string, get: () => unknown) => () => void;
50
+ registerPanel: (id: string, measure: unknown) => () => void;
51
+ markComponent: (id: string, phase?: "mount" | "render") => void;
52
+ panel: (_id: string, _o?: unknown) => Promise<null>;
53
+ panels: () => string[];
54
+ perf: () => {
55
+ lifecycle: {
56
+ ttfbMs: null;
57
+ domInteractiveMs: null;
58
+ domContentLoadedMs: null;
59
+ loadMs: null;
60
+ firstPaintMs: null;
61
+ firstContentfulPaintMs: null;
62
+ domNodes: number;
63
+ nowMs: number;
64
+ };
65
+ components: never[];
66
+ api: {
67
+ totalCalls: number;
68
+ failedCalls: number;
69
+ avgMs: number;
70
+ maxMs: number;
71
+ slowest: null;
72
+ calls: never[];
73
+ };
74
+ actions: never[];
75
+ timeline: never[];
76
+ };
77
+ dom: (_o?: unknown) => Promise<null>;
78
+ connect: (_o?: unknown) => Promise<boolean>;
79
+ disconnect: () => void;
80
+ isPaired: () => boolean;
81
+ record: (_o?: unknown) => boolean;
82
+ actions: () => unknown[];
83
+ run: (_name: string, _input?: unknown, _o?: unknown) => Promise<never>;
84
+ snapshot: (_name?: string) => Record<string, unknown>;
85
+ network: (_limit?: number) => unknown[];
86
+ errors: (_limit?: number) => unknown[];
87
+ };
88
+ export default reins;
package/dist/stub.js ADDED
@@ -0,0 +1,38 @@
1
+ // Production stand-in for `@mindmakr/reins`. Same API, does nothing, holds no dev marker.
2
+ // The Vite, Next and Expo plugins swap `@mindmakr/reins` for this file in production builds.
3
+ export * from './types.js';
4
+ const noop = () => { };
5
+ const off = () => noop;
6
+ const refuse = () => Promise.reject(new Error('reins is not in this build'));
7
+ export const VERSION = '0.1.0';
8
+ export const devEnabled = () => false;
9
+ export function createReins(_g) {
10
+ return {
11
+ marker: '',
12
+ action: off,
13
+ state: off,
14
+ registerPanel: off,
15
+ markComponent: noop,
16
+ panel: (_id, _o) => Promise.resolve(null),
17
+ panels: () => [],
18
+ perf: () => ({
19
+ lifecycle: { ttfbMs: null, domInteractiveMs: null, domContentLoadedMs: null, loadMs: null, firstPaintMs: null, firstContentfulPaintMs: null, domNodes: 0, nowMs: 0 },
20
+ components: [],
21
+ api: { totalCalls: 0, failedCalls: 0, avgMs: 0, maxMs: 0, slowest: null, calls: [] },
22
+ actions: [],
23
+ timeline: [],
24
+ }),
25
+ dom: (_o) => Promise.resolve(null),
26
+ connect: (_o) => Promise.resolve(false),
27
+ disconnect: noop,
28
+ isPaired: () => false,
29
+ record: (_o) => false,
30
+ actions: () => [],
31
+ run: (_name, _input, _o) => refuse(),
32
+ snapshot: (_name) => ({}),
33
+ network: (_limit) => [],
34
+ errors: (_limit) => [],
35
+ };
36
+ }
37
+ export const reins = createReins();
38
+ export default reins;