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,547 @@
1
+ // agent-authoring-contract.js — the one place that says how a model may write
2
+ // into a CarmaR document, and the only one that decides whether it did.
3
+ //
4
+ // WHY THIS IS A MODULE AND NOT A PROMPT. The rules below were previously
5
+ // stated in three unsynchronised places — the MCP server's initialize text,
6
+ // the in-browser system prompts, and each tool's description — and stated is
7
+ // all they were. A model that ignored "never bury runnable R inside a text
8
+ // cell's code fence" was not stopped by anything; the sentence was advice.
9
+ // Instructions teach the workflow, schemas restrict the shape, and THIS decides
10
+ // whether a proposal is valid. Only the third one is enforcement.
11
+ //
12
+ // Nothing here touches the DOM, the notebook, or the network, because
13
+ // `tools/mcp/carmar-mcp.mjs` imports it in Node (outside the browser bundle)
14
+ // and `lib/mcp-bridge.js` imports it in the page. One module, both sides, so
15
+ // the rule an agent is told and the rule it is held to cannot drift apart.
16
+
17
+ import { CODE_KIND, isCodeKind } from "./cell-kinds.js";
18
+
19
+ export const CONTRACT = "carmar.authoring/v1";
20
+ export const SCHEMA_VERSION = 1;
21
+
22
+ /** A browser answer may still be reviewed as one bounded user-requested set. */
23
+ export const MAX_BLOCKS = 24;
24
+
25
+ /** The whole block vocabulary. `r` runs; `text` is prose. */
26
+ export const BLOCK_KINDS = ["r", "text"];
27
+
28
+ /**
29
+ * The model-visible workflow. Kept as sentences rather than one blob so a
30
+ * caller can take the prefix it needs: Codex reads only the first 512
31
+ * characters of the MCP instructions when deciding which server tools belong
32
+ * in a workflow, so the one-block authoring rule has to live in sentence one.
33
+ */
34
+ export const AUTHORING_RULES = [
35
+ // The wording and the available tool now agree: terminal agents author one
36
+ // document unit at a time. The removed batch tool changed the model's unit
37
+ // of composition from "this notebook block" to "my reply, split into an
38
+ // array", which is how chat scaffolding reached saved documents.
39
+ "CarmaR is the user's live R notebook. Start with notebook_read. Build the analysis ONE "
40
+ + "document block at a time: call chunk_insert once for each finished prose section or runnable "
41
+ + "R step, in reading and execution order. AUTHOR every text block as part of the saved .qmd, "
42
+ + "not as a chat reply. Never paste or divide your chat response into notebook blocks. Keep "
43
+ + "explanation in text blocks and executable R in r blocks—never bury runnable R in prose.",
44
+
45
+ "Use the address returned by chunk_insert for chunk_run. After inserting an R chunk, run it "
46
+ + "before authoring any later block that depends on its result; do not pre-write claimed results. "
47
+ + "Runs are visible in the notebook. If the user requested authoring only, do not run.",
48
+
49
+ "Inserted cells remain marked as pending until the user decides. You may run pending R cells, "
50
+ + "but never claim that the user kept or accepted them unless a later notebook_read no longer "
51
+ + "reports review pending.",
52
+
53
+ "Preserve execution order: setup/import, transformation, model, then output or visualization. "
54
+ + "Put prose immediately before the code it explains. Use the revisionId returned by each "
55
+ + "chunk_insert as base_revision for the next call.",
56
+
57
+ "A text block is DOCUMENT PROSE, in the notebook's voice. Write headings, sentences and lists "
58
+ + "as they should appear in the finished .qmd. Never carry chat formatting into it: no banner "
59
+ + "rules, no box-drawing lines, no \"Insight\"/\"Note to self\" framing devices, no addressing "
60
+ + "the reader as if answering a message. The notebook is a document someone else will read, "
61
+ + "not a transcript of your reply.",
62
+
63
+ "notebook_read reports the document's identity, source format and revisionId. Pass that "
64
+ + "revisionId as base_revision on every insert so a notebook the user has changed underneath "
65
+ + "you is refused rather than written to; each insert returns the new revisionId to use next. "
66
+ + "Never change the source format — it is the user's decision, and it is recorded on the "
67
+ + "document.",
68
+
69
+ "To revise an existing block, call chunk_update with the current document revision and the "
70
+ + "complete replacement source. Preserve the block's kind. An update is provisional and "
71
+ + "reversible until the user keeps or rejects it; never describe a proposed edit only in chat "
72
+ + "when the user asked you to change the document.",
73
+
74
+ // Last, deliberately: the 512-character prefix above is the one-block rule,
75
+ // and a session that runs only R needs nothing from this sentence.
76
+ "A code block is R unless chunk_insert names another `engine`. notebook_read lists the engines the "
77
+ + "attached session runs in `engines` and marks each chunk with its own `engine`; write a chunk in "
78
+ + "another engine only when that list contains it. Engines do not share objects — each runs in its "
79
+ + "own process — so pass data between them through a file.",
80
+ ];
81
+
82
+ /** The MCP `initialize` instructions, and any other single-string surface. */
83
+ export function authoringInstructions() {
84
+ return AUTHORING_RULES.join(" ");
85
+ }
86
+
87
+ // A BRACED fence is an executable chunk: ```{r}, ```{r label}, ```{r, echo=FALSE}.
88
+ // An unbraced ```r fence is plain Markdown and runs nowhere — which is exactly
89
+ // the distinction lib/qmd.js preserves as `braced`, so the two agree about what
90
+ // "executable" means rather than each having an opinion.
91
+ const BRACED_R = /^\{\s*[rR][\s,}]/;
92
+ const PLAIN_R = /^[rR]$/;
93
+ const FENCE_LINE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
94
+
95
+ /**
96
+ * The info strings of this content's TOP-LEVEL fences.
97
+ *
98
+ * Nesting is the whole reason this is a scan and not a regex. An answer that
99
+ * TEACHES chunk syntax puts a ```{r} inside an outer ```` fence, and that inner
100
+ * line is content, not a chunk — refusing it would mean the notebook cannot
101
+ * explain itself. A rule that fires on correct input gets switched off by the
102
+ * first person it annoys, so the false-positive rate has to be zero.
103
+ */
104
+ function topLevelFenceInfo(content) {
105
+ const info = [];
106
+ let open = null;
107
+ String(content).split("\n").forEach((line) => {
108
+ const match = FENCE_LINE.exec(line);
109
+ if (!match) return;
110
+ const [, marker, rest] = match;
111
+ if (open) {
112
+ // A closing fence: same character, at least as long, nothing trailing.
113
+ if (marker[0] === open[0] && marker.length >= open.length && !rest.trim()) open = null;
114
+ return;
115
+ }
116
+ open = marker;
117
+ info.push(rest.trim());
118
+ });
119
+ return info;
120
+ }
121
+ // An R block whose source IS a Markdown fence: the model wrapped its code the
122
+ // way it would in chat. R cannot parse a backtick fence, so the chunk is dead
123
+ // on arrival — and the failure looks like a syntax error in the user's code.
124
+ const WRAPPED_IN_FENCE = /^[ \t]*(?:`{3,}|~{3,})/;
125
+
126
+ // ── chat furniture ───────────────────────────────────────────────────────────
127
+ //
128
+ // An assistant's TERMINAL voice is decorated: banner rules, "★ Insight ────"
129
+ // headers, a matching rule to close. In a chat window that is a reading aid.
130
+ // Written into a notebook it is a document wearing someone else's conversation
131
+ // — it survives the save, lands in the .qmd, and the author has to delete it by
132
+ // hand from work they did not write.
133
+ //
134
+ // Only HORIZONTAL rule characters count. A model drawing an actual diagram uses
135
+ // corners and verticals (┌ │ └), and that is legitimate content; a run of three
136
+ // or more horizontal line characters is never prose.
137
+ const RULE_CHARS = "─━═┄┅┈┉╌╍";
138
+ const BARE_RULE = new RegExp(`^[${RULE_CHARS}]{3,}$`);
139
+ // A labelled banner: a short label, then the rule. "★ Insight ─────────".
140
+ const LABELLED_RULE = new RegExp(`^\\S[^${RULE_CHARS}]{0,40}?[${RULE_CHARS}]{3,}$`);
141
+
142
+ /**
143
+ * A line with its Markdown wrapping removed, for rule-matching only.
144
+ *
145
+ * The first version matched the bare form and missed every real case, because
146
+ * assistants emit the banner INSIDE a code span — `` `★ Insight ────────` `` —
147
+ * and a trailing backtick meant the line no longer ended in rule characters.
148
+ * The pattern was written from a screenshot of the rendered block instead of
149
+ * from the source, which is exactly the mistake it exists to catch.
150
+ */
151
+ const unwrapLine = (line) => String(line).replace(/^[\s`*_~>]+/, "").replace(/[\s`*_~]+$/, "");
152
+
153
+ /**
154
+ * Remove banner rules from prose, keeping what they framed.
155
+ *
156
+ * Stripping rather than refusing, deliberately: the decoration wraps REAL
157
+ * content — the sentence inside the banner is the useful part. Refusing the
158
+ * whole transaction over a cosmetic line would throw the analysis away, and
159
+ * silently keeping it puts it in the author's document. So the lines come out,
160
+ * the prose stays, and the reply says what was removed.
161
+ *
162
+ * @returns {{text: string, removed: number}}
163
+ */
164
+ export function stripChatDecoration(content) {
165
+ const lines = String(content == null ? "" : content).split("\n");
166
+ const kept = lines.filter((line) => {
167
+ const bare = unwrapLine(line);
168
+ return !(BARE_RULE.test(bare) || LABELLED_RULE.test(bare));
169
+ });
170
+ const removed = lines.length - kept.length;
171
+ if (!removed) return { text: String(content == null ? "" : content), removed: 0 };
172
+ // Collapse the blank lines the banners leave behind, and trim the ends, so a
173
+ // stripped block does not arrive padded with the gaps its frame occupied.
174
+ const text = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
175
+ return { text, removed };
176
+ }
177
+
178
+ // ── what an agent gets wrong, enumerated ─────────────────────────────────────
179
+ //
180
+ // The first version of this module held five rules, all of them copied from
181
+ // the plan's validation section, all of them prohibitions on STRUCTURE. It had
182
+ // no positive model of the artifact — nothing saying a text block is document
183
+ // prose a stranger will read — so an assistant's chat decoration walked
184
+ // straight through and into a saved .qmd. A contract made only of "not X"
185
+ // catches exactly the X's somebody already named.
186
+ //
187
+ // So the failure space is written down here instead of assumed, and each entry
188
+ // is placed by one rule:
189
+ //
190
+ // REFUSE what CANNOT WORK OR IS NOT DOCUMENT PROSE — a fence-wrapped chunk,
191
+ // a pasted console prompt, a duplicate label, assistant/process
192
+ // register. The model can repair these without the host guessing how
193
+ // to rewrite a sentence that also carries real analytical facts.
194
+ // STRIP what is decoration AROUND content and safely separable. The prose
195
+ // is the useful part; throwing the transaction away over a frame
196
+ // would cost the author their analysis.
197
+ // LINT what works but may be intentional — an install, a setwd, pasted
198
+ // output. The user reviews every block before it is theirs, so the
199
+ // honest move is to SAY so rather than guess at authorization.
200
+ //
201
+ // Nothing here guesses at intent, and nothing calls another model.
202
+
203
+ /**
204
+ * Assistant register: talking TO the reader instead of writing the document.
205
+ *
206
+ * Each pattern carries its own plain-English `quote`. The first version built
207
+ * that string by stripping punctuation out of the regex source, which produced
208
+ * "would you lke me to" and "hope ths|that helps" — user-facing text must be
209
+ * written, not derived from the thing that matched it.
210
+ *
211
+ * `['’]` throughout: models emit curly apostrophes constantly, and a rule that
212
+ * only knows the straight one silently misses most of what it is looking for.
213
+ */
214
+ const ASSISTANT_VOICE = [
215
+ { re: /\blet me know\b/i, quote: "let me know" },
216
+ { re: /\bwould you like me to\b/i, quote: "would you like me to" },
217
+ { re: /\bhere['’]?s what i (found|did)\b/i, quote: "here's what I found" },
218
+ { re: /\bi hope (this|that) helps\b/i, quote: "I hope this helps" },
219
+ { re: /\bfeel free to\b/i, quote: "feel free to" },
220
+ { re: /\bas requested\b/i, quote: "as requested" },
221
+ {
222
+ re: /\bi\s?['’]?ve (added|created|inserted|updated|written)\b/i,
223
+ quote: "I've added / created / inserted",
224
+ },
225
+ {
226
+ re: /\bi (ran|tested|verified|checked|executed)\b/i,
227
+ quote: "I ran / tested / verified",
228
+ },
229
+ { re: /\byour installed\b/i, quote: "your installed" },
230
+ { re: /\bshall i\b/i, quote: "shall I" },
231
+ ];
232
+
233
+ // Deictic notebook-UI language has no stable meaning in a saved document.
234
+ // "That last block" depends on a vanished conversation and also calls an R
235
+ // chunk by the browser's generic internal noun. Refuse it so the author can
236
+ // name the actual step or chunk; do not silently substitute a possibly wrong
237
+ // reference.
238
+ const UI_REGISTER = /\b(?:this|that|last|previous|next)\s+(?:code\s+)?block\b/i;
239
+
240
+ // Recipe voice is still chat voice even when it avoids "I". These are the
241
+ // forms seen in real leaked prose: "without you assembling anything", "you
242
+ // place and run them", and "your notebook/session". A saved methods document
243
+ // names the analyst, reader, R session, object, or step; it does not address
244
+ // the person who happened to ask the pane a question.
245
+ const SECOND_PERSON = [
246
+ { re: /\bwithout you\b/i, quote: "without you" },
247
+ {
248
+ re: /\byou\s+(?:can|could|should|must|need|may|might|will|would|have|want|place|run|decide|use|inspect|assemble|copy|paste|add|edit|change|choose|see)\b/i,
249
+ quote: "you can / should / need",
250
+ },
251
+ {
252
+ re: /\byour\s+(?:notebook|document|session|installation|installed|code|analysis|model|result|output|data|machine|project|file)\b/i,
253
+ quote: "your notebook / session / analysis",
254
+ },
255
+ ];
256
+
257
+ /**
258
+ * The agent narrating its OWN affordances — "I can't place or run chunks from
259
+ * this pane", "each block below gets a Make this a chunk button", "I have not
260
+ * run any of it". A reader opening the .qmd next year has no pane, no buttons
261
+ * and no conversation with anyone.
262
+ *
263
+ * These are REMOVED, not linted, and a whole paragraph at a time. Linting them
264
+ * was the first attempt and a Playwright run through the real answer door
265
+ * showed why it was not enough: a note in the tool reply does not stop the
266
+ * sentence landing in the document, which is the entire complaint. A sentence
267
+ * about a button is never part of the analysis, so the paragraph goes whole;
268
+ * mixed assistant/process prose is refused below because deleting part of a
269
+ * factual paragraph would be an unsafe rewrite.
270
+ */
271
+ const TOOLING_TALK = [
272
+ /\bi (can['’]?t|cannot|can not) (place|run|insert|add|create|execute)\b/i,
273
+ /\b(this|the) pane\b/i,
274
+ /\bmake this a chunk\b/i,
275
+ /\beach (block|step) below\b/i,
276
+ /\bi have not run (any of )?(it|them|this)\b/i,
277
+ /\b(button|buttons) (above|below)\b/i,
278
+ /\bone step per block\b/i,
279
+ ];
280
+
281
+ /**
282
+ * Drop paragraphs that talk about the tool instead of the analysis.
283
+ *
284
+ * Paragraph-at-a-time because that is the unit these arrive in: an assistant's
285
+ * closing "here is how to use what I just gave you" is its own block of prose,
286
+ * and cutting a sentence out of the middle of a real paragraph would be the
287
+ * riskier edit.
288
+ *
289
+ * @returns {{text: string, removed: number}}
290
+ */
291
+ export function stripToolingTalk(content) {
292
+ const source = String(content == null ? "" : content);
293
+ const paragraphs = source.split(/\n{2,}/);
294
+ const kept = paragraphs.filter((para) => !TOOLING_TALK.some((re) => re.test(para)));
295
+ const removed = paragraphs.length - kept.length;
296
+ if (!removed) return { text: source, removed: 0 };
297
+ return { text: kept.join("\n\n").replace(/\n{3,}/g, "\n\n").trim(), removed };
298
+ }
299
+
300
+ // `[1] 42` is a printed R value. In a TEXT block it is output pasted as a
301
+ // picture of a result — the notebook runs the chunk and shows the real thing.
302
+ // A leading `> ` is deliberately NOT checked here: in Markdown that is a
303
+ // blockquote, and quoting is ordinary prose.
304
+ const PASTED_VALUE = /^\s*\[\d+\]\s+\S/m;
305
+
306
+ // In R SOURCE, a leading `> ` or `[1] ` is a transcript someone copied out of
307
+ // a console. Neither parses — `> x <- 1` is a syntax error — so this is a
308
+ // refusal, not a note. `#> ...` (the reprex convention) starts with `#` and is
309
+ // a comment, so it is untouched.
310
+ const CONSOLE_TRANSCRIPT = /^\s*(>\s+\S|\[\d+\]\s+\S)/m;
311
+
312
+ /** Side effects on the user's machine that a chunk should not quietly carry. */
313
+ const SIDE_EFFECTS = [
314
+ { re: /\binstall\.packages\s*\(/, what: "installs packages into the user's R library" },
315
+ { re: /\bsetwd\s*\(/, what: "changes the working directory for the whole session" },
316
+ { re: /\brm\s*\(\s*list\s*=\s*ls\s*\(/, what: "erases every object in the user's session" },
317
+ { re: /\bunlink\s*\(|\bfile\.remove\s*\(/, what: "deletes files from disk" },
318
+ ];
319
+
320
+ // A path out of the project, in a string. `~` alone is not checked: in R it is
321
+ // the formula operator, and `y ~ x` is not a path.
322
+ const ABSOLUTE_PATH = /["'](?:\/Users\/|\/home\/|~\/|[A-Za-z]:[\\/])/;
323
+
324
+ // ── the same checks, for a Python block ─────────────────────────────────────
325
+ // A code block's rules are about the LANGUAGE, so each engine brings its own.
326
+ // R's above are unchanged; an engine without a row here gets the checks that
327
+ // are language-free (a Markdown fence around the source, an absolute path).
328
+ const ENGINE_RULES = {
329
+ python: {
330
+ // `>>> ` is the Python REPL prompt. A block made of it is a transcript,
331
+ // and Python cannot parse the prompt any more than R can parse `> `.
332
+ transcript: /^\s*>>>\s/m,
333
+ transcriptWhat: "a Python console TRANSCRIPT — lines beginning `>>> `",
334
+ sideEffects: [
335
+ { re: /(^|\n)\s*[!%]\s*pip\s+install\b|\bpip\.main\s*\(|["']pip["']\s*,\s*["']install["']/, what: "installs packages into the user's Python environment" },
336
+ { re: /\bos\.chdir\s*\(/, what: "changes the working directory for the whole session" },
337
+ { re: /\bshutil\.rmtree\s*\(|\bos\.(?:remove|unlink|rmdir)\s*\(/, what: "deletes files from disk" },
338
+ ],
339
+ },
340
+ };
341
+
342
+ /** `#| label: fig-x` — the label a Quarto chunk carries in its body. */
343
+ const HASH_PIPE_LABEL = /^\s*#\|\s*label\s*:\s*(\S+)/m;
344
+
345
+ /** An error a model can act on: what is wrong, where, and what to do instead. */
346
+ function repair(message) {
347
+ return Object.assign(new Error(message), { code: "contract_violation", contract: CONTRACT });
348
+ }
349
+
350
+ const ordinal = (index) => `block ${index + 1}`;
351
+
352
+ /**
353
+ * Validate and normalize a proposed sequence of blocks.
354
+ *
355
+ * Hard rules throw — the proposal does not reach the notebook. Softer
356
+ * conventions come back as `lints`, reported to the model in the reply so it
357
+ * can do better next time without the write being refused.
358
+ *
359
+ * @param {Array<{kind?: string, content?: string}>} raw
360
+ * @param {Object} [opts]
361
+ * @param {string} [opts.tool] the tool name, for messages that name it
362
+ * @param {number} [opts.max] block ceiling (a single-block tool passes 1)
363
+ * @returns {{blocks: Array<{kind: 'code'|'md', content: string}>, lints: string[]}}
364
+ * @throws {Error} with `code: "contract_violation"` and a repair message
365
+ */
366
+ export function normalizeBlocks(raw, { tool = "This answer", max = MAX_BLOCKS, engine = "r" } = {}) {
367
+ // The language every CODE block in this call is written in. "r" (the default)
368
+ // keeps R's checks and R's sentences exactly; another engine swaps in its own
369
+ // language checks and names itself in the repair messages.
370
+ const lang = String(engine || "r").toLowerCase();
371
+ const isR = lang === "r";
372
+ const rules = isR ? null : (ENGINE_RULES[lang] || { transcript: null, sideEffects: [] });
373
+ const label = isR ? "R" : lang.charAt(0).toUpperCase() + lang.slice(1);
374
+ if (!Array.isArray(raw) || !raw.length) {
375
+ throw repair(`${tool} needs a non-empty list of blocks.`);
376
+ }
377
+ if (raw.length > max) {
378
+ throw repair(max === 1
379
+ ? `${tool} takes exactly one block. Author the next document block in a later chunk_insert call.`
380
+ : `${tool} accepts at most ${max} blocks at once; you sent ${raw.length}. `
381
+ + "Send the analysis as one coherent set within that limit.");
382
+ }
383
+
384
+ const lints = [];
385
+ const blocks = raw.map((item, index) => { // null → dropped, see below
386
+ const declared = item && item.kind != null ? String(item.kind) : "r";
387
+ // "md" is the internal name for a text block; accept it so an in-page
388
+ // caller and a terminal agent can share this function.
389
+ const isText = declared === "text" || declared === "md";
390
+ if (!isText && declared !== "r" && !isCodeKind(declared)) {
391
+ throw repair(`${ordinal(index)} has kind "${declared}". `
392
+ + `CarmaR blocks are ${BLOCK_KINDS.map((k) => `"${k}"`).join(" or ")}.`);
393
+ }
394
+ const content = String(item && item.content != null ? item.content : "");
395
+ if (!content.trim()) {
396
+ throw repair(`${ordinal(index)} is empty. Every block must carry content.`);
397
+ }
398
+
399
+ if (isText) {
400
+ const fences = topLevelFenceInfo(content);
401
+ // The rule the instructions could only ask for. A ```{r} fence inside
402
+ // prose is a chunk the user cannot run, cannot see the output of, and
403
+ // cannot review as code — the analysis arrives as a picture of itself.
404
+ if (fences.some((each) => BRACED_R.test(each))) {
405
+ throw repair(`${ordinal(index)} is a text block containing an executable \`\`\`{r} `
406
+ + "chunk. Runnable R belongs in its own block with kind \"r\"; split this into a "
407
+ + "text block for the prose and an r block for the code, in that order.");
408
+ }
409
+ if (fences.some((each) => PLAIN_R.test(each))) {
410
+ lints.push(`${ordinal(index)} embeds a \`\`\`r fence in prose. That is a literal `
411
+ + "example and will never run — if it was meant to run, send it as an r block.");
412
+ }
413
+ const banners = stripChatDecoration(content);
414
+ const cleaned = stripToolingTalk(banners.text);
415
+ cleaned.removed = banners.removed; // banner count, for the lint below
416
+ if (cleaned.text !== banners.text) {
417
+ lints.push(`${ordinal(index)} contained prose about the notebook's own buttons and `
418
+ + "panes, which was removed. The saved document is read by people who have no pane "
419
+ + "and were not in this conversation — write about the analysis only.");
420
+ }
421
+ if (banners.removed) {
422
+ lints.push(`${ordinal(index)} arrived wrapped in ${cleaned.removed} chat banner `
423
+ + `rule${cleaned.removed === 1 ? "" : "s"}, which were removed. A text block is `
424
+ + "document prose — write it as it should read in the saved .qmd, with no "
425
+ + "decoration around it.");
426
+ }
427
+ // A block that was ENTIRELY chat — a banner with nothing in it, or a
428
+ // paragraph purely about the buttons — is dropped, not refused.
429
+ //
430
+ // Refusing was the first behaviour and it broke the pane's own Keep
431
+ // button: an answer whose opening line happened to be "here is the
432
+ // workflow, one step per block" failed WHOLE, and the user lost every
433
+ // good block with it. The model's phrasing is not the user's mistake.
434
+ // Dropping an empty block cannot damage the document; refusing the
435
+ // transaction can.
436
+ if (!cleaned.text.trim()) {
437
+ lints.push(`${ordinal(index)} was entirely chat — a banner or a note about the `
438
+ + "notebook's own controls — so it was dropped. The remaining blocks were kept.");
439
+ return null;
440
+ }
441
+ const voice = ASSISTANT_VOICE.find((pattern) => pattern.re.test(cleaned.text));
442
+ if (voice) {
443
+ throw repair(`${ordinal(index)} uses assistant or process-reporting register `
444
+ + `("${voice.quote}"). The document outlives the conversation and is read by people `
445
+ + "who were not in it. Rewrite the complete block as facts about the analysis, with "
446
+ + "no claims about what you did, the user's installation, or what you can do next.");
447
+ }
448
+ const recipe = SECOND_PERSON.find((pattern) => pattern.re.test(cleaned.text));
449
+ if (recipe) {
450
+ throw repair(`${ordinal(index)} addresses the user in recipe/chat register `
451
+ + `("${recipe.quote}"). Rewrite it as standalone document prose that names the `
452
+ + "method, function, object, analyst, or reader instead of the current conversation.");
453
+ }
454
+ if (UI_REGISTER.test(cleaned.text)) {
455
+ throw repair(`${ordinal(index)} uses notebook UI/conversation vocabulary such as `
456
+ + '"this/that/last block". In saved prose, call runnable code an R chunk and identify '
457
+ + "the step, object or chunk explicitly instead of referring to a vanished screen position.");
458
+ }
459
+ if (PASTED_VALUE.test(cleaned.text)) {
460
+ lints.push(`${ordinal(index)} contains printed R output pasted into prose. The `
461
+ + "notebook runs the chunk and shows the real result; a copy goes stale the "
462
+ + "moment the data changes.");
463
+ }
464
+ return { kind: "md", content: cleaned.text };
465
+ }
466
+
467
+ if (!isR) {
468
+ if (WRAPPED_IN_FENCE.test(content)) {
469
+ throw repair(`${ordinal(index)} is a ${lang} block whose source starts with a Markdown fence. `
470
+ + `Send the ${label} source itself — the notebook supplies the chunk, so \`\`\` lines become `
471
+ + "syntax errors in the user's session.");
472
+ }
473
+ if (rules.transcript && rules.transcript.test(content)) {
474
+ throw repair(`${ordinal(index)} is a ${lang} block holding ${rules.transcriptWhat}. ${label} `
475
+ + "cannot parse those; send the source alone and let the notebook produce the output by running it.");
476
+ }
477
+ rules.sideEffects.forEach(({ re, what }) => {
478
+ if (re.test(content)) {
479
+ lints.push(`${ordinal(index)} ${what}. The user reviews this block before it is `
480
+ + "theirs, but do not include it unless they asked for it — a chunk should compute, "
481
+ + "not change the machine it runs on.");
482
+ }
483
+ });
484
+ if (ABSOLUTE_PATH.test(content)) {
485
+ lints.push(`${ordinal(index)} names an absolute path. Use a project-relative one, or the `
486
+ + "document stops working on any other machine — including the author's, tomorrow.");
487
+ }
488
+ return { kind: CODE_KIND, content };
489
+ }
490
+ if (WRAPPED_IN_FENCE.test(content)) {
491
+ throw repair(`${ordinal(index)} is an r block whose source starts with a Markdown fence. `
492
+ + "Send the R source itself — the notebook supplies the chunk, so ``` lines become "
493
+ + "syntax errors in the user's session.");
494
+ }
495
+ if (CONSOLE_TRANSCRIPT.test(content)) {
496
+ throw repair(`${ordinal(index)} is an r block holding a console TRANSCRIPT — lines `
497
+ + "beginning `> ` or `[1] `. R cannot parse those; send the source alone and let the "
498
+ + "notebook produce the output by running it.");
499
+ }
500
+ SIDE_EFFECTS.forEach(({ re, what }) => {
501
+ if (re.test(content)) {
502
+ lints.push(`${ordinal(index)} ${what}. The user reviews this block before it is `
503
+ + "theirs, but do not include it unless they asked for it — a chunk should compute, "
504
+ + "not change the machine it runs on.");
505
+ }
506
+ });
507
+ if (ABSOLUTE_PATH.test(content)) {
508
+ lints.push(`${ordinal(index)} names an absolute path. Use a project-relative one, or the `
509
+ + "document stops working on any other machine — including the author's, tomorrow.");
510
+ }
511
+
512
+ return { kind: CODE_KIND, content };
513
+ }).filter(Boolean);
514
+
515
+ // Everything was chat and nothing was analysis. Now it IS a refusal — there
516
+ // is no document left to insert, and silently adding nothing while reporting
517
+ // success would be the worst of the three options.
518
+ if (!blocks.length) {
519
+ throw repair(`${tool} received nothing but chat — banners and notes about the notebook's `
520
+ + "own controls, with no analysis in any block. Send the prose and the R itself.");
521
+ }
522
+
523
+ // Cross-block, so it cannot be checked while walking one block at a time.
524
+ // knitr errors on a duplicate label by default, which makes the whole
525
+ // document unrenderable — the failure surfaces at Render, far from the
526
+ // transaction that caused it, so it is refused here instead.
527
+ const labels = new Map();
528
+ blocks.forEach((block, index) => {
529
+ if (!isCodeKind(block.kind)) return;
530
+ const found = HASH_PIPE_LABEL.exec(block.content);
531
+ if (!found) return;
532
+ const label = found[1];
533
+ if (labels.has(label)) {
534
+ throw repair(`${ordinal(index)} reuses the chunk label "${label}", already used by `
535
+ + `${ordinal(labels.get(label))}. knitr refuses a document with duplicate labels, so `
536
+ + "this would fail at render. Give each chunk its own descriptive label.");
537
+ }
538
+ labels.set(label, index);
539
+ });
540
+
541
+ return { blocks, lints };
542
+ }
543
+
544
+ export default {
545
+ CONTRACT, SCHEMA_VERSION, MAX_BLOCKS, BLOCK_KINDS, AUTHORING_RULES,
546
+ authoringInstructions, normalizeBlocks,
547
+ };
@@ -0,0 +1,108 @@
1
+ // cell-kinds.js — the page's cell vocabulary, after the engine stopped being R.
2
+ //
3
+ // THE CHANGE, IN ONE LINE: the code verb's key was `rcode` and is now `code`,
4
+ // because the cell's IDENTITY is "this holds code" and the LANGUAGE lives in
5
+ // the chunk header (`params.chunk.engine`), where the document format has
6
+ // always kept it. A verb key that spells one language cannot describe a
7
+ // notebook with two, and `pycode` beside `rcode` would be the same mistake a
8
+ // second time (lib/engine-labels.js is the other half of this).
9
+ //
10
+ // THREE ALPHABETS, AND THEY ARE NOT THE SAME ALPHABET.
11
+ //
12
+ // live `code` · `md` the verb key, the handle's `kind`, every
13
+ // predicate in lib/ and src/. What the page
14
+ // EMITS.
15
+ // legacy `rcode` what every notebook saved before this
16
+ // change carries, in core's storage and in
17
+ // a saved .html. Still ACCEPTED everywhere,
18
+ // and still registered as a (hidden) verb so
19
+ // such a cell mounts. Never emitted.
20
+ // disk `rcode` · `md` the `kind` field of a `<!-- carmar-cell:`
21
+ // annotation in a .carmd/.qmd. THE DOCUMENT
22
+ // FORMAT, and deliberately unchanged: a file
23
+ // written today must still open in CarmaR
24
+ // 0.8.01, whose reader validates exactly
25
+ // `['rcode','md']`. lib/document-source-cells.js
26
+ // translates at that one boundary and
27
+ // nowhere else.
28
+ //
29
+ // So: ACCEPT both spellings, EMIT `code`, WRITE `rcode`. Anything that tests a
30
+ // kind goes through `isCodeKind`; anything that builds one goes through
31
+ // `cellKind` or `CODE_KIND`; the two disk helpers are used by the annotation
32
+ // reader and writer and by nothing else.
33
+
34
+ /** The code verb's key, and the `kind` a code cell reports. */
35
+ export const CODE_KIND = "code";
36
+
37
+ /** The prose verb's key. Unchanged — prose was never a language. */
38
+ export const PROSE_KIND = "md";
39
+
40
+ /** What a code cell was called before WP6. Accepted forever, never emitted. */
41
+ export const LEGACY_CODE_KIND = "rcode";
42
+
43
+ /** Every spelling a code cell may arrive as. */
44
+ export const CODE_KINDS = Object.freeze([CODE_KIND, LEGACY_CODE_KIND]);
45
+
46
+ /** Every cell kind the document model carries, in both spellings. */
47
+ export const DOCUMENT_KINDS = Object.freeze([CODE_KIND, LEGACY_CODE_KIND, PROSE_KIND]);
48
+
49
+ /**
50
+ * Is this a code cell? The one predicate — `kind === "rcode"` is a defect now,
51
+ * because it answers "no" for every cell this build creates.
52
+ *
53
+ * @param {string|null|undefined} kind
54
+ * @returns {boolean}
55
+ */
56
+ export function isCodeKind(kind) {
57
+ return kind === CODE_KIND || kind === LEGACY_CODE_KIND;
58
+ }
59
+
60
+ /** Is this a prose cell? */
61
+ export function isProseKind(kind) {
62
+ return kind === PROSE_KIND;
63
+ }
64
+
65
+ /** Is this a kind the document model knows at all? */
66
+ export function isDocumentKind(kind) {
67
+ return isCodeKind(kind) || isProseKind(kind);
68
+ }
69
+
70
+ /**
71
+ * The kind to EMIT for a requested one: prose stays prose, everything else is
72
+ * code. This is the `kind === "md" ? "md" : "rcode"` idiom the page repeated in
73
+ * a dozen places, with the answer in one.
74
+ *
75
+ * @param {string|null|undefined} kind
76
+ * @returns {"code"|"md"}
77
+ */
78
+ export function cellKind(kind) {
79
+ return isProseKind(kind) ? PROSE_KIND : CODE_KIND;
80
+ }
81
+
82
+ // ── the document-format boundary ────────────────────────────────────────────
83
+
84
+ /**
85
+ * The `kind` to WRITE into a `<!-- carmar-cell: … -->` annotation.
86
+ *
87
+ * A code chunk is written as `rcode` whatever this build calls it, because
88
+ * that is the format, and a file CarmaR 0.8.01 refuses to open is a file its
89
+ * author lost. The engine still travels where it always travelled — the
90
+ * visible fence header — so a `{python}` chunk round-trips through an `rcode`
91
+ * annotation with nothing missing.
92
+ */
93
+ export function annotationKind(key) {
94
+ return isCodeKind(key) ? LEGACY_CODE_KIND : PROSE_KIND;
95
+ }
96
+
97
+ /** The live kind for an annotation's `kind` field. */
98
+ export function kindFromAnnotation(kind) {
99
+ return isCodeKind(kind) ? CODE_KIND : PROSE_KIND;
100
+ }
101
+
102
+ /** The two spellings an annotation's `kind` may legally be, for validation. */
103
+ export const ANNOTATION_KINDS = Object.freeze([LEGACY_CODE_KIND, PROSE_KIND]);
104
+
105
+ export default {
106
+ CODE_KIND, PROSE_KIND, LEGACY_CODE_KIND, CODE_KINDS, DOCUMENT_KINDS, ANNOTATION_KINDS,
107
+ isCodeKind, isProseKind, isDocumentKind, cellKind, annotationKind, kindFromAnnotation,
108
+ };