@barivia/barmesh-mcp 0.5.4 → 0.6.1
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/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 +14 -3
- package/dist/results_metadata.js +78 -0
- package/dist/shared.js +1 -1
- package/dist/tools/barmesh_results_explorer.js +31 -53
- package/dist/tools/cfd.js +1 -0
- package/dist/tools/guide.js +1 -1
- package/dist/tools/jobs.js +4 -3
- package/dist/tools/results.js +3 -13
- package/dist/tools/training_monitor.js +75 -52
- package/dist/training_monitor_curve.js +60 -0
- package/dist/training_review.js +222 -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
|
@@ -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;
|
|
@@ -91,7 +92,7 @@ export function formatSnapshotLine(s) {
|
|
|
91
92
|
if (s.qe != null)
|
|
92
93
|
parts.push(`QE ${s.qe.toFixed(4)}`);
|
|
93
94
|
if (s.te != null)
|
|
94
|
-
parts.push(`TE ${s.te.toFixed(4)}`);
|
|
95
|
+
parts.push(`Epoch TE ${s.te.toFixed(4)}`);
|
|
95
96
|
if (s.eta_sec != null)
|
|
96
97
|
parts.push(`ETA ~${s.eta_sec}s`);
|
|
97
98
|
if (s.ordering_errors_tail?.length) {
|
|
@@ -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.
|
|
25
|
+
export const CLIENT_VERSION = "0.6.1";
|
|
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;
|
|
@@ -2,29 +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_all_meshes: 19,
|
|
15
|
-
plot_vol_coarse_fine: 20,
|
|
16
|
-
};
|
|
17
|
-
function sortRank(key) {
|
|
18
|
-
if (key in FIGURE_SORT_RANK)
|
|
19
|
-
return FIGURE_SORT_RANK[key];
|
|
20
|
-
if (key.startsWith("plot_vol"))
|
|
21
|
-
return 25;
|
|
22
|
-
if (key.startsWith("plot_"))
|
|
23
|
-
return 30;
|
|
24
|
-
return 50;
|
|
25
|
-
}
|
|
26
8
|
export function sortFigures(figures) {
|
|
27
|
-
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));
|
|
28
13
|
}
|
|
29
14
|
export function resolveDefaultFigureKey(figures) {
|
|
30
15
|
const byKey = (k) => figures.find((f) => f.key === k);
|
|
@@ -63,45 +48,23 @@ function labelForFigure(filename) {
|
|
|
63
48
|
return `Component plane: ${base.replace(/^plot_/, "").replace(/_/g, " ")}`;
|
|
64
49
|
return base.replace(/_/g, " ");
|
|
65
50
|
}
|
|
66
|
-
function figureKind(base) {
|
|
67
|
-
if (base === "combined")
|
|
68
|
-
return "summary";
|
|
69
|
-
if (base.startsWith("plot_") && !base.startsWith("plot_vol_"))
|
|
70
|
-
return "component";
|
|
71
|
-
return "diagnostic";
|
|
72
|
-
}
|
|
73
51
|
function buildMetrics(summary) {
|
|
74
52
|
const grid = summary.grid ?? [];
|
|
75
53
|
const qe = summary.quantization_error != null ? Number(summary.quantization_error).toFixed(4) : "N/A";
|
|
76
54
|
const te = summary.topographic_error != null ? Number(summary.topographic_error).toFixed(4) : "N/A";
|
|
77
55
|
const meshes = summary.meshes ?? [];
|
|
56
|
+
const epochs = summary.epochs ?? [];
|
|
78
57
|
return [
|
|
79
58
|
{ label: "Grid", value: grid.length >= 2 ? `${grid[0]}×${grid[1]}` : "N/A" },
|
|
80
59
|
{ label: "Preset", value: String(summary.preset ?? "generic") },
|
|
81
60
|
{ label: "Reference", value: String(summary.reference_mesh ?? "N/A") },
|
|
82
61
|
{ label: "Meshes", value: String(meshes.length || "N/A") },
|
|
62
|
+
{ label: "Epochs", value: epochs.length >= 2 ? `${epochs[0]}+${epochs[1]}` : "N/A" },
|
|
83
63
|
{ label: "QE", value: qe },
|
|
84
64
|
{ label: "TE", value: te },
|
|
85
65
|
{ label: "EMD method", value: String(summary.emd_method ?? "exact") },
|
|
86
66
|
];
|
|
87
67
|
}
|
|
88
|
-
function buildHighlights(summary) {
|
|
89
|
-
const out = [];
|
|
90
|
-
const convergence = summary.convergence;
|
|
91
|
-
const emd = convergence?.emd;
|
|
92
|
-
const skl = convergence?.skl;
|
|
93
|
-
if (emd) {
|
|
94
|
-
const plateau = emd.plateau === true ? "plateauing" : "not yet plateauing";
|
|
95
|
-
const mono = emd.monotonic_decrease === true ? "monotonic decrease" : "non-monotonic";
|
|
96
|
-
out.push(`EMD vs reference: ${mono}, ${plateau}.`);
|
|
97
|
-
}
|
|
98
|
-
if (skl) {
|
|
99
|
-
const plateau = skl.plateau === true ? "plateauing" : "not yet plateauing";
|
|
100
|
-
out.push(`Symmetric KL vs reference: ${plateau}.`);
|
|
101
|
-
}
|
|
102
|
-
out.push("Advisory: SOM distances complement, not replace, numerical uncertainty analysis (Richardson/GCI).");
|
|
103
|
-
return out;
|
|
104
|
-
}
|
|
105
68
|
/**
|
|
106
69
|
* Group `<base>.<ext>` figure files into one logical figure per base, collecting the
|
|
107
70
|
* available formats. Preview is the inline-displayable raster (PNG, else SVG); the
|
|
@@ -138,12 +101,12 @@ export function groupFigures(files) {
|
|
|
138
101
|
filename: preview,
|
|
139
102
|
downloadFilename,
|
|
140
103
|
formats,
|
|
141
|
-
|
|
104
|
+
section: figureSection(base),
|
|
142
105
|
caption: [baseCaption, vectorNote, emptyNote].filter(Boolean).join(" ") || undefined,
|
|
143
106
|
};
|
|
144
107
|
});
|
|
145
108
|
}
|
|
146
|
-
export function buildPayload(jobId, data) {
|
|
109
|
+
export function buildPayload(jobId, data, jobMeta) {
|
|
147
110
|
const summary = (data.summary ?? {});
|
|
148
111
|
const files = (summary.files ?? []).filter((f) => /\.(png|pdf|svg)$/i.test(f));
|
|
149
112
|
const figures = sortFigures(groupFigures(files));
|
|
@@ -153,8 +116,8 @@ export function buildPayload(jobId, data) {
|
|
|
153
116
|
jobId,
|
|
154
117
|
title: "Mesh Convergence Results",
|
|
155
118
|
subtitle: String(data.label ?? ""),
|
|
119
|
+
metadataStrip: buildMetadataStrip(jobId, data, jobMeta),
|
|
156
120
|
metrics: buildMetrics(summary),
|
|
157
|
-
highlights: buildHighlights(summary),
|
|
158
121
|
availableFigures: figures,
|
|
159
122
|
defaultFigureKey: resolveDefaultFigureKey(figures),
|
|
160
123
|
standaloneUrl: port
|
|
@@ -163,15 +126,18 @@ export function buildPayload(jobId, data) {
|
|
|
163
126
|
};
|
|
164
127
|
}
|
|
165
128
|
async function handleResultsExplorer(job_id) {
|
|
166
|
-
const data =
|
|
167
|
-
|
|
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);
|
|
168
134
|
const content = [
|
|
169
135
|
{
|
|
170
136
|
type: "text",
|
|
171
137
|
text: [
|
|
172
138
|
`Mesh convergence results explorer ready for job ${job_id}.`,
|
|
173
139
|
payload.subtitle ? `Label: ${payload.subtitle}` : "",
|
|
174
|
-
payload.
|
|
140
|
+
payload.metadataStrip?.[0] ?? "",
|
|
175
141
|
]
|
|
176
142
|
.filter(Boolean)
|
|
177
143
|
.join("\n"),
|
|
@@ -181,6 +147,14 @@ async function handleResultsExplorer(job_id) {
|
|
|
181
147
|
if (defaultFigure && /\.(png|svg|jpe?g|webp)$/i.test(defaultFigure.filename)) {
|
|
182
148
|
await tryAttachImage(content, job_id, defaultFigure.filename);
|
|
183
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
|
+
}
|
|
184
158
|
if (!getClientSupportsMcpApps() && payload.standaloneUrl) {
|
|
185
159
|
content.push({
|
|
186
160
|
type: "text",
|
|
@@ -189,12 +163,15 @@ async function handleResultsExplorer(job_id) {
|
|
|
189
163
|
`This localhost port is assigned per MCP session and changes if the proxy restarts — re-run barmesh_results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.`,
|
|
190
164
|
});
|
|
191
165
|
}
|
|
192
|
-
return
|
|
166
|
+
return {
|
|
167
|
+
...structuredTextResult(payload, undefined, content),
|
|
168
|
+
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
169
|
+
};
|
|
193
170
|
}
|
|
194
171
|
export function registerResultsExplorerTool(server) {
|
|
195
172
|
const toolConfig = {
|
|
196
173
|
title: "Mesh Convergence Results Explorer",
|
|
197
|
-
description: "PREFERRED way to browse a completed mesh_convergence job after a first barmesh_results(get) glance. Interactive explorer with a figure dropdown
|
|
174
|
+
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.",
|
|
198
175
|
inputSchema: {
|
|
199
176
|
job_id: z.string().describe("Job ID of a completed cfd_mesh_convergence job"),
|
|
200
177
|
},
|
|
@@ -219,3 +196,4 @@ export function registerResultsExplorerTool(server) {
|
|
|
219
196
|
}
|
|
220
197
|
});
|
|
221
198
|
}
|
|
199
|
+
export { FIGURE_SECTION_LABELS, FIGURE_SECTION_ORDER, figureSection };
|
package/dist/tools/cfd.js
CHANGED
|
@@ -25,6 +25,7 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
|
|
|
25
25
|
backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().describe("Compute backend (default auto / preset)"),
|
|
26
26
|
stratify_scale: z.number().optional().describe("[0,1] per-mesh training-row cap; 1 uses all cells (default 1)"),
|
|
27
27
|
emd_method: z.enum(["exact", "sinkhorn"]).optional().describe("EMD solver: exact LP (default) or sinkhorn (fast approximation for large grids)"),
|
|
28
|
+
te_inner_samples: z.number().int().optional().describe("Inner statistical sample count for per-batch topographic error estimates during SOM training (default clamp(grid_nodes*6, 500, 10000); display cap remains ≤1000 batch points/phase)"),
|
|
28
29
|
component_planes_physical: z.boolean().optional().describe("Physical-scale component-plane colorbars (default true)"),
|
|
29
30
|
figures: z.boolean().optional().describe("Generate publication figures (default true)"),
|
|
30
31
|
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "none"])).optional().describe("Per-feature transform applied before normalization (e.g. log1p to compress k/epsilon/omega). Same preprocessing engine as barsom training."),
|
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
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerAuditedTool } from "../audit.js";
|
|
5
5
|
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
|
|
6
|
+
import { formatConvergenceReading } from "../convergence_reading.js";
|
|
6
7
|
const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "plot_vol_all_meshes", "plot_vol_steps", "learning_curve"];
|
|
7
8
|
function formatMeshResultsSummary(jobId, data, summary) {
|
|
8
9
|
const label = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
@@ -13,18 +14,7 @@ function formatMeshResultsSummary(jobId, data, summary) {
|
|
|
13
14
|
const fmt = (v) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(4) : String(v ?? "N/A"));
|
|
14
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)}`);
|
|
15
16
|
const stepLines = stepwise.map((s) => ` ${String(s.pair ?? "?")}: EMD=${fmt(s.emd)} KL↓=${fmt(s.kl_coarsen)} KL↑=${fmt(s.kl_refine)}`);
|
|
16
|
-
const
|
|
17
|
-
const emd = convergence?.emd;
|
|
18
|
-
const skl = convergence?.skl;
|
|
19
|
-
const reading = [];
|
|
20
|
-
if (emd) {
|
|
21
|
-
const plateau = emd.plateau === true ? "plateauing" : "not yet plateauing";
|
|
22
|
-
const mono = emd.monotonic_decrease === true ? "monotonic decrease toward reference" : "non-monotonic vs reference";
|
|
23
|
-
reading.push(`EMD vs ${String(summary.reference_mesh ?? "reference")}: ${mono}, ${plateau}.`);
|
|
24
|
-
}
|
|
25
|
-
if (skl) {
|
|
26
|
-
reading.push(`Symmetric KL vs reference: ${skl.plateau === true ? "plateauing" : "not yet plateauing"}.`);
|
|
27
|
-
}
|
|
17
|
+
const readingLine = formatConvergenceReading(summary);
|
|
28
18
|
const ord = summary.ordering_errors;
|
|
29
19
|
const conv = summary.convergence_errors;
|
|
30
20
|
const curveNote = (ord?.length ?? 0) > 0 || (conv?.length ?? 0) > 0
|
|
@@ -36,7 +26,7 @@ function formatMeshResultsSummary(jobId, data, summary) {
|
|
|
36
26
|
`QE: ${fmt(summary.quantization_error)} | TE: ${fmt(summary.topographic_error)} | EMD method: ${String(summary.emd_method ?? "exact")}`,
|
|
37
27
|
meshes.length > 0 ? `\nDistances vs reference:\n${meshLines.join("\n")}` : "",
|
|
38
28
|
stepLines.length > 0 ? `\nStepwise:\n${stepLines.join("\n")}` : "",
|
|
39
|
-
|
|
29
|
+
readingLine ? `\nConvergence: ${readingLine}` : "",
|
|
40
30
|
curveNote,
|
|
41
31
|
"\nAdvisory: SOM distances complement, not replace, numerical uncertainty analysis (use barmesh_richardson for classical GCI).",
|
|
42
32
|
"For every figure: barmesh_results_explorer(job_id) or barmesh_results(action=download). Use figures=\"none\" to skip inline images.",
|