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,410 @@
1
+ # settings.R — the settings a USER may change, and the ones only an
2
+ # administrator may.
3
+ #
4
+ # Until now every kernel-side preference was an environment variable read once
5
+ # at startup: CARMAR_LINGER, CARMAR_NO_HISTORY, CARMAR_ROOT, CARMAR_LOG. That
6
+ # is exactly right for a deployment and useless for a person, who has no place
7
+ # to set one and no way to see what it currently is. This module adds a user
8
+ # config file and — more importantly — the rule for who wins.
9
+ #
10
+ # Pure, in the shape of deployment.R and ai-policy.R: `env` is injectable, the
11
+ # resolve returns a whole decision rather than mutating anything, and the only
12
+ # I/O is the one file. serve.R sources it; nothing else does.
13
+ #
14
+ # ── THE PRECEDENCE RULE ─────────────────────────────────────────────────────
15
+ #
16
+ # environment variable (set and non-empty) ▸ config file ▸ built-in default
17
+ #
18
+ # and two consequences that matter more than the ordering:
19
+ #
20
+ # · A setting whose env var is set is READ-ONLY for the life of this kernel.
21
+ # Not "the file is ignored this time" — `settings_set` refuses it and does
22
+ # not write the file, because a value silently stored and never applied is
23
+ # a worse lie than a refusal.
24
+ #
25
+ # · An administrator's setting is NOT IN THIS FILE'S VOCABULARY AT ALL. It is
26
+ # not "env wins if present": the key is rejected on read and refused on
27
+ # write, always. "Env wins when set" only guarantees anything WHEN THE ENV
28
+ # IS SET, and a deployment that never sets CARMAR_ROOT is still one where a
29
+ # user must not invent one — a root the user chose reads as protection
30
+ # while being none (worker.R is explicit that it is a guardrail, not a
31
+ # sandbox). The refusal must not depend on an administrator having
32
+ # remembered to set every variable defensively.
33
+ #
34
+ # The tempting alternative — a layered Sys.getenv-shaped getter handed to
35
+ # carmar_deployment() and carmar_ai_policy() — is deliberately NOT built. It
36
+ # would put every admin variable one argument-passing mistake away from being
37
+ # file-settable. Those two keep plain Sys.getenv; only the user-writable call
38
+ # sites in serve.R read this module.
39
+
40
+ SETTINGS_VERSION <- 1L
41
+
42
+ # The whole vocabulary. A key that is not here does not exist: there is no
43
+ # pass-through, so the file cannot grow a control by accident.
44
+ #
45
+ # `effect` is a FIELD, not a sentence the page composes, so a setting added
46
+ # later cannot arrive without an honest answer to "when does this take
47
+ # effect?". Its three values:
48
+ # live the running kernel changes now
49
+ # restart_r the next R session picks it up (Session ▸ Restart R)
50
+ # next_launch only a new kernel
51
+ SETTINGS <- list(
52
+ # Connection loss is not permission to discard the R workspace. Timed
53
+ # shutdown is opt-in; 0 retains an attached session until explicit Quit.
54
+ list(key = "linger_seconds", env = "CARMAR_LINGER", kind = "number",
55
+ default = 0, min = 0, max = 86400, effect = "live",
56
+ label = "Keep R running after all pages disconnect"),
57
+ list(key = "history_enabled", env = "CARMAR_NO_HISTORY", kind = "bool",
58
+ default = TRUE, effect = "live", inverted = TRUE,
59
+ label = "Remember console history between sessions"),
60
+ list(key = "analyze_budget_s", env = "CARMAR_ANALYZE_BUDGET", kind = "number",
61
+ default = 3, min = 0, max = 60, effect = "next_launch",
62
+ label = "Code-intelligence time budget"),
63
+ list(key = "quarto_path", env = "CARMAR_QUARTO", kind = "path",
64
+ default = "", effect = "live",
65
+ label = "Quarto binary"),
66
+ list(key = "rscript_path", env = "CARMAR_RSCRIPT", kind = "path",
67
+ default = "", effect = "restart_r",
68
+ label = "R binary"),
69
+ list(key = "port", env = "CARMAR_PORT", kind = "number",
70
+ default = 4747, min = 1024, max = 65535, effect = "next_launch",
71
+ label = "Preferred port")
72
+ )
73
+
74
+ # Reported to the page as read-only facts, never settable. Everything an
75
+ # administrator owns lives here and NOWHERE in SETTINGS, which is what makes
76
+ # the refusal structural rather than a lookup someone can forget.
77
+ SETTINGS_ADMIN <- c(
78
+ "CARMAR_MANAGED_CONFIG",
79
+ "CARMAR_ROOT", "CARMAR_LOG", "CARMAR_LOG_AI_TEXT",
80
+ "CARMAR_CRAN_MIRROR",
81
+ "CARMAR_AI_LOCAL_ONLY", "CARMAR_AI_POLICY", "CARMAR_AI_PROVIDERS",
82
+ "CARMAR_BIND", "CARMAR_HOSTS", "CARMAR_ORIGINS", "CARMAR_REQUIRE_ORIGIN",
83
+ "CARMAR_TRUST_PROXY", "CARMAR_USER_HEADER", "CARMAR_TRUSTED_PROXY",
84
+ "CARMAR_ALLOW_UNAUTHENTICATED", "CARMAR_PORT_STRICT",
85
+ "CARMAR_UPDATE_FEED", "CARMAR_UPDATE_MIRROR", "CARMAR_UPDATE_OFFLINE_DIR",
86
+ "CARMAR_UPDATE_MANAGED"
87
+ )
88
+
89
+ # A managed desktop cannot depend on shell startup files: Finder/LaunchServices
90
+ # does not read them, and a standard user can edit them. Jamf/Intune instead
91
+ # place this small JSON file in an administrator-owned directory. Its values
92
+ # become environment variables before deployment, AI, update, or user-setting
93
+ # policy is resolved, so the existing env-wins-and-locks rule remains the one
94
+ # source of truth.
95
+ MANAGED_SETTINGS_SCHEMA <- "carmar-managed-settings-v1"
96
+ MANAGED_SETTINGS_ENV <- unique(c(
97
+ vapply(SETTINGS, function(s) s$env, character(1)),
98
+ setdiff(SETTINGS_ADMIN, "CARMAR_MANAGED_CONFIG"),
99
+ "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"
100
+ ))
101
+
102
+ carmar_managed_default_path <- function(env = Sys.getenv,
103
+ os = .Platform$OS.type,
104
+ sysname = Sys.info()[["sysname"]]) {
105
+ if (identical(sysname, "Darwin")) {
106
+ return("/Library/Application Support/CarmaR/managed-settings.json")
107
+ }
108
+ if (identical(os, "windows")) {
109
+ base <- trimws(env("ProgramData", ""))
110
+ if (!nzchar(base)) base <- "C:/ProgramData"
111
+ return(file.path(base, "CarmaR", "managed-settings.json"))
112
+ }
113
+ "/etc/carmar/managed-settings.json"
114
+ }
115
+
116
+ # Protection is checked from the standard user's point of view. Unix also
117
+ # requires root ownership because a user-owned 0400 file can simply be chmod'd
118
+ # and replaced. On Windows, the deployment recipe gives ProgramData/CarmaR an
119
+ # Administrators+SYSTEM write ACL; neither the file nor its parent may be
120
+ # writable by the running account.
121
+ carmar_managed_protected <- function(path, os = .Platform$OS.type) {
122
+ if (nzchar(Sys.readlink(path))) return(FALSE)
123
+ if (file.access(path, 2L) == 0L || file.access(dirname(path), 2L) == 0L) {
124
+ return(FALSE)
125
+ }
126
+ if (!identical(os, "windows")) {
127
+ info <- file.info(path)
128
+ if (is.na(info$uid[[1]]) || info$uid[[1]] != 0L || settings_insecure(path)) {
129
+ return(FALSE)
130
+ }
131
+ }
132
+ TRUE
133
+ }
134
+
135
+ # Read one administrator policy atomically. Unlike the user settings file,
136
+ # unknown or malformed entries fail the WHOLE policy closed: partial policy is
137
+ # not a safe enterprise fallback.
138
+ carmar_managed_read <- function(path, required = FALSE,
139
+ protection = carmar_managed_protected) {
140
+ empty <- list(status = "missing", path = path, values = character(), errors = character())
141
+ if (!file.exists(path)) {
142
+ if (isTRUE(required)) {
143
+ empty$status <- "invalid"
144
+ empty$errors <- "CARMAR_MANAGED_CONFIG names a file that does not exist."
145
+ }
146
+ return(empty)
147
+ }
148
+ if (!isTRUE(protection(path))) {
149
+ empty$status <- "insecure"
150
+ empty$errors <- paste0(
151
+ "The managed settings file is writable by the current account, is a symlink, ",
152
+ "or is not administrator-owned; CarmaR refused it.")
153
+ return(empty)
154
+ }
155
+ size <- file.info(path)$size[[1]]
156
+ if (is.na(size) || size > 1024 * 1024) {
157
+ empty$status <- "invalid"
158
+ empty$errors <- "The managed settings file exceeds the 1 MB limit."
159
+ return(empty)
160
+ }
161
+ raw <- tryCatch(jsonlite::fromJSON(path, simplifyVector = FALSE),
162
+ error = function(e) NULL)
163
+ root_names <- if (is.list(raw)) names(raw) else NULL
164
+ if (is.null(root_names) || anyDuplicated(root_names) ||
165
+ !setequal(root_names, c("schema", "settings")) ||
166
+ !identical(raw$schema, MANAGED_SETTINGS_SCHEMA) ||
167
+ !is.list(raw$settings) || is.null(names(raw$settings)) ||
168
+ anyDuplicated(names(raw$settings))) {
169
+ empty$status <- "invalid"
170
+ empty$errors <- paste0("The managed settings file must use schema ",
171
+ MANAGED_SETTINGS_SCHEMA, " with one settings object.")
172
+ return(empty)
173
+ }
174
+ keys <- names(raw$settings)
175
+ unknown <- setdiff(keys, MANAGED_SETTINGS_ENV)
176
+ bad <- keys[!vapply(raw$settings, function(value) {
177
+ is.character(value) && length(value) == 1L && !is.na(value) &&
178
+ nzchar(value) && !grepl("[\r\n\t]", value)
179
+ }, logical(1))]
180
+ errors <- character()
181
+ if (length(unknown)) {
182
+ errors <- c(errors, paste0("Unknown managed setting: ", paste(unknown, collapse = ", "), "."))
183
+ }
184
+ if (length(bad)) {
185
+ errors <- c(errors, paste0("Managed settings must be non-empty single-line strings: ",
186
+ paste(bad, collapse = ", "), "."))
187
+ }
188
+ if (length(errors)) {
189
+ empty$status <- "invalid"
190
+ empty$errors <- errors
191
+ return(empty)
192
+ }
193
+ values <- vapply(raw$settings, identity, character(1))
194
+ list(status = "ok", path = path, values = values, errors = character())
195
+ }
196
+
197
+ carmar_managed_environment <- function(env = Sys.getenv, path = NULL,
198
+ protection = carmar_managed_protected,
199
+ apply_values = TRUE) {
200
+ explicit <- trimws(env("CARMAR_MANAGED_CONFIG", ""))
201
+ if (is.null(path)) path <- if (nzchar(explicit)) explicit else carmar_managed_default_path(env)
202
+ result <- carmar_managed_read(path, required = nzchar(explicit), protection = protection)
203
+ if (isTRUE(apply_values) && identical(result$status, "ok") && length(result$values)) {
204
+ do.call(Sys.setenv, as.list(result$values))
205
+ }
206
+ result
207
+ }
208
+
209
+ settings_entry <- function(key) {
210
+ for (s in SETTINGS) if (identical(s$key, key)) return(s)
211
+ NULL
212
+ }
213
+
214
+ #' Where the user's settings live — beside the AI key, same directory.
215
+ carmar_settings_path <- function(env = Sys.getenv) {
216
+ dir <- tools::R_user_dir("carmar", "config")
217
+ dir.create(dir, recursive = TRUE, showWarnings = FALSE)
218
+ file.path(dir, "settings.json")
219
+ }
220
+
221
+ #' Is the file writable by anyone but its owner?
222
+ #'
223
+ #' This file is an INTEGRITY surface rather than a secret: another local account
224
+ #' that can write it turns a knob in this user's next kernel. So a
225
+ #' group- or other-writable file is IGNORED rather than repaired — silently
226
+ #' chmod-ing someone else's file back to 0600 would hide that it had been
227
+ #' changed at all.
228
+ settings_insecure <- function(path) {
229
+ info <- file.info(path)
230
+ if (is.na(info$mode[[1]])) return(FALSE)
231
+ mode <- as.integer(info$mode[[1]])
232
+ bitwAnd(mode, as.integer(strtoi("077", 8L))) != 0L
233
+ }
234
+
235
+ #' Validate one value against its entry.
236
+ #' @return list(ok, value, reason)
237
+ carmar_settings_validate <- function(key, value) {
238
+ entry <- settings_entry(key)
239
+ if (is.null(entry)) {
240
+ if (key %in% SETTINGS_ADMIN) {
241
+ return(list(ok = FALSE, reason = "administrator setting"))
242
+ }
243
+ return(list(ok = FALSE, reason = "unknown setting"))
244
+ }
245
+ if (identical(entry$kind, "bool")) {
246
+ if (!is.logical(value) || length(value) != 1L || is.na(value)) {
247
+ return(list(ok = FALSE, reason = "must be true or false"))
248
+ }
249
+ return(list(ok = TRUE, value = value))
250
+ }
251
+ if (identical(entry$kind, "number")) {
252
+ if (!is.numeric(value) || length(value) != 1L || !is.finite(value)) {
253
+ return(list(ok = FALSE, reason = "must be a number"))
254
+ }
255
+ if (!is.null(entry$min) && value < entry$min) {
256
+ return(list(ok = FALSE, reason = paste0("must be at least ", entry$min)))
257
+ }
258
+ if (!is.null(entry$max) && value > entry$max) {
259
+ return(list(ok = FALSE, reason = paste0("must be at most ", entry$max)))
260
+ }
261
+ # Always a double. jsonlite reads 900 from a file as an INTEGER and 900.0
262
+ # from a set frame as a double, so without this the same setting has two
263
+ # types depending on how it arrived — and every `identical()` against it
264
+ # becomes a coin toss.
265
+ return(list(ok = TRUE, value = as.numeric(value)))
266
+ }
267
+ # path
268
+ if (!is.character(value) || length(value) != 1L || is.na(value)) {
269
+ return(list(ok = FALSE, reason = "must be a string"))
270
+ }
271
+ # An empty path means "use the ladder" — that is a legitimate value, not a
272
+ # missing one, and it is how a user clears an override.
273
+ if (nzchar(value) && !file.exists(value)) {
274
+ return(list(ok = FALSE, reason = "no such file"))
275
+ }
276
+ list(ok = TRUE, value = value)
277
+ }
278
+
279
+ #' Read the config file.
280
+ #'
281
+ #' FOUR states, kept distinct — the ai-policy.R lesson that folding failure
282
+ #' modes together is the fail-open bug:
283
+ #' missing no file. The ordinary single-user state, NOT an error.
284
+ #' ok parsed, an object, the right version.
285
+ #' invalid unparseable, not an object, wrong version → all defaults.
286
+ #' insecure group/other writable → all defaults, different words.
287
+ #'
288
+ #' Per-key failure stays per-key: an unknown key joins `ignored`, a bad value
289
+ #' joins `rejected`, and the rest of the file still applies. One bad number
290
+ #' must not silently revert eleven good ones.
291
+ carmar_settings_read <- function(path = carmar_settings_path()) {
292
+ empty <- list(status = "missing", path = path, values = list(),
293
+ ignored = character(), rejected = list(), error = "")
294
+ if (!file.exists(path)) return(empty)
295
+ if (settings_insecure(path)) {
296
+ return(modifyList(empty, list(status = "insecure",
297
+ error = "The settings file is writable by other accounts on this machine, so it was ignored.")))
298
+ }
299
+ raw <- tryCatch(jsonlite::fromJSON(path, simplifyVector = FALSE),
300
+ error = function(e) NULL)
301
+ if (is.null(raw) || !is.list(raw) || is.null(names(raw))) {
302
+ return(modifyList(empty, list(status = "invalid",
303
+ error = "The settings file could not be read as JSON.")))
304
+ }
305
+ if (!identical(as.integer(raw$version %||% 0L), SETTINGS_VERSION)) {
306
+ return(modifyList(empty, list(status = "invalid",
307
+ error = "The settings file is a version this CarmaR does not know.")))
308
+ }
309
+ block <- raw$settings
310
+ if (is.null(block)) block <- list()
311
+ if (!is.list(block) || (length(block) && is.null(names(block)))) {
312
+ return(modifyList(empty, list(status = "invalid",
313
+ error = "The settings file's `settings` is not an object.")))
314
+ }
315
+ values <- list()
316
+ ignored <- character()
317
+ rejected <- list()
318
+ for (key in names(block)) {
319
+ verdict <- carmar_settings_validate(key, block[[key]])
320
+ if (isTRUE(verdict$ok)) {
321
+ values[[key]] <- verdict$value
322
+ } else if (identical(verdict$reason, "unknown setting")) {
323
+ ignored <- c(ignored, key)
324
+ } else {
325
+ rejected[[length(rejected) + 1L]] <- list(key = key, reason = verdict$reason)
326
+ }
327
+ }
328
+ list(status = "ok", path = path, values = values,
329
+ ignored = ignored, rejected = rejected, error = "")
330
+ }
331
+
332
+ #' Write the whole settings block. Create → chmod 0600 → CHECK → write, the
333
+ #' ordering ai_key_write() uses and for the same reason: a file that was
334
+ #' briefly group-writable was briefly tamperable.
335
+ carmar_settings_write <- function(values, path = carmar_settings_path()) {
336
+ if (!file.exists(path) && !file.create(path, showWarnings = FALSE)) return(FALSE)
337
+ if (!isTRUE(Sys.chmod(path, "0600"))) {
338
+ unlink(path)
339
+ return(FALSE)
340
+ }
341
+ body <- list(version = SETTINGS_VERSION,
342
+ settings = if (length(values)) values else structure(list(), names = character()))
343
+ ok <- tryCatch({
344
+ writeLines(jsonlite::toJSON(body, auto_unbox = TRUE, pretty = TRUE, null = "null"), path)
345
+ TRUE
346
+ }, error = function(e) FALSE)
347
+ ok
348
+ }
349
+
350
+ `%||%` <- function(a, b) if (is.null(a)) b else a
351
+
352
+ #' The whole decision: what every setting resolves to, and from where.
353
+ #'
354
+ #' @return list(settings = list of per-key records, admin = list, file = the read)
355
+ carmar_settings_resolve <- function(env = Sys.getenv, file = NULL) {
356
+ if (is.null(file)) file <- carmar_settings_read()
357
+ usable <- identical(file$status, "ok")
358
+
359
+ out <- lapply(SETTINGS, function(s) {
360
+ raw <- env(s$env, "")
361
+ from_env <- nzchar(raw)
362
+ value <- s$default
363
+ source <- "default"
364
+ if (from_env) {
365
+ source <- "env"
366
+ value <- if (identical(s$kind, "bool")) {
367
+ # CARMAR_NO_HISTORY and CARMAR_NO_NATIVE_DIALOG are NEGATIVE flags —
368
+ # any value means "off" — while the file and the UI hold the positive.
369
+ # Storing the positive is what a checkbox actually is; translating
370
+ # happens here, once.
371
+ if (isTRUE(s$inverted)) FALSE else TRUE
372
+ } else if (identical(s$kind, "number")) {
373
+ n <- suppressWarnings(as.numeric(raw))
374
+ if (is.finite(n)) n else s$default
375
+ } else raw
376
+ } else if (usable && !is.null(file$values[[s$key]])) {
377
+ source <- "file"
378
+ value <- file$values[[s$key]]
379
+ }
380
+ list(key = s$key, label = s$label, kind = s$kind, effect = s$effect,
381
+ value = value, source = source, locked = from_env,
382
+ writable = !from_env, min = s$min, max = s$max, env = s$env)
383
+ })
384
+ names(out) <- vapply(SETTINGS, function(s) s$key, character(1))
385
+
386
+ admin <- lapply(SETTINGS_ADMIN, function(name) {
387
+ raw <- env(name, "")
388
+ list(key = name, value = raw, source = if (nzchar(raw)) "env" else "default")
389
+ })
390
+
391
+ list(settings = out, admin = admin, file = file)
392
+ }
393
+
394
+ #' The resolved scalar for one key.
395
+ carmar_settings_value <- function(resolved, key) {
396
+ entry <- resolved$settings[[key]]
397
+ if (is.null(entry)) NULL else entry$value
398
+ }
399
+
400
+ #' What a CHILD process should inherit — the settings that only take effect in
401
+ #' a freshly spawned worker, job or analyzer, expressed back in the environment
402
+ #' vocabulary those processes read.
403
+ carmar_settings_child_env <- function(resolved) {
404
+ out <- character()
405
+ q <- carmar_settings_value(resolved, "quarto_path")
406
+ if (is.character(q) && nzchar(q)) out["CARMAR_QUARTO"] <- q
407
+ b <- carmar_settings_value(resolved, "analyze_budget_s")
408
+ if (is.numeric(b)) out["CARMAR_ANALYZE_BUDGET"] <- as.character(b)
409
+ out
410
+ }