@barivia/barmesh-mcp 0.7.1 → 0.8.2

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/README.md CHANGED
@@ -13,16 +13,16 @@ form on a shared self-organizing map (SOM)**:
13
13
  - **`barmesh_mesh_convergence`** — trains one SOM on all meshes (joint-normalized), projects
14
14
  each mesh to a volume-weighted fingerprint, and computes **symmetric KL** and
15
15
  **Wasserstein-1 (EMD)** distances stepwise and against a reference mesh, with publication
16
- figures and an advisory convergence reading. The SOM features are preprocessed by the
17
- same staged pipeline as barsom training, so any dataset (small or large, CSV/gzip)
18
- is handled out-of-core by default; optional `transforms`, `normalize`,
19
- `normalization_methods`, and `row_range` give the same preprocessing controls. Submit
20
- enqueues **`prepare_training_matrix`** on worker-io when the dataset is staged; the proxy
21
- auto-polls `prepare_job_id` before the mesh job runs. Default **`defer_figures=true`** →
22
- **`cfd_finalize`** on worker-io; **`barmesh_jobs(status)`** auto-polls **`finalize_job_id`**
23
- when figures are deferred.
16
+ figures and an advisory convergence reading. Default **`mesh_order`** sorts by ascending
17
+ cell count and **interleaves U/G (or similar) families** when counts tie, so stepwise
18
+ distances do not jump across topology families. The SOM features use the same staged
19
+ pipeline as barsom training; optional `transforms`, `normalize`, `normalization_methods`,
20
+ and `row_range` apply. Submit enqueues **`prepare_training_matrix`** on worker-io when
21
+ staged; the proxy auto-polls `prepare_job_id`. Default **`defer_figures=true`** →
22
+ **`cfd_finalize`**; **`barmesh_jobs(status)`** auto-polls **`finalize_job_id`**.
24
23
  - **`barmesh_richardson`** — classical three-level Richardson extrapolation / Grid
25
- Convergence Index (GCI) on scalar quantities of interest.
24
+ Convergence Index (GCI) on scalar quantities of interest. Prefer **one topology family**
25
+ (all U or all G); mixed families return a **`topology_warning`** in the result summary.
26
26
 
27
27
  These complement, and do not replace, conventional numerical uncertainty analysis.
28
28
 
@@ -56,9 +56,9 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
56
56
  | `barmesh_mesh_convergence` | SOM fingerprint distances (async job). |
57
57
  | `barmesh_richardson` | Richardson/GCI on scalar QoIs (async job). |
58
58
  | `barmesh_jobs` | Poll job status / block until terminal (`action=monitor`) / list jobs. Auto-polls CFD prepare + finalize when applicable. Reports phase, epoch/total, elapsed, ETA, and QE live during the SOM training. |
59
- | `barmesh_training_monitor` | Alias for `barmesh_jobs(action=monitor)` server-side poll with throttled snapshots until terminal (preferred after submit). |
60
- | `barmesh_results` | Distances, convergence reading, and figures. `action=get` inlines headline PNGs; `action=download` saves artifacts to disk; `action=render` produces publication PDFs on demand. |
61
- | `barmesh_results_explorer` | Interactive MCP App with a **figure dropdown above the plot** (or standalone localhost page) to browse every figure after a job completes. |
59
+ | `barmesh_training_monitor` | Live MCP App (QE + panel TE) and/or headless monitor; always returns a standalone localhost URL + `ui_delivery`. |
60
+ | `barmesh_results` | Distances, convergence reading, and figures. `action=get` inlines headline PNGs; `action=download` saves artifacts to disk; `action=render` produces publication PDFs on demand; `action=feature_divergences` refreshes feature-plane KL/W1 (distinct from mesh SKL/EMD). |
61
+ | `barmesh_results_explorer` | Interactive MCP App with a **figure dropdown above the plot**; always leads with a standalone localhost URL + `ui_delivery`. |
62
62
  | `barmesh_send_feedback` | Send a short note or bug report to the Barivia team. |
63
63
 
64
64
  ### Figures and progress (0.5.2)
@@ -69,6 +69,11 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
69
69
  `plot_vol_all_meshes.png` shows volume fingerprints for every mesh in mesh_order.
70
70
  - **Learning curve:** every job produces `learning_curve.png` (QE by epoch) for training
71
71
  quality inspection; listed in the results explorer dropdown.
72
+ - **Asymmetric mesh KL (`KL_asymmetric.png`):** pairwise ``D_{KL}(P\\|Q)`` heatmap —
73
+ above-diagonal coarse→fine, below-diagonal fine→coarse (meshes ordered coarse→fine).
74
+ Distinct from feature-plane `divergence_kl` (component association).
75
+ - **Live ΔKL in training monitor:** panel SKL→ref mean/max curve (TE-panel sample) beside
76
+ the hit-grid; SOM batch-step axis labels (not FLooP) for regular mesh_convergence jobs.
72
77
  - **PDFs on demand:** publication vector PDFs are NOT generated by default. Render them
73
78
  after completion with `barmesh_results(action=render, format=pdf)`, then download with
74
79
  `barmesh_results(action=download, folder=...)` or `action=image`.
@@ -85,6 +90,8 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
85
90
 
86
91
  ### Migration notes
87
92
 
93
+ - **Feature-plane divergences (0.8.1):** Mesh jobs write `divergence_kl` / `feature_divergences.json` / `weights.json` (barsom-parallel). These measure association between SOM **component planes** — not mesh-vs-mesh fingerprint SKL/EMD (`KL_ref`, `EMD_*`). Refresh or add Wasserstein via `barmesh_results(action=feature_divergences)`.
94
+ - **UI delivery + mesh_order (0.7.3+ / 0.8.0):** App tools emit structured `ui_delivery` and **always put the standalone viz URL first** (hosts may advertise MCP Apps without mounting widgets). Env: `BARIVIA_UI_DELIVERY` (`auto` \| `apps` \| `localhost` \| `inline`), `BARIVIA_VIZ_PORT`. Default mesh_order interleaves U/G at tied cell counts; Richardson warns on mixed topology families.
88
95
  - **Fixed-panel live TE (0.6.3 / barsom 0.20.4):** mid-training TE uses a fixed evaluation panel (`te_panel_size`; `te_inner_samples` alias). Curves stay on panel TE; monitors show **Panel TE** and **Map TE** separately — no snap-to-map on the curve tail.
89
96
  - **`barmesh_training_monitor` (0.5.3):** server-side blocking monitor with throttled snapshots — preferred after job submit instead of manual `barmesh_jobs(status)` loops. Equivalent to `barmesh_jobs(action=monitor)`.
90
97
  - **`send_feedback` → `barmesh_send_feedback` (0.3.0):** the feedback tool was renamed so it no longer collides with the `@barivia/barsom-mcp` tool of the same name when both servers are enabled in one client. Update any direct call sites; the behavior is unchanged.
@@ -110,4 +117,5 @@ Parquet staging is supported by the API but not yet exposed as an MCP upload for
110
117
  | `BARIVIA_FETCH_TIMEOUT_MS` | `60000` | Per-request timeout (raise for large uploads). |
111
118
  | `BARIVIA_WORKSPACE_ROOT` | workspace/cwd | Root for resolving relative `file_path` uploads. |
112
119
  | `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | `1` | Restrict uploads to the workspace; set `0` to allow absolute paths. |
113
- | `BARIVIA_VIZ_PORT` | ephemeral | Fixed localhost port for the `barmesh_results_explorer` standalone page (otherwise OS-assigned per session). |
120
+ | `BARIVIA_VIZ_PORT` | ephemeral | Fixed localhost port for standalone App pages (otherwise OS-assigned per session). |
121
+ | `BARIVIA_UI_DELIVERY` | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline`. Responses always lead with a standalone localhost URL. |
package/dist/audit.js CHANGED
@@ -47,10 +47,12 @@ function extractIds(result) {
47
47
  function errorCodeFrom(err) {
48
48
  if (err && typeof err === "object") {
49
49
  const e = err;
50
+ if (e.errorCode)
51
+ return e.errorCode;
50
52
  if (e.httpStatus !== undefined)
51
53
  return `http_${e.httpStatus}`;
52
54
  const m = e.message ?? "";
53
- const codeMatch = m.match(/error_code:\s*(\w+)/);
55
+ const codeMatch = m.match(/error_code[=:]\s*(\w+)/);
54
56
  if (codeMatch)
55
57
  return codeMatch[1];
56
58
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Local offline queue for barmesh_send_feedback when the API or object storage is down.
3
+ * Persists under ~/.barivia/deferred-feedback (or BARIVIA_FEEDBACK_QUEUE_DIR).
4
+ * Same layout as @barivia/barsom-mcp so both proxies share one queue directory.
5
+ */
6
+ import fs from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { randomUUID } from "node:crypto";
10
+ export function deferredFeedbackQueueDir() {
11
+ const override = process.env.BARIVIA_FEEDBACK_QUEUE_DIR?.trim();
12
+ if (override)
13
+ return path.resolve(override);
14
+ return path.join(os.homedir(), ".barivia", "deferred-feedback");
15
+ }
16
+ async function ensureQueueDir() {
17
+ const dir = deferredFeedbackQueueDir();
18
+ await fs.mkdir(dir, { recursive: true });
19
+ return dir;
20
+ }
21
+ function entryPath(dir, id) {
22
+ return path.join(dir, `${id}.json`);
23
+ }
24
+ export async function enqueueDeferredFeedback(product, body, lastError) {
25
+ const dir = await ensureQueueDir();
26
+ const id = randomUUID();
27
+ const entry = {
28
+ id,
29
+ product,
30
+ created_at: new Date().toISOString(),
31
+ body,
32
+ ...(lastError ? { last_error: lastError } : {}),
33
+ };
34
+ const filePath = entryPath(dir, id);
35
+ await fs.writeFile(filePath, JSON.stringify(entry, null, 2), "utf8");
36
+ return { id, path: filePath };
37
+ }
38
+ export async function listDeferredFeedback(product) {
39
+ const dir = deferredFeedbackQueueDir();
40
+ let names;
41
+ try {
42
+ names = await fs.readdir(dir);
43
+ }
44
+ catch (err) {
45
+ if (err.code === "ENOENT")
46
+ return [];
47
+ throw err;
48
+ }
49
+ const out = [];
50
+ for (const name of names) {
51
+ if (!name.endsWith(".json"))
52
+ continue;
53
+ try {
54
+ const raw = await fs.readFile(path.join(dir, name), "utf8");
55
+ const entry = JSON.parse(raw);
56
+ if (product && entry.product !== product)
57
+ continue;
58
+ out.push(entry);
59
+ }
60
+ catch {
61
+ /* skip corrupt */
62
+ }
63
+ }
64
+ out.sort((a, b) => a.created_at.localeCompare(b.created_at));
65
+ return out;
66
+ }
67
+ /**
68
+ * Attempt to submit each queued entry via `submit`. Successful entries are deleted.
69
+ * Failures leave the file in place for a later retry.
70
+ */
71
+ export async function flushDeferredFeedback(product, submit) {
72
+ const entries = await listDeferredFeedback(product);
73
+ let flushed = 0;
74
+ const errors = [];
75
+ const dir = deferredFeedbackQueueDir();
76
+ for (const entry of entries) {
77
+ try {
78
+ await submit(entry.body);
79
+ await fs.unlink(entryPath(dir, entry.id)).catch(() => undefined);
80
+ flushed += 1;
81
+ }
82
+ catch (err) {
83
+ errors.push(err instanceof Error ? err.message : String(err));
84
+ }
85
+ }
86
+ const remaining = (await listDeferredFeedback(product)).length;
87
+ return { flushed, remaining, errors };
88
+ }
@@ -24,6 +24,9 @@ export function figureSection(key) {
24
24
  }
25
25
  if (key === "learning_curve" || key.startsWith("plot_vol"))
26
26
  return "diagnostics";
27
+ // Feature-plane association heatmaps (barsom-parallel) — not mesh fingerprint KL/EMD.
28
+ if (key.startsWith("divergence_"))
29
+ return "component";
27
30
  if (key.startsWith("plot_"))
28
31
  return "component";
29
32
  return "diagnostics";
@@ -33,17 +36,22 @@ export const FIGURE_SORT_RANK = {
33
36
  overview_distances: 1,
34
37
  KL_ref: 10,
35
38
  KL_stepwise: 11,
36
- EMD_ref: 12,
37
- EMD_stepwise: 13,
38
- learning_curve: 14,
39
+ KL_asymmetric: 12,
40
+ EMD_ref: 13,
41
+ EMD_stepwise: 14,
42
+ learning_curve: 15,
39
43
  plot_vol_all_meshes: 19,
40
44
  plot_vol_coarse_fine: 20,
45
+ divergence_kl: 28,
46
+ divergence_w1: 29,
41
47
  };
42
48
  export function sortRank(key) {
43
49
  if (key in FIGURE_SORT_RANK)
44
50
  return FIGURE_SORT_RANK[key];
45
51
  if (key.startsWith("plot_vol"))
46
52
  return 25;
53
+ if (key.startsWith("divergence_"))
54
+ return 28;
47
55
  if (key.startsWith("plot_"))
48
56
  return 30;
49
57
  return 50;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as r}from"@modelcontextprotocol/sdk/server/stdio.js";import{getUiCapability as s,registerAppResource as o,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as t}from"./viz-server.js";import{API_KEY as i,CLIENT_VERSION as a,apiCall as c,apiRawCall as m,loadViewHtml as l,setVizPort as d,setClientSupportsMcpApps as p}from"./shared.js";import{registerGuideTool as h}from"./tools/guide.js";import{registerDatasetsTool as u}from"./tools/datasets.js";import{registerCfdTools as b}from"./tools/cfd.js";import{registerJobsTool as f}from"./tools/jobs.js";import{registerResultsTool as v}from"./tools/results.js";import{registerResultsExplorerTool as y,RESULTS_EXPLORER_URI as _}from"./tools/barmesh_results_explorer.js";import{registerTrainingMonitorTool as g,TRAINING_MONITOR_URI as w}from"./tools/training_monitor.js";import{registerFeedbackTool as j}from"./tools/feedback.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));(async function(){const i=new e({name:"barmesh",version:a},{instructions:"# Barivia barmesh — CFD mesh-convergence analytics\n\nSOM-based mesh-convergence verification: compare CFD meshes of a refinement study by the\nvolume-weighted distribution their cells form on a shared self-organizing map, plus\nclassical Richardson/GCI on scalar quantities.\n\n## Two tracks\n- barmesh_mesh_convergence: high-dimensional field comparison. Symmetric KL and\n Wasserstein-1 (EMD) distances between each mesh's SOM fingerprint and a reference,\n and between consecutive meshes. Decreasing, plateauing distances toward the finest\n mesh indicate sufficiency. Complements (does not replace) numerical uncertainty analysis.\n- barmesh_richardson: classical grid-convergence index on scalar QoIs.\n\n## Workflow (read-only first)\n1. barmesh_guide_workflow — orient and confirm your plan includes CFD tools.\n2. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V).\n3. barmesh_datasets(action=upload) then preview.\n4. barmesh_mesh_convergence (and/or barmesh_richardson) — returns a job id.\n5. barmesh_training_monitor(job_id) — visual MCP App with live curves (default); barmesh_jobs(action=monitor) — headless blocking snapshots; or barmesh_jobs(status) for one-shot polls every 10-20s.\n6. barmesh_results(action=get) — distances, convergence reading, and figures; then\n barmesh_results_explorer(job_id) to browse every figure interactively.\n\nThese tools are gated by the 'cfd' entitlement; analysis calls return 403 if your plan\ndoes not include it."});o(i,_,_,{mimeType:n},async()=>{const e=await l("barmesh-results-explorer");return{contents:[{uri:_,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),o(i,w,w,{mimeType:n},async()=>{const e=await l("barmesh-training-monitor");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Training Monitor view not built yet. Run: npm run build:views</body></html>"}]}}),h(i),y(i),u(i),b(i),f(i),g(i),v(i),j(i);try{const e=await t(c,m,l);d(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("barmesh viz server failed to start:",e)}const x=i.server;x.oninitialized=()=>{const e=x.getClientCapabilities(),r=s(e);p(!!r?.mimeTypes?.includes(n))};const I=new r;await i.connect(I),console.error(`barmesh-mcp ${a} ready (API: ${process.env.BARIVIA_API_URL??"https://api.barivia.se"})`)})().catch(e=>{console.error("Fatal error starting barmesh-mcp:",e),process.exit(1)});
2
+ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as r}from"@modelcontextprotocol/sdk/server/stdio.js";import{getUiCapability as s,registerAppResource as t,RESOURCE_MIME_TYPE as o}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as n}from"./viz-server.js";import{API_KEY as a,CLIENT_VERSION as i,apiCall as l,apiRawCall as c,loadViewHtml as m,setVizPort as d,setClientSupportsMcpApps as h}from"./shared.js";import{registerGuideTool as u}from"./tools/guide.js";import{registerDatasetsTool as p}from"./tools/datasets.js";import{registerCfdTools as b}from"./tools/cfd.js";import{registerJobsTool as f}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerResultsExplorerTool as y,RESULTS_EXPLORER_URI as v}from"./tools/barmesh_results_explorer.js";import{registerTrainingMonitorTool as g,TRAINING_MONITOR_URI as w}from"./tools/training_monitor.js";import{registerFeedbackTool as I}from"./tools/feedback.js";a||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));(async function(){const a=new e({name:"barmesh",version:i},{instructions:"# Barivia barmesh — CFD mesh-convergence analytics\n\nSOM-based mesh-convergence verification: compare CFD meshes of a refinement study by the\nvolume-weighted distribution their cells form on a shared self-organizing map, plus\nclassical Richardson/GCI on scalar quantities.\n\n## Two tracks\n- barmesh_mesh_convergence: high-dimensional field comparison. Symmetric KL and\n Wasserstein-1 (EMD) distances between each mesh's SOM fingerprint and a reference,\n and between consecutive meshes. Decreasing, plateauing distances toward the finest\n mesh indicate sufficiency. Complements (does not replace) numerical uncertainty analysis.\n- barmesh_richardson: classical grid-convergence index on scalar QoIs.\n\n## Workflow (read-only first)\n1. barmesh_guide_workflow — orient and confirm your plan includes CFD tools.\n2. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V).\n3. barmesh_datasets(action=upload) then preview (optional description/tags for inventory).\n4. barmesh_mesh_convergence (and/or barmesh_richardson) — returns a job id.\n5. barmesh_training_monitor(job_id) — visual MCP App with live curves (default); barmesh_jobs(action=monitor) — headless blocking snapshots; or barmesh_jobs(status) for one-shot polls every 10-20s.\n6. barmesh_results(action=get) — distances, convergence reading, and figures; then\n barmesh_results_explorer(job_id) to browse every figure interactively.\n\n## Org inventory recipe\n- Prefer `barmesh_jobs(action=inventory)` for a markdown datasets+jobs overview.\n- Or compose `barmesh_datasets(list)` + `barmesh_jobs(action=list, limit=50)` (page with `cursor`; filter with `status`/`job_type`/`has_results=true` for a results catalog).\n- Set `description`+`tags` on upload/submit/update so duplicates stay navigable. List is slim by default (no fat params).\n\n## UI delivery (barmesh_training_monitor, barmesh_results_explorer)\n\nHosts vary: some embed MCP Apps; others list ui:// but never mount them (expected on some Cursor builds). Tool results always lead with a standalone localhost URL and a structured ui_delivery block (delivery, urls, hint_for_agent) — follow hint_for_agent without user prompting.\n\n- Surface the standalone URL as a clickable markdown link. Do not bury it after metrics.\n- Env: BARIVIA_UI_DELIVERY=auto|apps|localhost|inline; BARIVIA_VIZ_PORT pins the viz port across restarts.\n- Standalone pages cross-link training monitor ↔ results explorer for the same job_id.\n\nThese tools are gated by the 'cfd' entitlement; analysis calls return 403 if your plan\ndoes not include it.\n\n## API briefly unavailable (demos)\nIf barmesh_jobs / barmesh_results / barmesh_send_feedback (or any tool) reports the mesh analysis\nAPI temporarily unavailable: say so in one calm line, do not paste HTML/502 pages, and pause\nafter the proxy's burst retries (3 tries ~2s apart; then retry_after_sec≈30). Figures already\ndownloaded stay usable. Keep talking about the last good distances, GCI CSV, and figures —\ndegrade to \"review what we have\", not \"everything failed\". Suggest a later retry for new jobs\nor feedback.\n\n## Outages / feedback\n- barmesh_api_health probes GET /health + /ready (no R2). Use when tools fail with api_unreachable.\n- barmesh_send_feedback queues drafts under ~/.barivia/deferred-feedback when the API/gateway\n is down and flushes them on the next successful call (shared directory with barsom send_feedback).\n- API errors are slim structured lines (error_code, cause, request_id, retry_after_sec) — never raw Cloudflare HTML."});t(a,v,v,{mimeType:o},async()=>{const e=await m("barmesh-results-explorer");return{contents:[{uri:v,mimeType:o,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),t(a,w,w,{mimeType:o},async()=>{const e=await m("barmesh-training-monitor");return{contents:[{uri:w,mimeType:o,text:e??"<html><body>Training Monitor view not built yet. Run: npm run build:views</body></html>"}]}}),u(a),y(a),p(a),b(a),f(a),g(a),_(a),I(a);try{const e=await n(l,c,m);d(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("barmesh viz server failed to start:",e)}const j=a.server;j.oninitialized=()=>{const e=j.getClientCapabilities(),r=s(e);h(!!r?.mimeTypes?.includes(o))};const k=new r;await a.connect(k),console.error(`barmesh-mcp ${i} ready (API: ${process.env.BARIVIA_API_URL??"https://api.barivia.se"})`)})().catch(e=>{console.error("Fatal error starting barmesh-mcp:",e),process.exit(1)});
@@ -0,0 +1,69 @@
1
+ /** Shared markdown inventory formatting for barmesh list/inventory. */
2
+ export function formatTags(tags) {
3
+ if (!Array.isArray(tags) || tags.length === 0)
4
+ return "";
5
+ return tags.map((t) => String(t)).filter(Boolean).join(", ");
6
+ }
7
+ export function formatDatasetInventoryLine(ds) {
8
+ const id = String(ds.id ?? "");
9
+ const name = String(ds.name ?? "");
10
+ const rows = ds.rows != null ? Number(ds.rows) : "?";
11
+ const cols = ds.cols != null ? Number(ds.cols) : "?";
12
+ const st = ds.status != null ? String(ds.status) : "ready";
13
+ const statusBit = st !== "ready" ? ` | status=${st}` : "";
14
+ const desc = ds.description != null && String(ds.description).trim() !== ""
15
+ ? ` — ${String(ds.description).trim()}`
16
+ : "";
17
+ const tags = formatTags(ds.tags);
18
+ const tagsBit = tags ? ` [${tags}]` : "";
19
+ return `${name} (${id}) — ${rows}×${cols}${statusBit}${tagsBit}${desc}`;
20
+ }
21
+ export function formatJobInventoryLine(job) {
22
+ const id = String(job.id ?? "");
23
+ const st = String(job.status ?? "");
24
+ const label = job.label != null && job.label !== "" ? String(job.label) : null;
25
+ const jt = job.job_type != null ? String(job.job_type) : "?";
26
+ const ds = job.dataset_id != null ? String(job.dataset_id) : "";
27
+ const created = job.created_at != null ? String(job.created_at) : "";
28
+ const result = job.result_ref != null && String(job.result_ref) !== "" ? "yes" : "no";
29
+ const tags = formatTags(job.tags);
30
+ const tagsBit = tags ? ` [${tags}]` : "";
31
+ const desc = job.description != null && String(job.description).trim() !== ""
32
+ ? ` — ${String(job.description).trim()}`
33
+ : "";
34
+ const head = label ? `${label} (id: ${id})` : `id: ${id}`;
35
+ return `${head} — ${st} | type=${jt} | dataset=${ds} | created=${created} | result=${result}${tagsBit}${desc}`;
36
+ }
37
+ export function extractJobsList(data) {
38
+ if (Array.isArray(data)) {
39
+ return { jobs: data, next_cursor: null };
40
+ }
41
+ if (data && typeof data === "object") {
42
+ const obj = data;
43
+ const jobs = Array.isArray(obj.jobs) ? obj.jobs : [];
44
+ const next = obj.next_cursor != null && String(obj.next_cursor) !== ""
45
+ ? String(obj.next_cursor)
46
+ : null;
47
+ return { jobs, next_cursor: next };
48
+ }
49
+ return { jobs: [], next_cursor: null };
50
+ }
51
+ export function buildJobsListQuery(opts) {
52
+ const qs = new URLSearchParams();
53
+ if (opts.dataset_id)
54
+ qs.set("dataset_id", opts.dataset_id);
55
+ if (opts.status)
56
+ qs.set("status", opts.status);
57
+ if (opts.job_type)
58
+ qs.set("job_type", opts.job_type);
59
+ if (opts.has_results)
60
+ qs.set("has_results", "true");
61
+ if (opts.limit != null)
62
+ qs.set("limit", String(opts.limit));
63
+ if (opts.cursor)
64
+ qs.set("cursor", opts.cursor);
65
+ if (opts.fields)
66
+ qs.set("fields", opts.fields);
67
+ const s = qs.toString();
68
+ return s ? `/v1/jobs?${s}` : "/v1/jobs";
69
+ }
@@ -0,0 +1,35 @@
1
+ /** Format barmesh_results(get) text for cfd_richardson_gci jobs. */
2
+ export function formatRichardsonResultsSummary(jobId, data, summary) {
3
+ const label = data.label != null && data.label !== "" ? String(data.label) : null;
4
+ const header = label
5
+ ? `Richardson/GCI results for ${label} (job_id: ${jobId})`
6
+ : `Richardson/GCI results for job_id: ${jobId}`;
7
+ const qois = summary.qoi_columns ?? [];
8
+ const meshOrder = summary.mesh_order_fine_to_coarse ?? [];
9
+ const fmt = (v) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(4) : String(v ?? "N/A"));
10
+ const results = summary.results ?? {};
11
+ const lines = [
12
+ header,
13
+ `Meshes (fine→coarse): ${meshOrder.length > 0 ? meshOrder.join(" → ") : "N/A"}`,
14
+ `Safety factor Fs: ${fmt(summary.safety_factor ?? 1.25)} | h source: ${String(summary.h_source ?? "n_cells_column")}`,
15
+ ];
16
+ const warning = summary.topology_warning;
17
+ if (typeof warning === "string" && warning.length > 0) {
18
+ lines.push(`\n⚠ Topology: ${warning}`);
19
+ }
20
+ for (const q of qois) {
21
+ const triplets = results[q] ?? [];
22
+ if (triplets.length === 0)
23
+ continue;
24
+ lines.push(`\nQoI ${q} (${triplets.length} triplet${triplets.length === 1 ? "" : "s"}):`);
25
+ for (const t of triplets) {
26
+ const mono = t.monotonic === true ? "monotonic" : t.monotonic === false ? "non-monotonic" : "monotonicity n/a";
27
+ lines.push(` ${String(t.triplet ?? "?")}: p=${fmt(t.p)} f_ext=${fmt(t.f_ext)} GCI_fine=${fmt(t.gci_fine_pct)}% (${mono})`);
28
+ }
29
+ }
30
+ lines.push("\nFull table: richardson_gci.csv (one row per triplet per QoI).", "Advisory: classical GCI complements SOM fingerprint distances (barmesh_mesh_convergence).", "Use barmesh_results(action=download, figures=none, include_json=true) to save richardson_gci.csv locally.");
31
+ return lines.join("\n");
32
+ }
33
+ export function isRichardsonJob(summary) {
34
+ return String(summary.job_type ?? "") === "cfd_richardson_gci";
35
+ }