beatrina 0.8.6

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 (114) hide show
  1. package/LICENSE +92 -0
  2. package/NOTICES +72 -0
  3. package/README.md +124 -0
  4. package/bin/beatrina.mjs +223 -0
  5. package/bin/cli.mjs +80 -0
  6. package/bin/failsafe.mjs +74 -0
  7. package/bin/identity.mjs +45 -0
  8. package/bin/prova-post.mjs +51 -0
  9. package/bin/sessions.mjs +95 -0
  10. package/bin/shortcut.mjs +151 -0
  11. package/bin/update-check.mjs +55 -0
  12. package/bin/upgrade.mjs +76 -0
  13. package/build-info.json +1 -0
  14. package/carmar_V0.8.6.html +1310 -0
  15. package/check/acceptance.mjs +278 -0
  16. package/check/session.mjs +215 -0
  17. package/engines/js/document-exec.mjs +82 -0
  18. package/engines/js/persist.mjs +214 -0
  19. package/engines/js/worker.mjs +424 -0
  20. package/engines/python/adapter.py +577 -0
  21. package/engines/python/analyze.py +814 -0
  22. package/engines/python/bootstrap.py +309 -0
  23. package/engines/python/dataview.py +735 -0
  24. package/engines/python/debugger.py +346 -0
  25. package/engines/python/document_exec.py +158 -0
  26. package/engines/python/engine.json +28 -0
  27. package/engines/python/handoff.py +118 -0
  28. package/engines/python/worker.py +564 -0
  29. package/engines/r/engine.json +25 -0
  30. package/engines/r/handoff.R +92 -0
  31. package/failsafe/ai-policy.R +255 -0
  32. package/failsafe/ai-store.R +373 -0
  33. package/failsafe/cite.R +418 -0
  34. package/failsafe/journal.R +684 -0
  35. package/failsafe/plugins.R +809 -0
  36. package/failsafe/serve.R +5500 -0
  37. package/host/ai-policy.mjs +218 -0
  38. package/host/deployment.mjs +160 -0
  39. package/host/engine-js.mjs +98 -0
  40. package/host/engine-pool.mjs +383 -0
  41. package/host/engine-python.mjs +228 -0
  42. package/host/engine-r.mjs +206 -0
  43. package/host/engine-stdio.mjs +401 -0
  44. package/host/journal-store.mjs +749 -0
  45. package/host/main.mjs +503 -0
  46. package/host/planes/README.md +41 -0
  47. package/host/planes/ai-store.mjs +327 -0
  48. package/host/planes/ai.mjs +467 -0
  49. package/host/planes/analyze.mjs +397 -0
  50. package/host/planes/cite.mjs +517 -0
  51. package/host/planes/files.mjs +0 -0
  52. package/host/planes/jobs.mjs +704 -0
  53. package/host/planes/journal.mjs +53 -0
  54. package/host/planes/latex.mjs +201 -0
  55. package/host/planes/mcp.mjs +493 -0
  56. package/host/planes/pair.mjs +325 -0
  57. package/host/planes/pipe-term.mjs +122 -0
  58. package/host/planes/plugins.mjs +112 -0
  59. package/host/planes/proc-tree.mjs +76 -0
  60. package/host/planes/sessions.mjs +434 -0
  61. package/host/planes/settings.mjs +164 -0
  62. package/host/planes/terminal.mjs +286 -0
  63. package/host/planes/test-file.mjs +80 -0
  64. package/host/planes/update.mjs +214 -0
  65. package/host/plugin-store.mjs +838 -0
  66. package/host/server.mjs +441 -0
  67. package/host/settings.mjs +379 -0
  68. package/host/update-record.mjs +59 -0
  69. package/host/user-dirs.mjs +117 -0
  70. package/host/windows-runtime.mjs +145 -0
  71. package/host/worker-plane.mjs +713 -0
  72. package/host/ws.mjs +190 -0
  73. package/kernel/analyze.R +668 -0
  74. package/kernel/deployment.R +165 -0
  75. package/kernel/examples/NOTICE.md +38 -0
  76. package/kernel/examples/tna-complete-tutorial.Rmd +210 -0
  77. package/kernel/fileio.R +656 -0
  78. package/kernel/index.html +96 -0
  79. package/kernel/job-run.R +391 -0
  80. package/kernel/jobs.R +276 -0
  81. package/kernel/kernel-protocol +1 -0
  82. package/kernel/kernel-version +1 -0
  83. package/kernel/kernel.R +671 -0
  84. package/kernel/knitr-run.R +245 -0
  85. package/kernel/latex.R +609 -0
  86. package/kernel/mcp/carmar-mcp.mjs +516 -0
  87. package/kernel/notebook-page.R +67 -0
  88. package/kernel/plugins/csl/apa/apa.csl +2273 -0
  89. package/kernel/plugins/csl/apa/plugin.json +19 -0
  90. package/kernel/plugins/csl/chicago-author-date/chicago-author-date.csl +4216 -0
  91. package/kernel/plugins/csl/chicago-author-date/plugin.json +19 -0
  92. package/kernel/plugins/csl/harvard-cite-them-right/harvard-cite-them-right.csl +316 -0
  93. package/kernel/plugins/csl/harvard-cite-them-right/plugin.json +19 -0
  94. package/kernel/plugins/csl/ieee/ieee.csl +519 -0
  95. package/kernel/plugins/csl/ieee/plugin.json +19 -0
  96. package/kernel/plugins/csl/modern-language-association/modern-language-association.csl +1184 -0
  97. package/kernel/plugins/csl/modern-language-association/plugin.json +19 -0
  98. package/kernel/plugins/csl/nature/nature.csl +189 -0
  99. package/kernel/plugins/csl/nature/plugin.json +19 -0
  100. package/kernel/plugins/latex/apa7/apa7.json +14 -0
  101. package/kernel/plugins/latex/apa7/plugin.json +19 -0
  102. package/kernel/plugins/latex/elsarticle/elsarticle.json +14 -0
  103. package/kernel/plugins/latex/elsarticle/plugin.json +19 -0
  104. package/kernel/plugins/latex/ieeetran/ieeetran.json +10 -0
  105. package/kernel/plugins/latex/ieeetran/plugin.json +19 -0
  106. package/kernel/project.R +131 -0
  107. package/kernel/settings.R +410 -0
  108. package/kernel/sniff.R +769 -0
  109. package/kernel/worker-boot.R +22 -0
  110. package/kernel/worker.R +3496 -0
  111. package/lib/agent-authoring-contract.js +547 -0
  112. package/lib/cell-kinds.js +108 -0
  113. package/lib/engine-labels.js +324 -0
  114. package/package.json +32 -0
@@ -0,0 +1,309 @@
1
+ """bootstrap.py — code executed silently inside the ipykernel at startup.
2
+
3
+ Ported from `../carmar-python/py/carmapy/bootstrap.py` (read 2026-09-14), with
4
+ one addition: `__carmapy_reply` now also answers `env`, so the notebook's
5
+ Environment pane has something to show for a Python session.
6
+
7
+ This is how a pandas DataFrame becomes Beatrix's `dataframe` frame rather than
8
+ a blob of HTML: we register an IPython display formatter for a private MIME
9
+ type, so the ordinary displayhook — the thing Jupyter already does well — hands
10
+ the adapter a payload already in the frame's shape.
11
+
12
+ Borrowing the MIME-bundle displayhook instead of re-implementing a displayhook
13
+ is the entire reason the Python worker is 600 lines rather than 3,000.
14
+
15
+ Two rules copied from `emit_dataframe` in `spike/worker.R`, because they are
16
+ correctness, not polish:
17
+
18
+ * A returned value is a NOTEBOOK PREVIEW, not the data viewer. Bounding rows
19
+ alone is insufficient — one cell can hold a fitted model or megabytes of
20
+ text — so complex cells are flattened to a description and text is clipped,
21
+ then a final wire-size ceiling is enforced.
22
+ * The preview is 200 rows, because the page's own ROWS_PER_PAGE is 25 and a
23
+ smaller preview makes the row navigator unreachable.
24
+ """
25
+
26
+ import base64 as _base64
27
+ from pathlib import Path as _Path
28
+
29
+ CARMAR_MIME = "application/vnd.carmar.dataframe+json"
30
+ # The reply channel, and the kernel's own housekeeping, each on their own
31
+ # private MIME. Two, not one: a `view` reply is a REPLY the worker is waiting
32
+ # on by wire id, while the cwd is UNSOLICITED state the worker records and
33
+ # never forwards. Folding them together would make "is anyone waiting for
34
+ # this?" a field rather than a type.
35
+ REPLY_MIME = "application/vnd.carmapy.reply+json"
36
+ CONTROL_MIME = "application/vnd.carmapy.control+json"
37
+
38
+ # `dataview.py` is shipped into the kernel as SOURCE, base64-encoded.
39
+ #
40
+ # Source rather than an import, because the kernel may be any Python
41
+ # environment the user chose and Beatrix's engine package need not be installed
42
+ # in it. Base64 rather than an embedded string literal, because the bootstrap
43
+ # is Python source, so it eventually contains a quote, a backslash or a `%`,
44
+ # and one of those breaks the template the first time it appears.
45
+ _DATAVIEW_B64 = _base64.b64encode(
46
+ _Path(__file__).with_name("dataview.py").read_bytes()).decode()
47
+ # `debugger.py`, the same way and for the same reasons. Installed EAGERLY, not
48
+ # on first use: it names the next cell after its chunk (the breakpoint address),
49
+ # and a function defined before the first breakpoint was set must already carry
50
+ # that name, exactly as R parses every chunk under its srcname whether or not
51
+ # anyone is debugging.
52
+ _DEBUGGER_B64 = _base64.b64encode(
53
+ _Path(__file__).with_name("debugger.py").read_bytes()).decode()
54
+ # `handoff.py` (share/use), the same way: standard library only.
55
+ _HANDOFF_B64 = _base64.b64encode(
56
+ _Path(__file__).with_name("handoff.py").read_bytes()).decode()
57
+
58
+ BOOTSTRAP = r'''
59
+ def __carmar_install_formatters():
60
+ import json
61
+
62
+ PREVIEW_ROWS = 200
63
+ MAX_CELL_CHARS = 512
64
+ MAX_WIRE_BYTES = 512 * 1024
65
+
66
+ def _cell(v):
67
+ """Flatten anything that is not a plain scalar to a description."""
68
+ if v is None:
69
+ return None
70
+ if isinstance(v, bool) or isinstance(v, int):
71
+ return v
72
+ if isinstance(v, float):
73
+ # NaN/Inf are not JSON; the page reads null the way R's na="null" does.
74
+ return v if v == v and v not in (float("inf"), float("-inf")) else None
75
+ if isinstance(v, str):
76
+ return v if len(v) <= MAX_CELL_CHARS else v[:MAX_CELL_CHARS] + "…"
77
+ try:
78
+ import numpy as np
79
+ if isinstance(v, np.generic):
80
+ return _cell(v.item())
81
+ except Exception:
82
+ pass
83
+ s = repr(v)
84
+ if len(s) > MAX_CELL_CHARS:
85
+ s = s[:MAX_CELL_CHARS] + "…"
86
+ return s
87
+
88
+ def _frame(df):
89
+ total_rows, total_cols = int(df.shape[0]), int(df.shape[1])
90
+ head = df.head(PREVIEW_ROWS)
91
+ cols = [str(c) for c in head.columns]
92
+ types = [str(t) for t in head.dtypes]
93
+ rows = [{cols[i]: _cell(v) for i, v in enumerate(rec)}
94
+ for rec in head.itertuples(index=False, name=None)]
95
+
96
+ payload = {
97
+ "columns": cols,
98
+ "types": types,
99
+ "nrow": total_rows,
100
+ "ncol": total_cols,
101
+ "shown": len(rows),
102
+ "shownCols": len(cols),
103
+ "truncated": total_rows > len(rows) or total_cols > len(cols),
104
+ }
105
+
106
+ # Final wire ceiling. Halving is what R did and it is wrong from a big
107
+ # head (it costs a third of the rows that would have fit), so estimate
108
+ # the per-row cost and keep what fits, as view_payload does.
109
+ body = json.dumps({**payload, "rows": rows})
110
+ if len(body.encode()) > MAX_WIRE_BYTES and rows:
111
+ per = len(body.encode()) / max(1, len(rows))
112
+ keep = max(1, int(MAX_WIRE_BYTES // per))
113
+ rows = rows[:keep]
114
+ payload["shown"] = len(rows)
115
+ payload["truncated"] = True
116
+ payload["rows"] = rows
117
+ return json.dumps(payload)
118
+
119
+ try:
120
+ ip = get_ipython()
121
+ except NameError:
122
+ return
123
+ fmts = ip.display_formatter.formatters
124
+ if "__CARMAR_MIME__" not in fmts:
125
+ from IPython.core.formatters import BaseFormatter
126
+ from traitlets import Unicode, ObjectName
127
+ class _CarmarDF(BaseFormatter):
128
+ format_type = Unicode("__CARMAR_MIME__")
129
+ print_method = ObjectName("_repr_carmar_df_")
130
+ _return_type = str
131
+ fmts["__CARMAR_MIME__"] = _CarmarDF(parent=ip.display_formatter)
132
+ # pandas 3.0 sets DataFrame.__module__ = "pandas"; every older example in
133
+ # the ecosystem says "pandas.core.frame", which on pandas 3.x matches
134
+ # NOTHING and fails silently — the formatter registers, reports active, and
135
+ # is never called. Register both spellings; a deferred name that never
136
+ # resolves costs nothing.
137
+ for mod in ("pandas", "pandas.core.frame"):
138
+ fmts["__CARMAR_MIME__"].for_type_by_name(mod, "DataFrame", _frame)
139
+
140
+ # ---- figures -------------------------------------------------------
141
+ # The R worker captures a plot without being asked; Python must too, or
142
+ # `plt.plot(...)` produces nothing and the user is told to type a magic.
143
+ # Both halves are LAZY: neither imports matplotlib, so a session that never
144
+ # plots pays nothing.
145
+ #
146
+ # (1) a PNG formatter for Figure, so an explicitly displayed figure renders
147
+ # (2) a post-cell hook that shows figures the user created but never
148
+ # displayed — what %matplotlib inline does, without the magic
149
+
150
+ def _fig_png(fig):
151
+ import io, base64
152
+ buf = io.BytesIO()
153
+ fig.savefig(buf, format="png", dpi=96, bbox_inches="tight")
154
+ return base64.b64encode(buf.getvalue()).decode()
155
+
156
+ png = fmts.get("image/png")
157
+ if png is not None:
158
+ png.for_type_by_name("matplotlib.figure", "Figure", _fig_png)
159
+
160
+ def _flush_figures(*a, **k):
161
+ import sys
162
+ plt = sys.modules.get("matplotlib.pyplot")
163
+ if plt is None:
164
+ return
165
+ try:
166
+ from IPython.display import display
167
+ for num in plt.get_fignums():
168
+ display(plt.figure(num))
169
+ plt.close("all")
170
+ except Exception:
171
+ pass
172
+
173
+ ip.events.register("post_run_cell", _flush_figures)
174
+
175
+ # ---- the working directory, reported without being asked --------------
176
+ # Every relative path the host resolves is resolved against the WORKER's
177
+ # cwd, and an `os.chdir()` inside a cell moves it. R gets this free — its
178
+ # worker prints getwd() into the done frame it is already building. Here
179
+ # the frame is built outside the kernel, so the kernel has to volunteer the
180
+ # value, and post_run_cell is the one place it is certainly current: the
181
+ # cell that moved it has finished.
182
+ def _carmapy_cwd(*a, **k):
183
+ import os, json
184
+ from IPython.display import publish_display_data
185
+ publish_display_data({"__CARMAPY_CONTROL__": json.dumps({"cwd": os.getcwd()})})
186
+
187
+ ip.events.register("post_run_cell", _carmapy_cwd)
188
+
189
+ # ---- the data viewer, and the environment listing ----------------------
190
+ # `dataview.py`'s source, exec'd on FIRST USE and never at boot. Lazy so a
191
+ # session that never opens the viewer does not pay pandas' import to start.
192
+ state = {"ns": None}
193
+
194
+ def _dataview():
195
+ if state["ns"] is None:
196
+ import base64
197
+ ns = {"__name__": "carmapy_dataview"}
198
+ exec(compile(base64.b64decode("__CARMAPY_DATAVIEW_B64__").decode("utf-8"),
199
+ "<beatrina dataview>", "exec"), ns)
200
+ state["ns"] = ns
201
+ return state["ns"]
202
+
203
+ def _env_rows(user_ns):
204
+ """The Environment pane's rows, in `env`'s shape (spike/worker.R).
205
+
206
+ One row per binding the USER made: IPython's own names (`In`, `Out`,
207
+ `_i3`, `get_ipython`), dunders and this module's own helpers are not
208
+ the user's variables and would bury them. `bytes` is sys.getsizeof,
209
+ which is the object's own footprint and not a deep size — R's
210
+ object.size is not a deep size either, and claiming one would be worse
211
+ than reporting the shallow one both languages actually have.
212
+ """
213
+ import sys, types
214
+ hidden = {"In", "Out", "get_ipython", "exit", "quit", "open", "share", "use"}
215
+ rows = []
216
+ for name, value in list(user_ns.items()):
217
+ if name.startswith("_") or name in hidden or name.startswith("__carmapy"):
218
+ continue
219
+ # An `import pandas as pd` IS a binding the user made, but a pane
220
+ # that lists every module buries the three variables the notebook
221
+ # is about. R's Environment pane has the same rule by accident —
222
+ # a package is attached, not bound — so this makes it explicit.
223
+ if isinstance(value, types.ModuleType):
224
+ continue
225
+ cls = type(value).__name__
226
+ try:
227
+ shape = getattr(value, "shape", None)
228
+ if shape is not None:
229
+ dims = "×".join(str(int(d)) for d in shape)
230
+ elif isinstance(value, (list, tuple, dict, set, str, bytes)):
231
+ dims = str(len(value))
232
+ else:
233
+ dims = ""
234
+ except Exception:
235
+ dims = ""
236
+ try:
237
+ summary = repr(value)
238
+ if len(summary) > 120:
239
+ summary = summary[:120] + "…"
240
+ except Exception:
241
+ summary = "<unrepresentable>"
242
+ kind = "function" if callable(value) else "value"
243
+ try:
244
+ nbytes = int(sys.getsizeof(value))
245
+ except Exception:
246
+ nbytes = 0
247
+ rows.append({"name": name, "class": cls, "dims": dims,
248
+ "bytes": nbytes, "summary": summary, "kind": kind})
249
+ rows.sort(key=lambda r: r["name"])
250
+ return {"objects": rows}
251
+
252
+ def __carmapy_reply(op, spec_json):
253
+ """Answer one request into a private MIME on iopub.
254
+
255
+ NOT a return value and not stdout. A display_data message carries the
256
+ parent msg_id, so the worker can match the answer to the request it
257
+ sent — and user code printing the same text cannot be mistaken for a
258
+ reply, which is the property R buys with its 24-character sentinel.
259
+ """
260
+ import json
261
+ from IPython.display import publish_display_data
262
+ try:
263
+ spec = json.loads(spec_json)
264
+ if op == "env":
265
+ payload = _env_rows(get_ipython().user_ns)
266
+ elif op == "rm":
267
+ ns = get_ipython().user_ns
268
+ names = spec.get("names") or []
269
+ if isinstance(names, str):
270
+ names = [names]
271
+ for nm in names:
272
+ ns.pop(nm, None)
273
+ payload = {"removed": True}
274
+ else:
275
+ dv = _dataview()
276
+ fn = dv["view_reply"] if op == "view" else dv["colstats_reply"]
277
+ payload = fn(spec, get_ipython().user_ns)
278
+ except Exception as exc:
279
+ # An error FIELD, never a raised exception: pointing the viewer at
280
+ # a 40-column frame of fitted models is a normal thing to do by
281
+ # accident, and it must cost a message, not the session.
282
+ payload = {"error": "%s: %s" % (type(exc).__name__, exc)}
283
+ publish_display_data({"__CARMAPY_REPLY__": json.dumps(payload, allow_nan=False)})
284
+
285
+ ip.user_ns["__carmapy_reply"] = __carmapy_reply
286
+
287
+ # ---- the debugger (docs/wp/wp8b-python-debugger.md) ------------------
288
+ import base64 as _b64
289
+ dbg = {"__name__": "carmar_debugger"}
290
+ exec(compile(_b64.b64decode("__CARMAR_DEBUGGER_B64__").decode("utf-8"),
291
+ "<beatrina debugger>", "exec"), dbg)
292
+ dbg["install"](ip)
293
+ ip.user_ns["__carmar_debug_prepare"] = dbg["prepare"]
294
+
295
+ # ---- share / use (engines/python/handoff.py) -------------------------
296
+ hand = {"__name__": "beatrix_handoff"}
297
+ exec(compile(_b64.b64decode("__BEATRIX_HANDOFF_B64__").decode("utf-8"),
298
+ "<beatrina handoff>", "exec"), hand)
299
+ ip.user_ns["share"] = hand["share"]
300
+ ip.user_ns["use"] = hand["use"]
301
+
302
+ __carmar_install_formatters()
303
+ del __carmar_install_formatters
304
+ '''.replace("__CARMAR_MIME__", CARMAR_MIME) \
305
+ .replace("__CARMAPY_REPLY__", REPLY_MIME) \
306
+ .replace("__CARMAPY_CONTROL__", CONTROL_MIME) \
307
+ .replace("__CARMAPY_DATAVIEW_B64__", _DATAVIEW_B64) \
308
+ .replace("__CARMAR_DEBUGGER_B64__", _DEBUGGER_B64) \
309
+ .replace("__BEATRIX_HANDOFF_B64__", _HANDOFF_B64)