mindvest-atlas 0.98.0 → 0.100.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindvest-atlas",
3
- "version": "0.98.0",
3
+ "version": "0.100.0",
4
4
  "description": "Atlas CLI \u2014 OAuth login, tool calls, and live alert/flow streaming for the Atlas trading API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@ import {
27
27
  computerUseAllowed, listComputerSessions, readComputerActions, listComputerVideo, readComputerVideoPart,
28
28
  sessionReplay, venvPython, ambientPython,
29
29
  readSchedule, cronMatches, dueMinutes, scheduleMatches, scheduleDueMinutes, writeNext,
30
- SCRIPTS_DIR, projectsForAlert, readMode, checksWhen, projectPrints,
30
+ SCRIPTS_DIR, projectsForAlert, readMode, checksWhen, checksDue, projectPrints,
31
31
  streamStudioFrames,
32
32
  localBrokers, localBrokerCall,
33
33
  localMemory,
@@ -283,6 +283,8 @@ async function handleAsk(ask, root) {
283
283
  // results are read like any file (op 'read' on checks.json and checks/).
284
284
  if (ask.op === 'checks') return answer(ask.request_id, startChecks({ ...ask, why: 'web' }, root));
285
285
  if (ask.op === 'checks_auto') return answer(ask.request_id, setCheckAuto(ask, root));
286
+ // Pause, resume or delete one check (owner, 2026-10-15), in checks/settings.json.
287
+ if (ask.op === 'checks_state') return answer(ask.request_id, setCheckState(ask, root));
286
288
  // ── Sharing a project ────────────────────────────────────────────────
287
289
  // Not awaited: a pack, an upload and a download are seconds to minutes on a
288
290
  // home connection, and the ask loop has the run queue, the schedule and every
@@ -908,12 +910,12 @@ const REQUEST_TICK_MS = 1000;
908
910
  */
909
911
  export function requestTick(root, timeoutMs, spawn = spawnJob, stop = stopRun, nowMs = Date.now()) {
910
912
  const acted = [];
911
- for (const { dir, op, only, replay, why, assume } of takeRequests(root, nowMs)) {
913
+ for (const { dir, op, only, skip, replay, why, assume } of takeRequests(root, nowMs)) {
912
914
  const slug = path.basename(dir);
913
915
  // Atlas Studio's Run checks / Recheck: the same road a check run from the
914
916
  // web takes, so it runs exactly as the project's own run does.
915
917
  if (op === 'checks') {
916
- const r = startChecks({ job: slug, only, replay, why, assume }, root, spawn);
918
+ const r = startChecks({ job: slug, only, skip, replay, why, assume }, root, spawn);
917
919
  if (r.ok) eprintln(color.dim(`· ${slug} — checks asked for here`));
918
920
  acted.push([slug, 'checks']);
919
921
  continue;
@@ -1045,27 +1047,37 @@ export function checksTick(root, nowMs = Date.now(), spawn = spawnJob) {
1045
1047
  if (!fs.existsSync(path.join(dir, 'checks.py'))) continue;
1046
1048
  const when = checksWhen(dir);
1047
1049
  const prints = projectPrints(dir);
1050
+ const off = checksOff(dir);
1048
1051
  let s = checksSeen.get(slug);
1049
1052
  if (!s) { checksSeen.set(slug, { at: nowMs, ...prints, filesAt: 0, dataAt: 0, fixed: new Set() }); continue; }
1050
1053
  const busy = running.has(`${slug}:checks`) || starting.has(`${slug}:checks`) || nowMs - (checksEndedAt.get(slug) ?? 0) < 30_000;
1051
1054
  if (busy) { Object.assign(s, prints, { filesAt: 0, dataAt: 0, at: nowMs }); continue; }
1052
1055
  if (prints.files !== s.files) { s.files = prints.files; s.filesAt = nowMs; }
1053
1056
  if (prints.data !== s.data) { s.data = prints.data; s.dataAt = nowMs; }
1057
+ const results = readJsonFile(path.join(dir, 'checks.json'));
1058
+ const rows = Array.isArray(results?.checks) ? results.checks : [];
1059
+ // ON THEIR TIMES: each check's own (atlas.json "checks"."each"), and the
1060
+ // project's for every check without one; one run for all that are due.
1061
+ const due = checksDue(when, rows.map((c) => String(c?.id)), off, s.at / 1000, nowMs / 1000);
1054
1062
  let why = null;
1055
- if (when.schedule && scheduleDueMinutes(when.schedule, s.at / 1000, nowMs / 1000).length) why = 'schedule';
1063
+ if (due) why = 'schedule';
1056
1064
  else if (when.files && s.filesAt && nowMs - s.filesAt >= SETTLE_MS) why = 'files';
1057
1065
  else if (when.data && s.dataAt && nowMs - s.dataAt >= SETTLE_MS) why = 'data';
1058
1066
  s.at = nowMs;
1067
+ // EVERY CHECK PAUSED OR DELETED: nothing to run by itself (atlas_env would
1068
+ // skip each one anyway, after fetching its data for nothing).
1069
+ if (why && rows.length && rows.every((c) => off.has(String(c?.id)))) { s.filesAt = 0; s.dataAt = 0; continue; }
1059
1070
  if (why) {
1060
1071
  s.filesAt = 0; s.dataAt = 0;
1061
- const r = startChecks({ job: slug, why }, root, spawn);
1062
- if (r.ok) { started.push([slug, why]); eprintln(color.dim(`· ${slug} — checks (${why})`)); }
1072
+ const r = startChecks({ job: slug, why, ...(due ? due : {}) }, root, spawn);
1073
+ const which = due?.only.length ? `: ${due.only.join(', ')}` : '';
1074
+ if (r.ok) { started.push([slug, why]); eprintln(color.dim(`· ${slug} — checks (${why}${which})`)); }
1063
1075
  continue;
1064
1076
  }
1065
1077
  // A FIX THAT NOTHING HAS RUN SINCE: that check again, fresh.
1066
- const results = readJsonFile(path.join(dir, 'checks.json'));
1067
1078
  const fixes = readJsonFile(path.join(dir, 'checks', 'resolutions.json'));
1068
- for (const c of Array.isArray(results?.checks) ? results.checks : []) {
1079
+ for (const c of rows) {
1080
+ if (off.has(String(c?.id))) continue;
1069
1081
  const f = fixes && fixes[c?.id];
1070
1082
  if (!f || f.ok === false || c.status === 'pass' || s.fixed.has(`${c.id}@${f.at}`)) continue;
1071
1083
  if (Date.parse(f.at) <= Date.parse(c.ran_at || '')) continue;
@@ -1220,12 +1232,21 @@ export async function remoteControlCommand(args) {
1220
1232
  /** For the tests: the one road every run takes. */
1221
1233
  export const runJobForTests = (job, root, timeoutMs) => runJob(job, root, timeoutMs);
1222
1234
 
1235
+ const CHECK_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
1236
+
1223
1237
  /** Start a project's checks.py through the ordinary run (plan check included). */
1224
1238
  export function startChecks(ask, root, spawn = spawnJob) {
1225
1239
  const job = String(ask?.job || '');
1226
1240
  const { dir } = workspace(job, root);
1227
1241
  if (!job || !fs.existsSync(path.join(dir, 'checks.py'))) return { ok: false, error: 'This project has no checks yet.' };
1228
- const only = (Array.isArray(ask.only) ? ask.only : []).map(String).filter((x) => /^[A-Za-z0-9_-]{1,80}$/.test(x));
1242
+ const asked = (Array.isArray(ask.only) ? ask.only : []).map(String).filter((x) => CHECK_ID_RE.test(x));
1243
+ // A paused or deleted check is never run, not even when named.
1244
+ const off = checksOff(dir);
1245
+ const only = asked.filter((x) => !off.has(x));
1246
+ if (asked.length && !only.length) return { ok: false, error: 'That check is paused. Resume it to run it.' };
1247
+ // Not due on this minute: a check whose own time did not come round, in a
1248
+ // run the project's schedule started (atlas_env's wants() says no).
1249
+ const skip = (Array.isArray(ask.skip) ? ask.skip : []).map(String).filter((x) => CHECK_ID_RE.test(x)).slice(0, 200);
1229
1250
  const replay = /^\d{8}-\d{6}$/.test(String(ask.replay || '')) ? String(ask.replay) : '';
1230
1251
  // What started it, for the run's history line (a word, or nothing).
1231
1252
  const why = /^[a-z]{1,20}$/.test(String(ask.why || '')) ? String(ask.why) : '';
@@ -1235,6 +1256,7 @@ export function startChecks(ask, root, spawn = spawnJob) {
1235
1256
  automation_id: job, name: job, source: 'local', file: 'checks.py',
1236
1257
  env: {
1237
1258
  ATLAS_CHECK_ONLY: only.join(','), ATLAS_CHECK_REPLAY: replay, ...(why ? { ATLAS_CHECK_WHY: why } : {}),
1259
+ ...(skip.length && !only.length ? { ATLAS_CHECK_SKIP: skip.join(',') } : {}),
1238
1260
  ...(assume ? { ATLAS_CHECK_ASSUME: assume } : {}),
1239
1261
  },
1240
1262
  }, root, CHECKS_TIMEOUT_MS);
@@ -1264,6 +1286,59 @@ export function cleanAssume(raw) {
1264
1286
  return Object.keys(out).length ? JSON.stringify(out) : '';
1265
1287
  }
1266
1288
 
1289
+ /** The checks the person paused or deleted (checks/settings.json), by id. */
1290
+ export function checksOff(dir) {
1291
+ const d = readJsonFile(path.join(dir, 'checks', 'settings.json')) || {};
1292
+ const paused = d.paused && typeof d.paused === 'object' ? d.paused : {};
1293
+ const deleted = d.deleted && typeof d.deleted === 'object' ? d.deleted : {};
1294
+ return new Set([...Object.keys(paused).filter((k) => paused[k]), ...Object.keys(deleted)]);
1295
+ }
1296
+
1297
+ /**
1298
+ * PAUSE, RESUME OR DELETE ONE CHECK (owner, 2026-10-15: "I can pause and
1299
+ * delete checks ... a button to pause checks so it never runs"), from the web
1300
+ * or a phone. checks/settings.json, which Atlas Studio writes the same way
1301
+ * and atlas_env reads at every run: a paused check never runs; a deleted one
1302
+ * also leaves the results and is not written back.
1303
+ */
1304
+ export function setCheckState(ask, root) {
1305
+ const id = String(ask?.id || '');
1306
+ const action = String(ask?.action || '');
1307
+ if (!/^[A-Za-z0-9_-]{1,80}$/.test(id)) return { ok: false, error: 'That is not a check.' };
1308
+ if (!['pause', 'resume', 'delete'].includes(action)) return { ok: false, error: 'Pause, resume or delete.' };
1309
+ const { dir } = workspace(String(ask?.job || ''), root);
1310
+ if (!fs.existsSync(dir)) return { ok: false, error: 'There is no such project on this computer.' };
1311
+ const file = path.join(dir, 'checks', 'settings.json');
1312
+ const d = readJsonFile(file) || {};
1313
+ const paused = d.paused && typeof d.paused === 'object' ? d.paused : {};
1314
+ const deleted = d.deleted && typeof d.deleted === 'object' ? d.deleted : {};
1315
+ if (action === 'pause') paused[id] = true;
1316
+ else delete paused[id];
1317
+ if (action === 'delete') {
1318
+ deleted[id] = new Date().toISOString();
1319
+ if (d.auto_resolve && typeof d.auto_resolve === 'object') delete d.auto_resolve[id];
1320
+ }
1321
+ d.paused = paused;
1322
+ d.deleted = deleted;
1323
+ try {
1324
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1325
+ fs.writeFileSync(`${file}.tmp`, JSON.stringify(d, null, 2));
1326
+ fs.renameSync(`${file}.tmp`, file);
1327
+ } catch {
1328
+ return { ok: false, error: 'That could not be saved.' };
1329
+ }
1330
+ if (action === 'delete') {
1331
+ // Its row leaves the results; its history stays (the person's record).
1332
+ const res = path.join(dir, 'checks.json');
1333
+ const r = readJsonFile(res);
1334
+ if (r && Array.isArray(r.checks) && r.checks.some((c) => c?.id === id)) {
1335
+ r.checks = r.checks.filter((c) => c?.id !== id);
1336
+ try { fs.writeFileSync(`${res}.tmp`, JSON.stringify(r, null, 2)); fs.renameSync(`${res}.tmp`, res); } catch { /* the settings already hide it */ }
1337
+ }
1338
+ }
1339
+ return { ok: true };
1340
+ }
1341
+
1267
1342
  /** One check's auto-resolve switch, in checks/settings.json (Atlas Studio reads it too). */
1268
1343
  export function setCheckAuto(ask, root) {
1269
1344
  const id = String(ask?.id || '');
package/src/config.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
 
5
- export const VERSION = '0.98.0';
5
+ export const VERSION = '0.100.0';
6
6
  export const DEFAULT_BASE_URL = 'https://atlasmcp.finmanagerai.com';
7
7
  export const DEFAULT_SCOPE = 'atlas broker';
8
8
  export const CLIENT_NAME = 'Atlas CLI';
@@ -1030,6 +1030,23 @@ def _read_json(path, default):
1030
1030
  return default
1031
1031
 
1032
1032
 
1033
+ def _check_id(value):
1034
+ return "".join(c if c.isalnum() or c in "-_" else "-" for c in str(value))[:80] or "check"
1035
+
1036
+
1037
+ def _checks_off():
1038
+ """THE CHECKS THE PERSON PAUSED OR DELETED (owner, 2026-10-15: "a button to
1039
+ pause checks so it never runs"). Kept in checks/settings.json beside the
1040
+ auto-fix switches, by Atlas Studio, the web (through the runner) and
1041
+ restore_check(). A paused or deleted check never runs: wants() says no and
1042
+ result() keeps nothing, whatever started the run (its schedule, a change,
1043
+ Run all, an agent building)."""
1044
+ d = _read_json(os.path.join(_checks_dir(), "checks", "settings.json"), {})
1045
+ paused = d.get("paused") if isinstance(d.get("paused"), dict) else {}
1046
+ deleted = d.get("deleted") if isinstance(d.get("deleted"), dict) else {}
1047
+ return {str(k) for k, v in paused.items() if v} | {str(k) for k in deleted}
1048
+
1049
+
1033
1050
  def _library_versions():
1034
1051
  try:
1035
1052
  from importlib import metadata as _md
@@ -1048,6 +1065,9 @@ class _CheckRun:
1048
1065
  import time as _time
1049
1066
  self.dir = _checks_dir()
1050
1067
  self.only = [x for x in (os.environ.get("ATLAS_CHECK_ONLY") or "").split(",") if x]
1068
+ # A run its project's schedule started, minus the checks that keep a
1069
+ # time of their own which is not now (atlas.json "checks"."each").
1070
+ self.skip = [x for x in (os.environ.get("ATLAS_CHECK_SKIP") or "").split(",") if x]
1051
1071
  self.replay_of = (os.environ.get("ATLAS_CHECK_REPLAY") or "").strip() or None
1052
1072
  self.started = _time.time()
1053
1073
  self.id = _time.strftime("%Y%m%d-%H%M%S", _time.localtime(self.started))
@@ -1069,6 +1089,8 @@ class _CheckRun:
1069
1089
  # per check): what assume() handed out, per check, in order.
1070
1090
  self._assumed = {}
1071
1091
  self._mine_cache = None
1092
+ # Paused or deleted by the person: never run, never recorded.
1093
+ self.off = _checks_off()
1072
1094
  self._watch_the_end()
1073
1095
  # THE ONE MODEL THAT RUNS THESE CHECKS (owner, 2026-10-15: "what model
1074
1096
  # performed the test"): the project's atlas.json "checks": {"model"},
@@ -1302,7 +1324,12 @@ class _CheckRun:
1302
1324
 
1303
1325
  # ── results ──
1304
1326
  def wants(self, check_id):
1305
- """False for a check this run was not asked for (Recheck of one)."""
1327
+ """False for a check this run was not asked for (Recheck of one, or a
1328
+ run on the times of a few checks), for one whose own time is not now
1329
+ in a run on the project's schedule, and for one the person paused or
1330
+ deleted."""
1331
+ if _check_id(check_id) in self.off or check_id in self.skip:
1332
+ return False
1306
1333
  return not self.only or check_id in self.only
1307
1334
 
1308
1335
  def result(self, check_id, status, title, summary="", fixed=None, report=None,
@@ -1316,7 +1343,11 @@ class _CheckRun:
1316
1343
  agent is asked when the person presses Resolve."""
1317
1344
  import time as _time
1318
1345
  status = status if status in _CHECK_STATUSES else "warn"
1319
- cid = "".join(c if c.isalnum() or c in "-_" else "-" for c in str(check_id))[:80] or "check"
1346
+ cid = _check_id(check_id)
1347
+ # Paused or deleted: nothing of it is kept, even from a checks.py that
1348
+ # did not ask wants() first.
1349
+ if cid in self.off:
1350
+ return
1320
1351
  report_rel = None
1321
1352
  if report:
1322
1353
  text = str(report)
@@ -1583,6 +1614,18 @@ def checks():
1583
1614
  return _CheckRun()
1584
1615
 
1585
1616
 
1617
+ def restore_check(check_id):
1618
+ """Bring back a check the person deleted (or paused), ONLY when they ask
1619
+ for it back: it runs again from the next run."""
1620
+ cid = _check_id(check_id)
1621
+ path = os.path.join(_checks_dir(), "checks", "settings.json")
1622
+ data = _read_json(path, {})
1623
+ for key in ("paused", "deleted"):
1624
+ if isinstance(data.get(key), dict):
1625
+ data[key].pop(cid, None)
1626
+ _write_json(path, data)
1627
+
1628
+
1586
1629
  def progress(step="", done=None, total=None, message="", **extra):
1587
1630
  """Say where you are, so the dashboard can draw it.
1588
1631
 
@@ -689,19 +689,59 @@ export function checksCommand(dir) {
689
689
 
690
690
  /**
691
691
  * When a project's checks run by themselves: atlas.json "checks": {"schedule":
692
- * "30 8 * * 1-5", "on": ["files", "data"]} (local time, like Atlas Studio).
692
+ * "30 8 * * 1-5", "on": ["files", "data"], "timezone": "America/New_York"}.
693
693
  */
694
694
  export function checksWhen(dir) {
695
- const c = readManifest(dir).checks;
695
+ const m = readManifest(dir);
696
+ const c = m.checks;
696
697
  const o = c && typeof c === 'object' ? c : {};
697
- const expr = typeof o.schedule === 'string' ? o.schedule.trim() : '';
698
- const cron = expr ? parseCron(expr) : null;
699
698
  const on = Array.isArray(o.on) ? o.on.map(String) : [];
700
- return {
701
- schedule: cron ? { expr, lines: [{ kind: 'cron', cron, text: expr }], tz: '', error: null, cron } : null,
702
- files: on.includes('files'),
703
- data: on.includes('data'),
699
+ // The zone it is read in: the checks' own, else the project's, else UTC like
700
+ // every schedule (Atlas Studio and the web say it in these words too).
701
+ const tz = String([o.timezone, m.timezone].find((z) => typeof z === 'string' && z.trim()) || '').trim();
702
+ const sched = (raw) => {
703
+ const expr = typeof raw === 'string' ? raw.trim() : '';
704
+ const cron = expr ? parseCron(expr) : null;
705
+ return cron ? { expr, lines: [{ kind: 'cron', cron, text: expr }], tz, error: null, cron } : null;
704
706
  };
707
+ // A TIME PER CHECK (owner, 2026-10-16: "for each checks, let this be here"):
708
+ // "each": {"<check id>": "<five-field cron>"}, read in the same zone. A check
709
+ // with no entry (or one that is not a cron) keeps the project's schedule.
710
+ const each = {};
711
+ const e = o.each && typeof o.each === 'object' && !Array.isArray(o.each) ? o.each : {};
712
+ for (const [id, raw] of Object.entries(e).slice(0, 200)) {
713
+ if (!CHECK_ID.test(id)) continue;
714
+ const s = sched(raw);
715
+ if (s) each[id] = s;
716
+ }
717
+ return { schedule: sched(o.schedule), each, files: on.includes('files'), data: on.includes('data') };
718
+ }
719
+
720
+ const CHECK_ID = /^[A-Za-z0-9_-]{1,80}$/;
721
+
722
+ /**
723
+ * WHICH CHECKS ARE DUE between two moments (from, to], or null for none: the
724
+ * checks whose own time ("each") came round, and, when the project's schedule
725
+ * came round, every check without a time of its own. One answer, so two
726
+ * schedules on the same minute start ONE run. `known` is the ids checks.json
727
+ * holds, `off` the paused and deleted.
728
+ * {only: [...]} : just these checks (only their own times came round);
729
+ * {only: [], skip: [...]} : all of them but these (their own time did not).
730
+ * The python runner's checks_due is the same, line for line.
731
+ */
732
+ export function checksDue(when, known, off, fromEpoch, toEpoch) {
733
+ const own = Object.keys(when?.each || {}).filter((id) => !off.has(id)).sort();
734
+ const ownDue = own.filter((id) => scheduleDueMinutes(when.each[id], fromEpoch, toEpoch).length);
735
+ const projectDue = !!(when?.schedule && scheduleDueMinutes(when.schedule, fromEpoch, toEpoch).length);
736
+ if (projectDue) {
737
+ const skip = own.filter((id) => !ownDue.includes(id));
738
+ // Nothing left to run: every check it knows is paused, deleted, or keeps
739
+ // a time of its own that is not now.
740
+ const left = known.filter((id) => !off.has(id) && !skip.includes(id));
741
+ if (known.length && !left.length) return null;
742
+ return { only: [], skip };
743
+ }
744
+ return ownDue.length ? { only: ownDue, skip: [] } : null;
705
745
  }
706
746
 
707
747
  /** What a folder holds, as one string that changes when anything in it does. */
@@ -1448,12 +1488,72 @@ export function readJobTable(job, rel, root) {
1448
1488
  }
1449
1489
  }
1450
1490
 
1491
+ /**
1492
+ * A PDF, Word or PowerPoint file goes to the web as its bytes, like a picture
1493
+ * (owner, 2026-10-15: "pdf file support, word docs ... for everything"): the
1494
+ * website draws it (its components/files/DocView.tsx). Read as text it was a
1495
+ * screenful of bytes, or refused past the text ceiling. Since 2026-10-16 the
1496
+ * old Office files (.doc, .ppt, .xls) and Parquet too (owner: "yes for
1497
+ * parquet ... yes for old office files"): the website reads their words or
1498
+ * their first rows itself. Nothing else is sent as bytes: a zip or an
1499
+ * OpenDocument file is not drawn there, so it is not carried.
1500
+ */
1501
+ export const DOC_TYPES = {
1502
+ '.pdf': 'application/pdf',
1503
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1504
+ '.docm': 'application/vnd.ms-word.document.macroEnabled.12',
1505
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1506
+ '.pptm': 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
1507
+ '.ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
1508
+ '.doc': 'application/msword',
1509
+ '.ppt': 'application/vnd.ms-powerpoint',
1510
+ '.xls': 'application/vnd.ms-excel',
1511
+ '.parquet': 'application/vnd.apache.parquet',
1512
+ };
1513
+
1514
+ /** The relay's ceiling for a document: a picture's. Atlas's server holds it too. */
1515
+ export const MAX_DOC_BYTES = MAX_IMAGE_BYTES;
1516
+
1517
+ /** The content type for a document's name, or '' if it is not one. */
1518
+ export function docType(name) {
1519
+ const n = String(name).toLowerCase();
1520
+ const dot = n.lastIndexOf('.');
1521
+ return dot >= 0 ? (DOC_TYPES[n.slice(dot)] || '') : '';
1522
+ }
1523
+
1524
+ /** A Jupyter notebook: JSON text with its outputs' pictures inside. */
1525
+ export function isNotebook(name) {
1526
+ return String(name).toLowerCase().endsWith('.ipynb');
1527
+ }
1528
+
1529
+ /** "12.4 MB", the website's words (lib/doc-view.ts sizeWords). */
1530
+ function sizeWords(bytes) {
1531
+ if (bytes < 1024) return `${bytes} bytes`;
1532
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
1533
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1534
+ }
1535
+
1451
1536
  export function readJobFile(job, rel, root) {
1452
1537
  const where = jobTarget(job, rel, root);
1453
1538
  if (!where.ok) return where;
1454
1539
  const { target } = where;
1455
1540
  const mime = imageType(path.basename(target));
1541
+ const doc = docType(path.basename(target));
1456
1542
  try {
1543
+ if (doc) {
1544
+ const size = fs.statSync(target).size;
1545
+ // Worded as the website's viewer words it, which tells it apart from a
1546
+ // computer from before (that one refused a document as too long a text).
1547
+ if (size > MAX_DOC_BYTES) return { ok: false, error: `This file is ${sizeWords(size)}, too large to show here.` };
1548
+ return { ok: true, mime: doc, b64: fs.readFileSync(target).toString('base64') };
1549
+ }
1550
+ // A JUPYTER NOTEBOOK (2026-10-16) is text, but its pictures are inside it:
1551
+ // it may be as large as a document, and past that it is worded the same.
1552
+ if (isNotebook(target)) {
1553
+ const size = fs.statSync(target).size;
1554
+ if (size > MAX_DOC_BYTES) return { ok: false, error: `This file is ${sizeWords(size)}, too large to show here.` };
1555
+ return { ok: true, text: fs.readFileSync(target, 'utf8') };
1556
+ }
1457
1557
  if (fs.statSync(target).size > (mime ? MAX_IMAGE_BYTES : MAX_VIEW_BYTES)) {
1458
1558
  return { ok: false, error: 'That file is too large to show here.' };
1459
1559
  }
@@ -1805,7 +1905,7 @@ export function writeRequest(job, op, root, extra = {}) {
1805
1905
  try {
1806
1906
  fs.mkdirSync(dir, { recursive: true });
1807
1907
  // A checks request says which checks and which run it repeats.
1808
- const more = want === 'checks' ? { only: extra.only, replay: extra.replay, why: extra.why, assume: extra.assume } : {};
1908
+ const more = want === 'checks' ? { only: extra.only, skip: extra.skip, replay: extra.replay, why: extra.why, assume: extra.assume } : {};
1809
1909
  fs.writeFileSync(path.join(dir, `${at}-${want}.json`), JSON.stringify({ op: want, at: at / 1000, ...more }), 'utf8');
1810
1910
  } catch (err) {
1811
1911
  return { ok: false, error: `Could not leave the request: ${err.message}` };
@@ -1849,7 +1949,7 @@ export function takeRequests(root, nowMs = Date.now()) {
1849
1949
  if (Number.isFinite(body.at) && nowMs - body.at * 1000 > REQUEST_TTL_MS) continue;
1850
1950
  // A checks request carries which checks and which run it repeats.
1851
1951
  out.push(op === 'checks'
1852
- ? { dir: path.join(base, slug), op, only: body.only, replay: body.replay, why: body.why, assume: body.assume }
1952
+ ? { dir: path.join(base, slug), op, only: body.only, skip: body.skip, replay: body.replay, why: body.why, assume: body.assume }
1853
1953
  : { dir: path.join(base, slug), op });
1854
1954
  }
1855
1955
  }