@barivia/barmesh-mcp 0.5.3 → 0.6.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/README.md +2 -1
- package/dist/blocking_monitor.js +34 -0
- package/dist/convergence_reading.js +25 -0
- package/dist/figure_sections.js +50 -0
- package/dist/index.js +1 -1
- package/dist/job_monitor.js +13 -2
- package/dist/results_metadata.js +78 -0
- package/dist/shared.js +18 -2
- package/dist/tools/barmesh_results_explorer.js +29 -51
- package/dist/tools/guide.js +1 -1
- package/dist/tools/jobs.js +4 -3
- package/dist/tools/results.js +46 -7
- package/dist/tools/training_monitor.js +72 -52
- package/dist/training_monitor_curve.js +30 -0
- package/dist/training_review.js +182 -0
- package/dist/views/src/views/barmesh-results-explorer/index.html +20 -19
- package/dist/views/src/views/barmesh-training-monitor/index.html +155 -0
- package/dist/viz-server.js +52 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -65,7 +65,8 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
|
|
|
65
65
|
|
|
66
66
|
- **Combined overview (`combined.png`):** barsom-style **grid of all component planes**
|
|
67
67
|
on the trained SOM — the primary headline artifact. A separate
|
|
68
|
-
`overview_distances.png` holds the KL/EMD
|
|
68
|
+
`overview_distances.png` holds the KL/EMD diagnostic mosaic (all four distance panels).
|
|
69
|
+
`plot_vol_all_meshes.png` shows volume fingerprints for every mesh in mesh_order.
|
|
69
70
|
- **Learning curve:** every job produces `learning_curve.png` (QE by epoch) for training
|
|
70
71
|
quality inspection; listed in the results explorer dropdown.
|
|
71
72
|
- **PDFs on demand:** publication vector PDFs are NOT generated by default. Render them
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resetInlineAttachBudget, structuredTextResult, tryAttachImage, } from "./shared.js";
|
|
2
|
+
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC, formatMonitorText, monitorJob, } from "./job_monitor.js";
|
|
3
|
+
/** Headless blocking monitor for barmesh_jobs(action=monitor). */
|
|
4
|
+
export async function runBlockingMonitor(args) {
|
|
5
|
+
const block_until_sec = args.block_until_sec ?? DEFAULT_BLOCK_UNTIL_SEC;
|
|
6
|
+
const poll_interval_sec = args.poll_interval_sec ?? DEFAULT_POLL_INTERVAL_SEC;
|
|
7
|
+
const result = await monitorJob(args.job_id, {
|
|
8
|
+
block_until_sec,
|
|
9
|
+
poll_interval_sec,
|
|
10
|
+
wait_finalize: args.wait_finalize,
|
|
11
|
+
});
|
|
12
|
+
const reviewMode = result.data.monitor_mode === "review";
|
|
13
|
+
const text = reviewMode
|
|
14
|
+
? String(result.data.status_text ?? "")
|
|
15
|
+
: formatMonitorText(result, { block_until_sec, poll_interval_sec });
|
|
16
|
+
resetInlineAttachBudget();
|
|
17
|
+
const content = [{ type: "text", text }];
|
|
18
|
+
if (reviewMode && result.terminal && String(result.data.status) === "completed") {
|
|
19
|
+
await tryAttachImage(content, args.job_id, "learning_curve.png");
|
|
20
|
+
}
|
|
21
|
+
return structuredTextResult({
|
|
22
|
+
...result.data,
|
|
23
|
+
monitor: {
|
|
24
|
+
job_id: result.job_id,
|
|
25
|
+
terminal: result.terminal,
|
|
26
|
+
timed_out: result.timed_out,
|
|
27
|
+
mode: reviewMode ? "review" : "live",
|
|
28
|
+
snapshots: result.snapshots,
|
|
29
|
+
status_text: result.status_text,
|
|
30
|
+
suggested_next_step: result.suggested_next_step,
|
|
31
|
+
},
|
|
32
|
+
status_text: text,
|
|
33
|
+
}, undefined, content);
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single canonical convergence reading formatter (summary.json / cfd_metrics fields).
|
|
3
|
+
* Used by barmesh_results(get) only — explorer does not repeat this prose.
|
|
4
|
+
*/
|
|
5
|
+
export function formatConvergenceReading(summary) {
|
|
6
|
+
const convergence = summary.convergence;
|
|
7
|
+
if (!convergence)
|
|
8
|
+
return null;
|
|
9
|
+
const emd = convergence.emd;
|
|
10
|
+
const skl = convergence.skl;
|
|
11
|
+
const ref = String(summary.reference_mesh ?? "reference");
|
|
12
|
+
const parts = [];
|
|
13
|
+
if (emd) {
|
|
14
|
+
const plateau = emd.plateau === true ? "plateauing" : "not plateauing";
|
|
15
|
+
const mono = emd.monotonic_decrease === true ? "monotonic decrease" : "non-monotonic";
|
|
16
|
+
parts.push(`EMD vs ${ref}: ${mono}, ${plateau}`);
|
|
17
|
+
}
|
|
18
|
+
if (skl) {
|
|
19
|
+
const plateau = skl.plateau === true ? "plateauing" : "not plateauing";
|
|
20
|
+
parts.push(`SKL vs ${ref}: ${plateau}`);
|
|
21
|
+
}
|
|
22
|
+
if (parts.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
return parts.join("; ");
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical figure taxonomy for barmesh_results_explorer dropdown sections.
|
|
3
|
+
*/
|
|
4
|
+
export const FIGURE_SECTION_ORDER = [
|
|
5
|
+
"overview",
|
|
6
|
+
"distances",
|
|
7
|
+
"diagnostics",
|
|
8
|
+
"component",
|
|
9
|
+
];
|
|
10
|
+
export const FIGURE_SECTION_LABELS = {
|
|
11
|
+
overview: "Overview",
|
|
12
|
+
distances: "Distances",
|
|
13
|
+
diagnostics: "Diagnostics",
|
|
14
|
+
component: "Component planes",
|
|
15
|
+
};
|
|
16
|
+
/** Map a figure base key into one of four explorer sections. */
|
|
17
|
+
export function figureSection(key) {
|
|
18
|
+
if (key === "combined")
|
|
19
|
+
return "overview";
|
|
20
|
+
if (key === "overview_distances" ||
|
|
21
|
+
key.startsWith("KL_") ||
|
|
22
|
+
key.startsWith("EMD_")) {
|
|
23
|
+
return "distances";
|
|
24
|
+
}
|
|
25
|
+
if (key === "learning_curve" || key.startsWith("plot_vol"))
|
|
26
|
+
return "diagnostics";
|
|
27
|
+
if (key.startsWith("plot_"))
|
|
28
|
+
return "component";
|
|
29
|
+
return "diagnostics";
|
|
30
|
+
}
|
|
31
|
+
export const FIGURE_SORT_RANK = {
|
|
32
|
+
combined: 0,
|
|
33
|
+
overview_distances: 1,
|
|
34
|
+
KL_ref: 10,
|
|
35
|
+
KL_stepwise: 11,
|
|
36
|
+
EMD_ref: 12,
|
|
37
|
+
EMD_stepwise: 13,
|
|
38
|
+
learning_curve: 14,
|
|
39
|
+
plot_vol_all_meshes: 19,
|
|
40
|
+
plot_vol_coarse_fine: 20,
|
|
41
|
+
};
|
|
42
|
+
export function sortRank(key) {
|
|
43
|
+
if (key in FIGURE_SORT_RANK)
|
|
44
|
+
return FIGURE_SORT_RANK[key];
|
|
45
|
+
if (key.startsWith("plot_vol"))
|
|
46
|
+
return 25;
|
|
47
|
+
if (key.startsWith("plot_"))
|
|
48
|
+
return 30;
|
|
49
|
+
return 50;
|
|
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
|
|
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)});
|
package/dist/job_monitor.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { apiCall } from "./shared.js";
|
|
7
7
|
import { formatJobStatusText } from "./job_status_format.js";
|
|
8
8
|
import { pollCfdFinalizeIfPresent, refreshJobAfterFinalize } from "./cfd_finalize.js";
|
|
9
|
+
import { buildPostHocReview, isTerminalStatus } from "./training_review.js";
|
|
9
10
|
export const DEFAULT_BLOCK_UNTIL_SEC = 900;
|
|
10
11
|
export const DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
11
12
|
export const MIN_POLL_INTERVAL_SEC = 5;
|
|
@@ -156,10 +157,20 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
156
157
|
const start = Date.now();
|
|
157
158
|
const snapshots = [];
|
|
158
159
|
let lastSnap = null;
|
|
159
|
-
let data = {};
|
|
160
|
+
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
161
|
+
const initialStatus = String(data.status ?? "");
|
|
162
|
+
if (isTerminalStatus(initialStatus)) {
|
|
163
|
+
const suggested = suggestedNextStep(job_id, data);
|
|
164
|
+
return buildPostHocReview(job_id, data, suggested);
|
|
165
|
+
}
|
|
160
166
|
let heartbeat = 0;
|
|
161
167
|
while (Date.now() - start < blockMs) {
|
|
162
|
-
|
|
168
|
+
if (snapshots.length === 0) {
|
|
169
|
+
/* first fetch already done above */
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
173
|
+
}
|
|
163
174
|
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
164
175
|
const snap = snapshotFromJob(data, elapsedSec);
|
|
165
176
|
heartbeat += 1;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact metadata strip for barmesh_results_explorer header.
|
|
3
|
+
*/
|
|
4
|
+
function fmtNum(v, digits = 4) {
|
|
5
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
6
|
+
return v.toFixed(digits);
|
|
7
|
+
return v != null ? String(v) : "N/A";
|
|
8
|
+
}
|
|
9
|
+
function truncId(jobId) {
|
|
10
|
+
return jobId.length > 8 ? `${jobId.slice(0, 8)}…` : jobId;
|
|
11
|
+
}
|
|
12
|
+
function meshCellsSummary(summary) {
|
|
13
|
+
const meshes = summary.meshes ?? [];
|
|
14
|
+
if (meshes.length === 0)
|
|
15
|
+
return "N/A";
|
|
16
|
+
return meshes
|
|
17
|
+
.map((m) => `${String(m.mesh ?? "?")}:${m.n_cells ?? "?"}`)
|
|
18
|
+
.join(", ");
|
|
19
|
+
}
|
|
20
|
+
export function buildMetadataStrip(jobId, data, jobMeta) {
|
|
21
|
+
const summary = (data.summary ?? {});
|
|
22
|
+
const label = data.label != null && data.label !== ""
|
|
23
|
+
? String(data.label)
|
|
24
|
+
: jobMeta?.label != null
|
|
25
|
+
? String(jobMeta.label)
|
|
26
|
+
: null;
|
|
27
|
+
const jobType = String(summary.job_type ?? "cfd_mesh_convergence");
|
|
28
|
+
const datasetName = jobMeta?.dataset_name != null ? String(jobMeta.dataset_name) : null;
|
|
29
|
+
const grid = summary.grid ?? [];
|
|
30
|
+
const epochs = summary.epochs ?? [];
|
|
31
|
+
const epochStr = epochs.length >= 2 ? `[${epochs[0]},${epochs[1]}]` : epochs.length === 1 ? String(epochs[0]) : "N/A";
|
|
32
|
+
const line1 = [
|
|
33
|
+
label ? `label ${label}` : null,
|
|
34
|
+
`job ${truncId(jobId)}`,
|
|
35
|
+
jobType,
|
|
36
|
+
datasetName ? `dataset ${datasetName}` : null,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(" · ");
|
|
40
|
+
const nTrain = summary.n_train_total != null ? String(summary.n_train_total) : null;
|
|
41
|
+
const ref = summary.reference_mesh != null ? String(summary.reference_mesh) : null;
|
|
42
|
+
const meshOrder = Array.isArray(summary.mesh_order) && summary.mesh_order.length > 0
|
|
43
|
+
? summary.mesh_order.map(String).join("→")
|
|
44
|
+
: null;
|
|
45
|
+
const line2 = [
|
|
46
|
+
nTrain != null ? `n_train ${nTrain}` : null,
|
|
47
|
+
meshOrder ? `meshes ${meshOrder}` : `cells ${meshCellsSummary(summary)}`,
|
|
48
|
+
ref ? `ref ${ref}` : null,
|
|
49
|
+
]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(" · ");
|
|
52
|
+
const wall = jobMeta?.wall_elapsed_sec != null ? Math.round(Number(jobMeta.wall_elapsed_sec)) : null;
|
|
53
|
+
const kernel = jobMeta?.kernel_elapsed_sec != null
|
|
54
|
+
? Math.round(Number(jobMeta.kernel_elapsed_sec))
|
|
55
|
+
: jobMeta?.training_elapsed_sec != null
|
|
56
|
+
? Math.round(Number(jobMeta.training_elapsed_sec))
|
|
57
|
+
: null;
|
|
58
|
+
const line3 = [
|
|
59
|
+
`preset ${String(summary.preset ?? "generic")}`,
|
|
60
|
+
grid.length >= 2 ? `grid ${grid[0]}×${grid[1]}` : null,
|
|
61
|
+
`epochs ${epochStr}`,
|
|
62
|
+
summary.batch_size != null ? `batch ${summary.batch_size}` : null,
|
|
63
|
+
summary.stratify_scale != null ? `stratify ${summary.stratify_scale}` : null,
|
|
64
|
+
summary.backend != null ? `backend ${summary.backend}` : null,
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join(" · ");
|
|
68
|
+
const line4 = [
|
|
69
|
+
wall != null ? `wall ${wall}s` : null,
|
|
70
|
+
kernel != null ? `kernel ${kernel}s` : null,
|
|
71
|
+
summary.quantization_error != null ? `QE ${fmtNum(summary.quantization_error)}` : null,
|
|
72
|
+
summary.topographic_error != null ? `TE ${fmtNum(summary.topographic_error)}` : null,
|
|
73
|
+
summary.emd_method != null ? `emd ${summary.emd_method}` : null,
|
|
74
|
+
]
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.join(" · ");
|
|
77
|
+
return [line1, line2, line3, line4].filter((l) => l.length > 0);
|
|
78
|
+
}
|
package/dist/shared.js
CHANGED
|
@@ -22,7 +22,7 @@ export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ??
|
|
|
22
22
|
export const MAX_RETRIES = 2;
|
|
23
23
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
24
24
|
/** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
|
|
25
|
-
export const CLIENT_VERSION = "0.5.
|
|
25
|
+
export const CLIENT_VERSION = "0.5.4";
|
|
26
26
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
27
27
|
/** Large per-cell CSV uploads may exceed the default fetch timeout. */
|
|
28
28
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
|
@@ -389,7 +389,7 @@ export function getCaptionForImage(filename) {
|
|
|
389
389
|
if (base === "combined")
|
|
390
390
|
return "All feature component planes on the trained SOM in one grid — primary comparison artifact (barsom-style combined view).";
|
|
391
391
|
if (base === "overview_distances")
|
|
392
|
-
return "Diagnostic mosaic:
|
|
392
|
+
return "Diagnostic mosaic: KL and EMD vs reference and stepwise (all distance panels).";
|
|
393
393
|
if (base === "learning_curve")
|
|
394
394
|
return "Quantization error by epoch: ordering phase (steel blue) then convergence (coral).";
|
|
395
395
|
if (base === "KL_ref")
|
|
@@ -400,6 +400,8 @@ export function getCaptionForImage(filename) {
|
|
|
400
400
|
return "Wasserstein-1 (EMD) distance from each mesh to the reference. A monotone decrease toward the finest mesh indicates field-level convergence.";
|
|
401
401
|
if (base === "EMD_stepwise")
|
|
402
402
|
return "Stepwise EMD between consecutive meshes. Small, plateauing values on the finest pairs indicate the fingerprint has stopped changing.";
|
|
403
|
+
if (base === "plot_vol_all_meshes")
|
|
404
|
+
return "Volume-weighted SOM fingerprints P_vol for every mesh in mesh_order (shared color scale).";
|
|
403
405
|
if (base === "plot_vol_coarse_fine")
|
|
404
406
|
return "Volume-weighted SOM fingerprints P_vol for the coarsest and reference meshes (shared color scale).";
|
|
405
407
|
if (base.startsWith("plot_vol_steps") || base.startsWith("plot_vol_seven_steps"))
|
|
@@ -428,6 +430,12 @@ export async function loadViewHtml(viewName) {
|
|
|
428
430
|
}
|
|
429
431
|
return null;
|
|
430
432
|
}
|
|
433
|
+
export const MAX_INLINE_BYTES = parseInt(process.env.BARIVIA_MAX_INLINE_BYTES ?? "8000000", 10);
|
|
434
|
+
let _inlineAttachBytesUsed = 0;
|
|
435
|
+
/** Reset inline image byte budget (call at the start of each MCP tool response). */
|
|
436
|
+
export function resetInlineAttachBudget() {
|
|
437
|
+
_inlineAttachBytesUsed = 0;
|
|
438
|
+
}
|
|
431
439
|
export async function tryAttachImage(content, jobId, filename) {
|
|
432
440
|
if (filename.endsWith(".pdf") || filename.endsWith(".svg")) {
|
|
433
441
|
content.push({
|
|
@@ -441,6 +449,14 @@ export async function tryAttachImage(content, jobId, filename) {
|
|
|
441
449
|
content.push({ type: "text", text: `${filename}: ${caption}` });
|
|
442
450
|
try {
|
|
443
451
|
const { data: imgBuf } = await apiRawCall(`/v1/results/${jobId}/image/${filename}`);
|
|
452
|
+
if (_inlineAttachBytesUsed + imgBuf.length > MAX_INLINE_BYTES) {
|
|
453
|
+
content.push({
|
|
454
|
+
type: "text",
|
|
455
|
+
text: `(${filename} omitted — inline image budget exceeded; use barmesh_results(action=image, job_id="${jobId}", filename="${filename}") or action=download)`,
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
_inlineAttachBytesUsed += imgBuf.length;
|
|
444
460
|
content.push({
|
|
445
461
|
type: "image",
|
|
446
462
|
data: imgBuf.toString("base64"),
|
|
@@ -2,28 +2,14 @@ import { z } from "zod";
|
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
4
4
|
import { apiCall, apiRawCall, getClientSupportsMcpApps, getVizPort, getCaptionForImage, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
|
|
5
|
+
import { FIGURE_SECTION_LABELS, FIGURE_SECTION_ORDER, figureSection, sortRank, } from "../figure_sections.js";
|
|
6
|
+
import { buildMetadataStrip } from "../results_metadata.js";
|
|
5
7
|
export const RESULTS_EXPLORER_URI = "ui://barmesh/results-explorer";
|
|
6
|
-
const FIGURE_SORT_RANK = {
|
|
7
|
-
combined: 0,
|
|
8
|
-
overview_distances: 1,
|
|
9
|
-
KL_ref: 10,
|
|
10
|
-
KL_stepwise: 11,
|
|
11
|
-
EMD_ref: 12,
|
|
12
|
-
EMD_stepwise: 13,
|
|
13
|
-
learning_curve: 14,
|
|
14
|
-
plot_vol_coarse_fine: 20,
|
|
15
|
-
};
|
|
16
|
-
function sortRank(key) {
|
|
17
|
-
if (key in FIGURE_SORT_RANK)
|
|
18
|
-
return FIGURE_SORT_RANK[key];
|
|
19
|
-
if (key.startsWith("plot_vol"))
|
|
20
|
-
return 25;
|
|
21
|
-
if (key.startsWith("plot_"))
|
|
22
|
-
return 30;
|
|
23
|
-
return 50;
|
|
24
|
-
}
|
|
25
8
|
export function sortFigures(figures) {
|
|
26
|
-
return [...figures].sort((a, b) =>
|
|
9
|
+
return [...figures].sort((a, b) => FIGURE_SECTION_ORDER.indexOf(a.section ?? "diagnostics") -
|
|
10
|
+
FIGURE_SECTION_ORDER.indexOf(b.section ?? "diagnostics") ||
|
|
11
|
+
sortRank(a.key) - sortRank(b.key) ||
|
|
12
|
+
a.label.localeCompare(b.label));
|
|
27
13
|
}
|
|
28
14
|
export function resolveDefaultFigureKey(figures) {
|
|
29
15
|
const byKey = (k) => figures.find((f) => f.key === k);
|
|
@@ -52,6 +38,8 @@ function labelForFigure(filename) {
|
|
|
52
38
|
return "Wasserstein (EMD) vs reference";
|
|
53
39
|
if (base === "EMD_stepwise")
|
|
54
40
|
return "Wasserstein (EMD) stepwise";
|
|
41
|
+
if (base === "plot_vol_all_meshes")
|
|
42
|
+
return "Volume fingerprint: all meshes";
|
|
55
43
|
if (base === "plot_vol_coarse_fine")
|
|
56
44
|
return "Volume fingerprint: coarse vs reference";
|
|
57
45
|
if (base.startsWith("plot_vol_"))
|
|
@@ -60,45 +48,23 @@ function labelForFigure(filename) {
|
|
|
60
48
|
return `Component plane: ${base.replace(/^plot_/, "").replace(/_/g, " ")}`;
|
|
61
49
|
return base.replace(/_/g, " ");
|
|
62
50
|
}
|
|
63
|
-
function figureKind(base) {
|
|
64
|
-
if (base === "combined")
|
|
65
|
-
return "summary";
|
|
66
|
-
if (base.startsWith("plot_") && !base.startsWith("plot_vol_"))
|
|
67
|
-
return "component";
|
|
68
|
-
return "diagnostic";
|
|
69
|
-
}
|
|
70
51
|
function buildMetrics(summary) {
|
|
71
52
|
const grid = summary.grid ?? [];
|
|
72
53
|
const qe = summary.quantization_error != null ? Number(summary.quantization_error).toFixed(4) : "N/A";
|
|
73
54
|
const te = summary.topographic_error != null ? Number(summary.topographic_error).toFixed(4) : "N/A";
|
|
74
55
|
const meshes = summary.meshes ?? [];
|
|
56
|
+
const epochs = summary.epochs ?? [];
|
|
75
57
|
return [
|
|
76
58
|
{ label: "Grid", value: grid.length >= 2 ? `${grid[0]}×${grid[1]}` : "N/A" },
|
|
77
59
|
{ label: "Preset", value: String(summary.preset ?? "generic") },
|
|
78
60
|
{ label: "Reference", value: String(summary.reference_mesh ?? "N/A") },
|
|
79
61
|
{ label: "Meshes", value: String(meshes.length || "N/A") },
|
|
62
|
+
{ label: "Epochs", value: epochs.length >= 2 ? `${epochs[0]}+${epochs[1]}` : "N/A" },
|
|
80
63
|
{ label: "QE", value: qe },
|
|
81
64
|
{ label: "TE", value: te },
|
|
82
65
|
{ label: "EMD method", value: String(summary.emd_method ?? "exact") },
|
|
83
66
|
];
|
|
84
67
|
}
|
|
85
|
-
function buildHighlights(summary) {
|
|
86
|
-
const out = [];
|
|
87
|
-
const convergence = summary.convergence;
|
|
88
|
-
const emd = convergence?.emd;
|
|
89
|
-
const skl = convergence?.skl;
|
|
90
|
-
if (emd) {
|
|
91
|
-
const plateau = emd.plateau === true ? "plateauing" : "not yet plateauing";
|
|
92
|
-
const mono = emd.monotonic_decrease === true ? "monotonic decrease" : "non-monotonic";
|
|
93
|
-
out.push(`EMD vs reference: ${mono}, ${plateau}.`);
|
|
94
|
-
}
|
|
95
|
-
if (skl) {
|
|
96
|
-
const plateau = skl.plateau === true ? "plateauing" : "not yet plateauing";
|
|
97
|
-
out.push(`Symmetric KL vs reference: ${plateau}.`);
|
|
98
|
-
}
|
|
99
|
-
out.push("Advisory: SOM distances complement, not replace, numerical uncertainty analysis (Richardson/GCI).");
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
68
|
/**
|
|
103
69
|
* Group `<base>.<ext>` figure files into one logical figure per base, collecting the
|
|
104
70
|
* available formats. Preview is the inline-displayable raster (PNG, else SVG); the
|
|
@@ -135,12 +101,12 @@ export function groupFigures(files) {
|
|
|
135
101
|
filename: preview,
|
|
136
102
|
downloadFilename,
|
|
137
103
|
formats,
|
|
138
|
-
|
|
104
|
+
section: figureSection(base),
|
|
139
105
|
caption: [baseCaption, vectorNote, emptyNote].filter(Boolean).join(" ") || undefined,
|
|
140
106
|
};
|
|
141
107
|
});
|
|
142
108
|
}
|
|
143
|
-
export function buildPayload(jobId, data) {
|
|
109
|
+
export function buildPayload(jobId, data, jobMeta) {
|
|
144
110
|
const summary = (data.summary ?? {});
|
|
145
111
|
const files = (summary.files ?? []).filter((f) => /\.(png|pdf|svg)$/i.test(f));
|
|
146
112
|
const figures = sortFigures(groupFigures(files));
|
|
@@ -150,8 +116,8 @@ export function buildPayload(jobId, data) {
|
|
|
150
116
|
jobId,
|
|
151
117
|
title: "Mesh Convergence Results",
|
|
152
118
|
subtitle: String(data.label ?? ""),
|
|
119
|
+
metadataStrip: buildMetadataStrip(jobId, data, jobMeta),
|
|
153
120
|
metrics: buildMetrics(summary),
|
|
154
|
-
highlights: buildHighlights(summary),
|
|
155
121
|
availableFigures: figures,
|
|
156
122
|
defaultFigureKey: resolveDefaultFigureKey(figures),
|
|
157
123
|
standaloneUrl: port
|
|
@@ -160,15 +126,18 @@ export function buildPayload(jobId, data) {
|
|
|
160
126
|
};
|
|
161
127
|
}
|
|
162
128
|
async function handleResultsExplorer(job_id) {
|
|
163
|
-
const data =
|
|
164
|
-
|
|
129
|
+
const [data, jobMeta] = await Promise.all([
|
|
130
|
+
apiCall("GET", `/v1/results/${job_id}`),
|
|
131
|
+
apiCall("GET", `/v1/jobs/${job_id}`).catch(() => ({})),
|
|
132
|
+
]);
|
|
133
|
+
const payload = buildPayload(job_id, data, jobMeta);
|
|
165
134
|
const content = [
|
|
166
135
|
{
|
|
167
136
|
type: "text",
|
|
168
137
|
text: [
|
|
169
138
|
`Mesh convergence results explorer ready for job ${job_id}.`,
|
|
170
139
|
payload.subtitle ? `Label: ${payload.subtitle}` : "",
|
|
171
|
-
payload.
|
|
140
|
+
payload.metadataStrip?.[0] ?? "",
|
|
172
141
|
]
|
|
173
142
|
.filter(Boolean)
|
|
174
143
|
.join("\n"),
|
|
@@ -178,6 +147,14 @@ async function handleResultsExplorer(job_id) {
|
|
|
178
147
|
if (defaultFigure && /\.(png|svg|jpe?g|webp)$/i.test(defaultFigure.filename)) {
|
|
179
148
|
await tryAttachImage(content, job_id, defaultFigure.filename);
|
|
180
149
|
}
|
|
150
|
+
if (payload.standaloneUrl) {
|
|
151
|
+
content.push({
|
|
152
|
+
type: "text",
|
|
153
|
+
text: `Standalone results explorer URL (copy if Open is blocked):\n${payload.standaloneUrl}\n` +
|
|
154
|
+
`AGENT: surface this URL to the user — markdown link or copy/paste into a browser when the inline MCP panel cannot open localhost. ` +
|
|
155
|
+
`This localhost port is per MCP session and goes stale if the proxy restarts — re-run barmesh_results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
181
158
|
if (!getClientSupportsMcpApps() && payload.standaloneUrl) {
|
|
182
159
|
content.push({
|
|
183
160
|
type: "text",
|
|
@@ -191,7 +168,7 @@ async function handleResultsExplorer(job_id) {
|
|
|
191
168
|
export function registerResultsExplorerTool(server) {
|
|
192
169
|
const toolConfig = {
|
|
193
170
|
title: "Mesh Convergence Results Explorer",
|
|
194
|
-
description: "PREFERRED way to browse a completed mesh_convergence job after a first barmesh_results(get) glance. Interactive explorer with a figure dropdown
|
|
171
|
+
description: "PREFERRED way to browse a completed mesh_convergence job after a first barmesh_results(get) glance. Interactive explorer with a grouped figure dropdown (Overview → Distances → Diagnostics → Component planes): combined mosaic headline, overview_distances, KL/EMD panels, learning curve, volume fingerprints, per-feature planes. Header shows job/dataset/training metadata from status + summary.json. Convergence reading lives in barmesh_results(get) / summary.convergence only — not repeated here. Each figure shows its PNG preview inline (PDFs cannot render in the sandboxed panel) and offers vector download when rendered. Embeds as an MCP App or falls back to a standalone localhost page.",
|
|
195
172
|
inputSchema: {
|
|
196
173
|
job_id: z.string().describe("Job ID of a completed cfd_mesh_convergence job"),
|
|
197
174
|
},
|
|
@@ -216,3 +193,4 @@ export function registerResultsExplorerTool(server) {
|
|
|
216
193
|
}
|
|
217
194
|
});
|
|
218
195
|
}
|
|
196
|
+
export { FIGURE_SECTION_LABELS, FIGURE_SECTION_ORDER, figureSection };
|
package/dist/tools/guide.js
CHANGED
|
@@ -2,7 +2,7 @@ import { registerAuditedTool } from "../audit.js";
|
|
|
2
2
|
import { apiCall, textResult } from "../shared.js";
|
|
3
3
|
const OFFLINE_GUIDE = `barmesh: CFD mesh-convergence on the Barivia API.
|
|
4
4
|
Two tracks: barmesh_mesh_convergence (SOM fingerprint distances) and barmesh_richardson (classical GCI).
|
|
5
|
-
Workflow: barmesh_prepare_mesh_data -> barmesh_datasets(upload) -> barmesh_mesh_convergence / barmesh_richardson -> barmesh_training_monitor (or barmesh_jobs
|
|
5
|
+
Workflow: barmesh_prepare_mesh_data -> barmesh_datasets(upload) -> barmesh_mesh_convergence / barmesh_richardson -> barmesh_training_monitor (default visual MCP App; post-hoc review when already done) or barmesh_jobs(monitor/status) headless fallback -> barmesh_results(get) -> barmesh_results_explorer for figures.
|
|
6
6
|
(API unreachable; this is the offline summary. Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
|
|
7
7
|
const OFFLINE_PREP = `barmesh mesh-data prep (offline summary; API unreachable):
|
|
8
8
|
Build ONE combined per-cell CSV across all meshes of the refinement study:
|
package/dist/tools/jobs.js
CHANGED
|
@@ -4,12 +4,13 @@ import { apiCall, textResult } from "../shared.js";
|
|
|
4
4
|
import { pollCfdFinalizeIfPresent, refreshJobAfterFinalize } from "../cfd_finalize.js";
|
|
5
5
|
import { formatJobStatusText } from "../job_status_format.js";
|
|
6
6
|
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC } from "../job_monitor.js";
|
|
7
|
-
import {
|
|
7
|
+
import { runBlockingMonitor } from "../blocking_monitor.js";
|
|
8
8
|
export function registerJobsTool(server) {
|
|
9
9
|
registerAuditedTool(server, "barmesh_jobs", `Check job status, block until terminal, or list jobs.
|
|
10
10
|
|
|
11
11
|
BEST FOR: action=monitor after submit (one call, throttled snapshots — preferred for agents). action=status for a single one-shot check.
|
|
12
|
-
|
|
12
|
+
MONITOR MODES: barmesh_training_monitor — default visual MCP App (live curves, post-hoc review on completed jobs). barmesh_jobs(action=monitor) — headless blocking snapshots for agents (live or post-hoc review; attaches learning_curve.png when already completed).
|
|
13
|
+
ASYNC PROTOCOL: monitor blocks server-side until completed/failed or block_until timeout (default ${DEFAULT_BLOCK_UNTIL_SEC}s, poll every ${DEFAULT_POLL_INTERVAL_SEC}s). status is one-shot; poll every 10-20s manually if not using monitor. When status=completed, call barmesh_results(action=get, job_id=...) then barmesh_results_explorer(job_id=...).
|
|
13
14
|
ESCALATION: status=failed returns an error message and (when available) a failure_stage; read it before retrying.`, {
|
|
14
15
|
action: z
|
|
15
16
|
.enum(["status", "monitor", "list"])
|
|
@@ -36,7 +37,7 @@ ESCALATION: status=failed returns an error message and (when available) a failur
|
|
|
36
37
|
if (action === "monitor") {
|
|
37
38
|
if (!job_id)
|
|
38
39
|
throw new Error("barmesh_jobs(monitor) requires job_id.");
|
|
39
|
-
return
|
|
40
|
+
return runBlockingMonitor({ job_id, block_until_sec, poll_interval_sec, wait_finalize });
|
|
40
41
|
}
|
|
41
42
|
if (action === "status") {
|
|
42
43
|
if (!job_id)
|
package/dist/tools/results.js
CHANGED
|
@@ -2,8 +2,36 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerAuditedTool } from "../audit.js";
|
|
5
|
-
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
|
|
6
|
-
|
|
5
|
+
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
|
|
6
|
+
import { formatConvergenceReading } from "../convergence_reading.js";
|
|
7
|
+
const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "plot_vol_all_meshes", "plot_vol_steps", "learning_curve"];
|
|
8
|
+
function formatMeshResultsSummary(jobId, data, summary) {
|
|
9
|
+
const label = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
10
|
+
const header = label ? `Mesh convergence results for ${label} (job_id: ${jobId})` : `Mesh convergence results for job_id: ${jobId}`;
|
|
11
|
+
const grid = summary.grid ?? [];
|
|
12
|
+
const meshes = summary.meshes ?? [];
|
|
13
|
+
const stepwise = summary.stepwise ?? [];
|
|
14
|
+
const fmt = (v) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(4) : String(v ?? "N/A"));
|
|
15
|
+
const meshLines = meshes.map((m) => ` ${String(m.mesh ?? "?")} (n=${m.n_cells ?? "?"}): SKL→ref=${fmt(m.skl_to_ref)} EMD→ref=${fmt(m.emd_to_ref)}`);
|
|
16
|
+
const stepLines = stepwise.map((s) => ` ${String(s.pair ?? "?")}: EMD=${fmt(s.emd)} KL↓=${fmt(s.kl_coarsen)} KL↑=${fmt(s.kl_refine)}`);
|
|
17
|
+
const readingLine = formatConvergenceReading(summary);
|
|
18
|
+
const ord = summary.ordering_errors;
|
|
19
|
+
const conv = summary.convergence_errors;
|
|
20
|
+
const curveNote = (ord?.length ?? 0) > 0 || (conv?.length ?? 0) > 0
|
|
21
|
+
? `Training curves: ${ord?.length ?? 0} ordering + ${conv?.length ?? 0} convergence batch samples (≤1000 each in API/MCP responses).`
|
|
22
|
+
: "";
|
|
23
|
+
return [
|
|
24
|
+
header,
|
|
25
|
+
`Preset: ${String(summary.preset ?? "generic")} | Grid: ${grid.length >= 2 ? `${grid[0]}×${grid[1]}` : "N/A"} | Reference: ${String(summary.reference_mesh ?? "N/A")}`,
|
|
26
|
+
`QE: ${fmt(summary.quantization_error)} | TE: ${fmt(summary.topographic_error)} | EMD method: ${String(summary.emd_method ?? "exact")}`,
|
|
27
|
+
meshes.length > 0 ? `\nDistances vs reference:\n${meshLines.join("\n")}` : "",
|
|
28
|
+
stepLines.length > 0 ? `\nStepwise:\n${stepLines.join("\n")}` : "",
|
|
29
|
+
readingLine ? `\nConvergence: ${readingLine}` : "",
|
|
30
|
+
curveNote,
|
|
31
|
+
"\nAdvisory: SOM distances complement, not replace, numerical uncertainty analysis (use barmesh_richardson for classical GCI).",
|
|
32
|
+
"For every figure: barmesh_results_explorer(job_id) or barmesh_results(action=download). Use figures=\"none\" to skip inline images.",
|
|
33
|
+
].filter(Boolean).join("\n");
|
|
34
|
+
}
|
|
7
35
|
const TEXT_ARTIFACTS = [
|
|
8
36
|
"summary.json",
|
|
9
37
|
"distances.csv",
|
|
@@ -23,7 +51,7 @@ export function registerResultsTool(server) {
|
|
|
23
51
|
| download | Save figures and metrics to a local folder (headless / agent path). |
|
|
24
52
|
|
|
25
53
|
BEST FOR: After barmesh_jobs(action=status) shows completed (and finalize finished if defer_figures was used).
|
|
26
|
-
FIGURES: Default
|
|
54
|
+
FIGURES: Default bundle is combined.png, overview_distances.png (all KL/EMD panels), plot_vol_all_meshes.png, plot_vol_steps.png, and learning_curve.png — not redundant per-feature PNGs. action=get inlines the headline set; pass figures="all" for every artifact listed in summary.files, figures="none" for metrics only (recommended for agents).
|
|
27
55
|
PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image or action=download.
|
|
28
56
|
If GET /v1/results returns 404 shortly after status=completed, cfd_finalize may still be rendering — poll barmesh_jobs(status) until finalize_job_id completes.
|
|
29
57
|
NOT FOR: Submitting jobs.`, {
|
|
@@ -96,9 +124,18 @@ NOT FOR: Submitting jobs.`, {
|
|
|
96
124
|
const files = summary.files ?? [];
|
|
97
125
|
const isImage = (f) => /\.(png|svg|pdf)$/i.test(f);
|
|
98
126
|
let toDownload;
|
|
99
|
-
if (figures === "all"
|
|
127
|
+
if (figures === "all") {
|
|
100
128
|
toDownload = include_json ? files : files.filter(isImage);
|
|
101
129
|
}
|
|
130
|
+
else if (figures === undefined) {
|
|
131
|
+
toDownload = MESH_DEFAULT_FIGURES.flatMap((b) => files.filter((f) => f.replace(/\.[^.]+$/, "") === b || f.startsWith(`${b}.`)));
|
|
132
|
+
if (include_json) {
|
|
133
|
+
for (const t of TEXT_ARTIFACTS) {
|
|
134
|
+
if (files.includes(t) && !toDownload.includes(t))
|
|
135
|
+
toDownload.push(t);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
102
139
|
else if (Array.isArray(figures)) {
|
|
103
140
|
toDownload = figures.flatMap((key) => {
|
|
104
141
|
if (/\.(png|pdf|svg|csv|txt|json)$/i.test(key))
|
|
@@ -139,9 +176,10 @@ NOT FOR: Submitting jobs.`, {
|
|
|
139
176
|
? `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}`
|
|
140
177
|
: `No files saved. Check job_id and that the job (and cfd_finalize, if any) is completed.`);
|
|
141
178
|
}
|
|
142
|
-
|
|
179
|
+
resetInlineAttachBudget();
|
|
180
|
+
let data;
|
|
143
181
|
try {
|
|
144
|
-
|
|
182
|
+
data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
145
183
|
}
|
|
146
184
|
catch (err) {
|
|
147
185
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -154,7 +192,8 @@ NOT FOR: Submitting jobs.`, {
|
|
|
154
192
|
}
|
|
155
193
|
throw err;
|
|
156
194
|
}
|
|
157
|
-
const
|
|
195
|
+
const summary = (data.summary ?? {});
|
|
196
|
+
const content = [{ type: "text", text: formatMeshResultsSummary(job_id, data, summary) }];
|
|
158
197
|
if (figures === "none")
|
|
159
198
|
return { content };
|
|
160
199
|
const allFiles = summary.files ?? [];
|