@barivia/barsom-mcp 0.23.0 → 0.23.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
@@ -54,7 +54,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
54
54
  | `BARIVIA_API_URL` | No | `https://api.barivia.se` | API base URL |
55
55
  | `BARIVIA_WORKSPACE_ROOT` | No | `process.cwd()` or `PWD` | Directory for relative `file_path` and `save_to_disk`. In Cursor MCP, `process.cwd()` is often the MCP install dir — add `BARIVIA_WORKSPACE_ROOT` to your MCP config `env` with your project path (e.g. `/home/user/myproject`). Absolute paths and `file://` URIs work without it. |
56
56
  | `BARIVIA_FETCH_TIMEOUT_MS` | No | `60000` | Per-request HTTP timeout (ms) to the API. Increase (e.g. `120000`) on slow networks or when the API does long-running work. Large dataset uploads use a separate longer timeout internally, and `datasets(analyze)` runs asynchronously (auto-polled) so it is not bound by this. A timeout is not auto-retried (re-firing slow work would only multiply load / risk duplicates). |
57
- | `BARIVIA_VIZ_PORT` | No | OS-assigned | When the client has no MCP Apps support, the proxy may start a local viz server on `127.0.0.1`. The port is **assigned per MCP session and changes when the proxy restarts**, so standalone `…/viz/…` URLs from a previous session go stale (the page then can't reach `/api/*` and shows a "stale session — re-run the tool" message). Set a fixed port for stable bookmark URLs. Diagnostic: `GET http://127.0.0.1:<port>/api/health?job_id=<id>` reports the port, API reachability, and the job's status. |
57
+ | `BARIVIA_VIZ_PORT` | No | OS-assigned | Local viz server on `127.0.0.1`. Port is **per MCP session** unless pinned standalone `…/viz/…` URLs go stale after proxy restart. Set a fixed port for stable bookmark URLs. Diagnostic: `GET http://127.0.0.1:<port>/api/health?job_id=<id>`. |
58
+ | `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline`. Tool responses always lead with a standalone localhost URL (hosts may list `ui://` without mounting widgets) and include a structured `ui_delivery` block. `account(status)` reports detected Apps support, viz port, and active tier. |
58
59
  | `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | No | `1` (enabled) | File uploads are constrained to the MCP workspace root by default. Set to `0` or `false` to allow absolute paths anywhere on the machine (high trust). **Changed in v0.x:** previously defaulted to off; now on for security. If uploads fail with "paths outside the workspace are disabled", either set `BARIVIA_WORKSPACE_ROOT` to cover your data directory, or opt out with `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0`. |
59
60
 
60
61
  Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
@@ -67,7 +68,7 @@ Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also ac
67
68
 
68
69
  **Supported stack:** Node **18+** (see `engines` in `package.json`). Built with `@modelcontextprotocol/sdk` **^1.x** — if a major MCP client upgrade breaks tools, check SDK release notes alongside this package version.
69
70
 
70
- **Local viz fallback:** If the IDE does not advertise MCP Apps, the proxy starts an HTTP server bound to **127.0.0.1** only (not exposed to the LAN). It serves built-in HTML for training prep / monitor / results explorer and proxies read-only API calls with your existing API key. Responses may include `http://127.0.0.1:<port>/viz/...` links. Browser `Access-Control-Allow-Origin: *` applies only to that localhost origin so the embedded pages can load data.
71
+ **Local viz fallback:** The proxy starts an HTTP server bound to **127.0.0.1** only (not exposed to the LAN). It serves built-in HTML for training monitor / results explorer and proxies read-only API calls with your existing API key. **`training_monitor` / `results_explorer` always lead with a standalone `http://127.0.0.1:<port>/viz/...` URL** (even when the host advertises MCP Apps — some Cursor builds list `ui://` but never mount widgets). Browser `Access-Control-Allow-Origin: *` applies only to that localhost origin so the embedded pages can load data.
71
72
 
72
73
  ## Tools and prompts (15 tools + 2 prompts)
73
74
 
@@ -102,7 +103,7 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
102
103
  | `analyze` | Pre-training recommendations (correlation, columns to consider dropping, etc.) |
103
104
  | `list` | Finding dataset IDs |
104
105
  | `get` | One dataset by id — status, staging fields, `stage_job_id`, ingest errors |
105
- | `subset` | Creating a filtered/sliced copy (row_range, filter conditions) |
106
+ | `subset` | Creating a filtered/sliced copy (row_range, filter, `sample_n`) — **only when the user asks**; agents must not auto-downsample before train/analyze |
106
107
  | `add_expression` | Add a derived column from an expression (formula → new column on the dataset) |
107
108
  | `reduce_spectral` | Pre-training reducer for long ordered numeric blocks (spectra, time series, sensor fingerprints, gene panels). Methods: **pca** (top-k principal components), **log_sample** (k columns at log-spaced indices — scattering, audio bands), **uniform_sample** (k columns at evenly-spaced indices — regularly-sampled time series), **stats** (6 fixed per-row summary columns). All produce one feature vector per row; appends derived columns to the dataset. |
108
109
  | `delete` | Removing a dataset |
@@ -172,11 +173,11 @@ Capacity is shared LOCAL/GKE worker pools (no per-key cloud burst lease).
172
173
 
173
174
  | Tool | Role |
174
175
  |------|------|
175
- | `training_monitor` | Live QE + **panel** topographic-error curves (ordering/convergence, dual y-axis); **Panel TE** and **Map TE** scalars at kernel end; FLooP step progress when applicable; optional `jobs(action=status)` is enough |
176
- | `results_explorer` | Browse metrics and figures after training completes |
176
+ | `training_monitor` | Live QE + **panel** TE curves; auto-opened from `train()` submit when the host mounts Apps; cross-link to results explorer when finalize completes; always returns a standalone URL + `ui_delivery` |
177
+ | `results_explorer` | Browse metrics and figures after training completes; cross-link back to training monitor; always returns a standalone URL + `ui_delivery` |
177
178
  | `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
178
179
 
179
- When the client does not advertise MCP Apps support, the proxy starts **`viz-server`** on **`127.0.0.1`** (localhost-only). Tool responses can include `http://127.0.0.1:PORT/viz/...` links. See **Local viz fallback** under Environment Variables.
180
+ Standalone localhost URLs are always first in tool content (see `BARIVIA_UI_DELIVERY` / **Local viz fallback**). Pages cross-link monitor results for the same `job_id`.
180
181
 
181
182
  #### Choosing where to view results
182
183
 
@@ -190,6 +191,7 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
190
191
 
191
192
  ### Migration notes
192
193
 
194
+ - **UI delivery + train auto-open (0.22.4+):** `train()` returns `training_monitor` App structured content so supporting hosts open the monitor in the same turn. App tools emit structured `ui_delivery` (`delivery`, `urls`, `hint_for_agent`) and always put the standalone viz URL first. Env: `BARIVIA_UI_DELIVERY`, `BARIVIA_VIZ_PORT`. Cyclic cos/sin component PNGs are omitted from default `summary.files` / `figures=all` (encoding for training; panels still appear in `combined` / recolor); opt in with train param `viz_upload_cyclic_components=true`.
193
195
  - **Account surface (0.22.0, breaking):** `account(request_compute|compute_status|release_compute)` removed — AWS cloud burst leases are gone. Use `account(status|history|add_funds)`. Capacity is shared LOCAL/GKE pools.
194
196
  - **Structured results + train hints (0.21.x, non-breaking):**
195
197
  - **`results(action=get)`** returns **`structuredContent`** (job id/type, label, summary metrics, file list) alongside the text summary — same pattern as `training_monitor` / `results_explorer`. Agent hosts can read fields without parsing prose.
@@ -229,7 +231,7 @@ When adding or refining tools, follow [MCP best practices](https://modelcontextp
229
231
 
230
232
  ## Data preparation
231
233
 
232
- To train on a subset of your data (e.g. first 2000 rows, or rows where region=Europe) without re-uploading: use **datasets(action=subset)** with `row_range` and/or `filter` to create a new dataset, then train with **train(action=map, dataset_id=...)**, **train(action=siom_map, dataset_id=...)**, or **train(action=floop_siom, dataset_id=...)** on the new dataset_id; or pass **row_range** in the training job params for a one-off slice.
234
+ To train on a subset of your data (e.g. first 2000 rows, or rows where region=Europe) without re-uploading: use **datasets(action=subset)** with `row_range` and/or `filter` (or `sample_n`) to create a new dataset, then train with **train(action=map, dataset_id=...)** on the new dataset_id; or pass **row_range** in the training job params for a one-off slice. **Agents must not call subset/`sample_n` unless the user explicitly asks** — train on the full uploaded table by default.
233
235
 
234
236
  ## How It Works
235
237
 
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 t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as s,registerAppResource as r,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as h}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as k}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list (slim + limit/cursor/status/job_type/has_results), update (label/description/tags), compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs. Results catalog = `jobs(list, has_results=true)` |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, **inventory** (markdown datasets+jobs overview), add_funds, **health** (API liveness via GET /health — no R2) |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees; queues locally if API/storage is down |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Org inventory recipe\n\nTo rebuild a durable markdown overview of an org (datasets + jobs):\n1. `account(action=inventory)` — preferred one-shot (datasets name/id/rows/cols/description/tags + slim recent jobs).\n2. Or compose: `datasets(list)` + `jobs(action=list, limit=50)` (page with `cursor` / filter with `status` / `job_type` / `has_results=true` for a results catalog).\n3. Set `description` + `tags` on `datasets(upload|update)` and `train(...)` / `jobs(update)` so duplicates stay navigable.\n\n## API briefly unavailable (pause, do not crash)\nIf tools return `api_unreachable` / gateway HTML / slim `error_code` lines: summarize in **one calm sentence** — never paste Cloudflare HTML. The proxy already burst-retried (**3** tries, ~**2s** apart); honor `retry_after_sec` (~**30**) before another round. Use `account(action=health)` (no R2). Local downloads and prior results stay usable; `send_feedback` queues under `~/.barivia/deferred-feedback` and flushes when the API returns. Do not start large uploads/trains until `account(status)` or health looks healthy again.\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks for a filter, slice, or random sample. Train and analyze on the uploaded `dataset_id` at full row count — GPU staging/prepare handles hundreds of thousands of rows quickly; silent downsampling hides real prepare/train behavior and wastes scale demos. Use small named fixtures (e.g. sample.csv, periodic_load) only when the user or runbook names them.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});r(T,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),r(T,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),k(T),y(T),A(T),f(T),h(T),g(T,b),_(T),v(T),j(T),x(T),P(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const M=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=s(t);d(!!o?.mimeTypes?.includes(n))},await T.connect(M)})().catch(console.error);
2
+ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as s,registerAppResource as r,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as h}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as k}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list (slim + limit/cursor/status/job_type/has_results), update (label/description/tags), compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs. Results catalog = `jobs(list, has_results=true)` |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, **inventory** (markdown datasets+jobs overview), add_funds, **health** (API liveness via GET /health — no R2) |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees; queues locally if API/storage is down |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Org inventory recipe\n\nTo rebuild a durable markdown overview of an org (datasets + jobs):\n1. `account(action=inventory)` — preferred one-shot (datasets name/id/rows/cols/description/tags + slim recent jobs).\n2. Or compose: `datasets(list)` + `jobs(action=list, limit=50)` (page with `cursor` / filter with `status` / `job_type` / `has_results=true` for a results catalog).\n3. Set `description` + `tags` on `datasets(upload|update)` and `train(...)` / `jobs(update)` so duplicates stay navigable.\n\n## API briefly unavailable (pause, do not crash)\nIf tools return `api_unreachable` / gateway HTML / slim `error_code` lines: summarize in **one calm sentence** — never paste Cloudflare HTML. The proxy already burst-retried (**3** tries, ~**2s** apart); honor `retry_after_sec` (~**30**) before another round. Use `account(action=health)` (no R2). Local downloads and prior results stay usable; `send_feedback` queues under `~/.barivia/deferred-feedback` and flushes when the API returns. Do not start large uploads/trains until `account(status)` or health looks healthy again.\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks. On CPU, `preset=quick` (15×15) is typically very fast up to ~100k rows — do **not** downsample by default when exploring below 100k; only suggest `sample_n` when n>100k or the user asks. Train/analyze the full `dataset_id`. Use small named fixtures only when the user or runbook names them.\n- **Small-n before train:** If preview/analyze shows n<1000 (especially n<100), heed the JUMPBACK/warning — omit `grid_x`/`grid_y` for auto (~sqrt(5·√n)), avoid oversized presets on tiny tables, drop redundant correlated geo features, and check `active_node_fraction`/TE before narrating map insights.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});r(T,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),r(T,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),k(T),y(T),A(T),f(T),h(T),g(T,b),_(T),v(T),j(T),x(T),P(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset below ~100k rows (CPU quick 15×15 is fast); never call `datasets(subset)` / `sample_n` unless the user asks or n>100k exploratory. Small-n (<1000): prefer auto grid, not oversized presets. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const M=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=s(t);d(!!o?.mimeTypes?.includes(n))},await T.connect(M)})().catch(console.error);
@@ -1,8 +1,9 @@
1
1
  import { apiCall } from "./shared.js";
2
- export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07-offline";
2
+ export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07f-offline";
3
3
  const FALLBACK_LINES = [
4
4
  "Before upload: for train(action=map) ensure training columns have no NaNs; for train(action=impute) missing cells in training columns are OK (plain numeric only).",
5
- "Do not auto-subset: never call datasets(subset) or sample_n unless the user explicitly asks — train on the full uploaded dataset_id.",
5
+ "Do not auto-subset: never call datasets(subset) or sample_n unless the user explicitly asks — on CPU, preset=quick 15×15 is typically very fast up to ~100k rows; only suggest sample_n when n>100k or the user asks. Train on the full uploaded dataset_id.",
6
+ "Small-n: if preview/analyze shows n<1000 (especially n<100), omit grid_x/grid_y for auto (~sqrt(5·√n)); avoid preset=quick/standard on tiny tables (dead nodes / TE≈1). Drop redundant correlated geo features before train.",
6
7
  'Please run datasets(action=preview, dataset_id="{id}") to inspect columns, then datasets(action=analyze, dataset_id="{id}") to see which columns and temporal periods are most informative.',
7
8
  "Choose the training path:",
8
9
  '- train(action=map, dataset_id="{id}", ...) for complete-case data',
package/dist/shared.js CHANGED
@@ -39,7 +39,7 @@ function newRequestId() {
39
39
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
40
40
  * wrapper version each action requires. Keep in sync with package.json on bump.
41
41
  */
42
- export const CLIENT_VERSION = "0.23.0";
42
+ export const CLIENT_VERSION = "0.23.2";
43
43
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
44
44
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
45
45
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Format the SIOM occupation / coverage block for results(get) text.
3
+ * Surfaces effective hyperparams + util/Gini/entropy/siom_qe alongside grid
4
+ * dead-node counts (which come from hit_stats elsewhere).
5
+ */
6
+ export function formatSiomOccupationSummary(siom, opts) {
7
+ if (!siom || typeof siom !== "object")
8
+ return [];
9
+ const fmt4 = (v) => v !== null && v !== undefined && v !== "" && Number.isFinite(Number(v))
10
+ ? Number(v).toFixed(4)
11
+ : "N/A";
12
+ const fmt3 = (v) => v !== null && v !== undefined && v !== "" && Number.isFinite(Number(v))
13
+ ? Number(v).toFixed(3)
14
+ : "N/A";
15
+ const fmtAny = (v) => v === null || v === undefined || v === "" ? "N/A" : String(v);
16
+ const hasHyper = siom.gamma !== undefined ||
17
+ siom.gamma_f !== undefined ||
18
+ siom.decay !== undefined ||
19
+ siom.siom_decay !== undefined ||
20
+ siom.penalty !== undefined ||
21
+ siom.siom_penalty !== undefined ||
22
+ siom.reset_per_epoch !== undefined;
23
+ const decay = siom.decay ?? siom.siom_decay;
24
+ const penalty = siom.penalty ?? siom.siom_penalty;
25
+ const files = opts?.files ?? [];
26
+ const hasOccFile = files.some((f) => String(f).includes("siom_occupation"));
27
+ if (opts?.compact) {
28
+ const parts = [
29
+ `utilization: ${fmt4(siom.utilization)}`,
30
+ `dead_fraction: ${fmt4(siom.dead_fraction)}`,
31
+ `gini: ${fmt4(siom.gini)}`,
32
+ `entropy: ${fmt4(siom.entropy)}`,
33
+ siom.siom_qe !== undefined ? `siom_qe: ${fmt4(siom.siom_qe)}` : "",
34
+ hasHyper
35
+ ? `γ=${fmt3(siom.gamma)}${siom.gamma_f !== undefined && Number(siom.gamma_f) > 0 ? `→${fmt3(siom.gamma_f)}` : ""} penalty=${fmtAny(penalty)} decay=${fmt4(decay)}`
36
+ : "",
37
+ ].filter(Boolean);
38
+ return [`SIOM occupation summary — ${parts.join(" | ")}`];
39
+ }
40
+ const lines = [
41
+ "SIOM occupation summary (layer coverage; distinct from grid BMU dead nodes):",
42
+ ` Utilization: ${fmt4(siom.utilization)} | dead_fraction: ${fmt4(siom.dead_fraction)}`,
43
+ ` Gini: ${fmt4(siom.gini)} | Entropy: ${fmt4(siom.entropy)}${siom.max_entropy !== undefined ? ` / max ${fmt4(siom.max_entropy)}` : ""}`,
44
+ siom.siom_qe !== undefined ? ` SIOM QE: ${fmt4(siom.siom_qe)}` : "",
45
+ ];
46
+ if (hasHyper) {
47
+ lines.push(` Knobs: gamma=${fmt3(siom.gamma)}` +
48
+ (siom.gamma_f !== undefined ? ` gamma_f=${fmt3(siom.gamma_f)}` : "") +
49
+ ` siom_decay=${fmt4(decay)} siom_penalty=${fmtAny(penalty)}` +
50
+ (siom.penalty_alpha !== undefined ? ` α=${fmt3(siom.penalty_alpha)}` : "") +
51
+ (siom.reset_per_epoch !== undefined ? ` reset_per_epoch=${fmtAny(siom.reset_per_epoch)}` : ""));
52
+ }
53
+ if (hasOccFile) {
54
+ lines.push(" Artifact: siom_occupation.json (per-node μ) — results(action=download, include_json=true).");
55
+ }
56
+ return lines.filter((l) => l !== "");
57
+ }
@@ -7,6 +7,7 @@ import { registerAuditedTool } from "../audit.js";
7
7
  import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, structuredTextResult, pollUntilComplete, POLL_DERIVE_MAX_MS, POLL_ANALYZE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestDatasetPreview, } from "../shared.js";
8
8
  import { GZIP_UPLOAD_HINT } from "../job_status_format.js";
9
9
  import { formatDatasetInventoryLine, formatTags } from "../inventory_format.js";
10
+ import { PERIODICITY_ROW_ORDER_NOTE, SMALL_N_SOFT, buildCpuScaleGuidanceLines, buildSmallNGuardrailLines, } from "../training_scale_guidance.js";
10
11
  /**
11
12
  * Normalize a nullable string field from the API. Returns "" for absent values,
12
13
  * empty strings, and the literal SQL/serialization sentinels "missing"/"null"
@@ -111,6 +112,16 @@ function formatAnalyzeResult(data, dataset_id) {
111
112
  lines.push(``);
112
113
  }
113
114
  const nextSteps = [];
115
+ const nRows = typeof totalRows === "number" && Number.isFinite(totalRows) ? totalRows : 0;
116
+ if (nRows > 0 && nRows < SMALL_N_SOFT) {
117
+ nextSteps.push(...buildSmallNGuardrailLines({ n: nRows, nFeatures: trainList.length || columns.length }));
118
+ }
119
+ if (nRows > 0) {
120
+ nextSteps.push(...buildCpuScaleGuidanceLines(nRows));
121
+ }
122
+ if (considerDropping.length > 0) {
123
+ nextSteps.push(`Drop redundant correlated columns before train (e.g. keep distance OR lat/lon, not all three when |r|>0.9): ${considerDropping.join(", ")}.`);
124
+ }
114
125
  if (trainList.length > 0) {
115
126
  nextSteps.push(`Train with columns: [${trainList.join(", ")}].`);
116
127
  }
@@ -120,11 +131,11 @@ function formatAnalyzeResult(data, dataset_id) {
120
131
  const periodicStrong = periodicity.filter((p) => p.dominant_score > 0.4);
121
132
  if (periodicStrong.length > 0) {
122
133
  const lags = [...new Set(periodicStrong.map((p) => p.dominant_lag))].sort((a, b) => a - b);
123
- nextSteps.push(`Note: periodicity is computed on row order. If your CSV is not sorted by time, ignore lag-based suggestions; only use cyclic_features when the column is genuinely cyclic (e.g. hour, month, angle).`);
134
+ nextSteps.push(`Note: ${PERIODICITY_ROW_ORDER_NOTE}`);
124
135
  nextSteps.push(`Columns show periodic behavior at lags ${lags.join(", ")} (assuming row order is meaningful). Consider cyclic_features or temporal_features with matching periods.`);
125
136
  nextSteps.push(`For datetime columns: run datasets(action=preview) for temporal_suggestions; match lags (e.g. 365→day_of_year, 12→month, 7→day_of_week, 24→hour_of_day) to choose which temporal components to extract.`);
126
137
  }
127
- nextSteps.push(`Then call train(action=map, dataset_id=${dataset_id}, columns=[...], ...).`);
138
+ nextSteps.push(`Then call train(action=map, dataset_id=${dataset_id}, columns=[...], ...) — omit grid_x/grid_y for auto-size unless you intentionally override.`);
128
139
  lines.push(`Next steps:`, ...nextSteps.map((s) => ` ${s}`));
129
140
  return lines.join("\n");
130
141
  }
@@ -141,7 +152,7 @@ Formats: plain CSV/TSV or gzip (.csv.gz / .tsv.gz). For files above ~100 MB, pre
141
152
  | list | Finding dataset IDs for train_map, preview, or subset — see all available datasets (includes description/tags) |
142
153
  | get | Fetch one dataset by id — status, staging fields, ingest_error, description/tags (use after upload or when staging is slow) |
143
154
  | update | Edit name / description / tags on an existing dataset (helps navigate duplicates) |
144
- | subset | ONLY when the user asks for row_range, filters, or sample_n — never auto-shrink before train/analyze |
155
+ | subset | ONLY when the user asks for row_range, filters, or sample_n — never auto-shrink before train/analyze (CPU quick is fast to ~100k; do not sample_n by default below that) |
145
156
  | add_expression | Add a computed column from an expression (dataset_id + name + expression). With project_onto_job=<training_job_id>, instead projects the computed expression onto that map as a component plane in one step (no column added). |
146
157
  | reduce_spectral | Collapse a long ordered numeric block (e.g. 1000-point spectrum, time series, sensor fingerprint) into a small per-row feature set so the SOM can train on signal-dense features. Methods: pca, log_sample, uniform_sample, stats. |
147
158
  | delete | Cleaning up after experiments or freeing the dataset slot |
@@ -487,7 +498,13 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
487
498
  lines.push(`| ${cols.map((c) => String(row[c] ?? "")).join(" | ")} |`);
488
499
  }
489
500
  }
490
- lines.push(``, `Suggested next step: train(action=map, dataset_id=${dataset_id}, ...) or use the prepare_training prompt; to add derived columns use datasets(action=add_expression, dataset_id=${dataset_id}, name=..., expression=...).`);
501
+ if (totalRows > 0 && totalRows < SMALL_N_SOFT) {
502
+ lines.push(``, ...buildSmallNGuardrailLines({ n: totalRows, nFeatures: cols.length }));
503
+ }
504
+ if (totalRows > 0) {
505
+ lines.push(``, ...buildCpuScaleGuidanceLines(totalRows));
506
+ }
507
+ lines.push(``, `Suggested next step: datasets(action=analyze) then train(action=map, dataset_id=${dataset_id}, ...) or use the prepare_training prompt; to add derived columns use datasets(action=add_expression, dataset_id=${dataset_id}, name=..., expression=...).`);
491
508
  return { content: [{ type: "text", text: lines.join("\n") }] };
492
509
  }
493
510
  if (action === "analyze") {
@@ -32,7 +32,7 @@ ASYNC POLLING PROTOCOL (action=status):
32
32
 
33
33
  action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
34
34
  action=update: PATCH label/description/tags (pass at train submit when possible).
35
- action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown; pass label="…" at train time for readable rows.
35
+ action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown (including SIOM knobs gamma/gamma_f/siom_decay/siom_penalty/reset_per_epoch when they vary); pass label="…" at train time for readable rows.
36
36
  action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
37
37
  action=delete: WARNING — job ID will no longer work with results or any other tool.
38
38
 
@@ -80,7 +80,11 @@ export function renderCompareTable(comparisons) {
80
80
  { key: "n_initial", label: "NInitial", render: fmtAny },
81
81
  { key: "growth_interval", label: "GrowthInt", render: fmtAny },
82
82
  { key: "gamma", label: "Gamma", render: fmtAny },
83
+ { key: "gamma_f", label: "GammaF", render: fmtAny },
83
84
  { key: "siom_decay", label: "SIOMDecay", render: fmtAny },
85
+ { key: "siom_penalty", label: "SIOMPenalty", render: fmtAny },
86
+ { key: "penalty_alpha", label: "Penaltyα", render: fmtAny },
87
+ { key: "reset_per_epoch", label: "Reset/Epoch", render: fmtAny },
84
88
  { key: "elastic_lambda", label: "Elasticλ", render: fmtAny },
85
89
  { key: "elastic_mu", label: "Elasticμ", render: fmtAny },
86
90
  { key: "elastic_anchor", label: "ElasticAnchor", render: fmtAny },
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
3
3
  import { z } from "zod";
4
4
  import { registerAuditedTool } from "../audit.js";
5
5
  import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget, attachResultsImages, resolveOutputDpi, POLL_RECOLOR_MAX_MS, fmtDensityDiffNode, getCaptionForImage, shouldFetchAllRemainingFigures, mimeForFilename, structuredTextResult, } from "../shared.js";
6
+ import { formatSiomOccupationSummary } from "../siom_occupation_format.js";
6
7
  export function registerResultsTool(server) {
7
8
  registerAuditedTool(server, "results", `Retrieve, recolor, download, or export figures and metrics for a completed map job. Presentation and artifact access only — operations on the frozen map (predict, compare, project, impute_column, transition_flow) live in **inference**.
8
9
 
@@ -225,7 +226,10 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
225
226
  hitStats.active_node_fraction !== undefined
226
227
  ? `Grid BMU occupancy: ${(Number(hitStats.active_node_fraction) * 100).toFixed(1)}% active nodes on the map grid`
227
228
  : "",
228
- ...(siom ? [`SIOM layer coverage (distinct from grid dead nodes) — utilization: ${fmt(siom.utilization)} | dead_fraction: ${fmt(siom.dead_fraction)} | siom_qe: ${fmt(siom.siom_qe)}`] : []),
229
+ ...formatSiomOccupationSummary(siom, {
230
+ files: summary.files ?? [],
231
+ compact: true,
232
+ }),
229
233
  hitStats.grid_suggestion ? `Grid hint: ${String(hitStats.grid_suggestion)}` : "",
230
234
  ...(policy.auto_rules_applied && Array.isArray(policy.auto_rules_applied) && policy.auto_rules_applied.length > 0
231
235
  ? [`Auto policy: ${policy.auto_rules_applied.join(", ")} (${policy.viz_mode ?? "viz"}, metrics=${policy.quality_metrics ?? "?"})`]
@@ -296,19 +300,15 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
296
300
  `Coverage Assessment:`,
297
301
  ` Status: ${coverageStatus}${isDegenerate ? " *** NON-COMPETITIVE ***" : isWarning ? " * CAUTION *" : ""}`,
298
302
  ` Diagnosis: ${String(coverage.diagnosis ?? "No plain-language diagnosis available.")}`,
299
- ` Utilization: ${siom.utilization !== undefined ? Number(siom.utilization).toFixed(4) : "N/A"}`,
300
- ` SIOM dead fraction (layer coverage, not grid BMU hits): ${siom.dead_fraction !== undefined ? Number(siom.dead_fraction).toFixed(4) : "N/A"}`,
303
+ ...formatSiomOccupationSummary(siom, { files }),
301
304
  ` Occupied nodes:${hitStats.occupied_nodes !== undefined ? ` ${hitStats.occupied_nodes}` : " N/A"}`,
302
305
  ` Top hit share: ${hitStats.top_hit_share !== undefined ? Number(hitStats.top_hit_share).toFixed(4) : "N/A"}`,
303
- ` Gini: ${siom.gini !== undefined ? Number(siom.gini).toFixed(4) : "N/A"}`,
304
- ` Entropy: ${siom.entropy !== undefined ? Number(siom.entropy).toFixed(4) : "N/A"}`,
305
- ` SIOM gamma: ${siom.gamma !== undefined ? Number(siom.gamma).toFixed(3) : "N/A"} | decay: ${siom.decay !== undefined ? Number(siom.decay).toFixed(4) : "N/A"}`,
306
306
  ...(summary.elastic ? (() => {
307
307
  const el = summary.elastic;
308
308
  return [` Elastic: λ=${el.lambda !== undefined ? Number(el.lambda).toFixed(3) : "0"} μ=${el.mu !== undefined ? Number(el.mu).toFixed(3) : "0"} anchor=${el.anchor !== undefined ? Number(el.anchor).toFixed(3) : "0"}`];
309
309
  })() : []),
310
310
  ``,
311
- `Layout tuning: if Voronoi cells look crowded in weight space (high top_hit_share), reduce max_nodes and/or raise gamma/siom_decay before growth_interval. See training_guidance keys floop_voronoi_crowding, floop_siom_params, floop_elastic_params.`,
311
+ `Layout tuning: if Voronoi cells look crowded in weight space (high top_hit_share), reduce max_nodes and/or raise gamma/siom_decay before growth_interval. See training_guidance keys floop_voronoi_crowding, floop_siom_params, floop_elastic_params, siom_gamma.`,
312
312
  ``,
313
313
  `Quality Metrics:`,
314
314
  ` Quantization Error: ${summary.quantization_error !== undefined ? Number(summary.quantization_error).toFixed(4) : "N/A"}${qeNote}`,
@@ -386,11 +386,11 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
386
386
  ...(compLines ? [`Component value ranges:\n${compLines}`] : []),
387
387
  ...(clusterLines ? ["", "Cluster map (defining features):", clusterLines] : []),
388
388
  ...(topCorrLine ? ["", topCorrLine] : []),
389
- ...(siom ? ["", "SIOM layer coverage (distinct from grid dead nodes):",
390
- ` Utilization: ${siom.utilization !== undefined ? Number(siom.utilization).toFixed(4) : "N/A"}`,
391
- ` SIOM dead fraction: ${siom.dead_fraction !== undefined ? Number(siom.dead_fraction).toFixed(4) : "N/A"}`,
392
- ` Gini: ${siom.gini !== undefined ? Number(siom.gini).toFixed(4) : "N/A"}`,
393
- ` Entropy: ${siom.entropy !== undefined ? Number(siom.entropy).toFixed(4) : "N/A"}`] : []),
389
+ ...(siom
390
+ ? ["", ...formatSiomOccupationSummary(siom, {
391
+ files: summary.files ?? [],
392
+ })]
393
+ : []),
394
394
  ``, `Features: ${features.join(", ")}`,
395
395
  summary.selected_columns ? `Selected columns: ${summary.selected_columns.join(", ")}` : "",
396
396
  summary.transforms ? `Transforms: ${Object.entries(summary.transforms).map(([k, v]) => `${k}=${v}`).join(", ")}` : "",
@@ -4,6 +4,7 @@ import { apiCall } from "../shared.js";
4
4
  import { buildTrainSubmitExtras } from "../train_submit_message.js";
5
5
  import { TRAINING_MONITOR_URI, buildTrainingMonitorResult, } from "./training_monitor.js";
6
6
  import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
7
+ import { SMALL_N_SOFT, buildSmallNGuardrailLines, suggestedAutoGridSide, } from "../training_scale_guidance.js";
7
8
  export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Returns a job_id; poll with jobs(action=status) and then results(action=get). All actions are async.
8
9
 
9
10
  | Action | Use when |
@@ -13,11 +14,11 @@ export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Ret
13
14
  | impute | Sparse training data: trains a missing-tolerant map (accelerated missSOM; SIOM missSOM when model=auto on som_siom+ or model=SIOM) AND returns dense imputed.csv in one job. Plain numeric columns only. Optional SIOM knobs: gamma, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. |
14
15
  | floop_siom | FLooP-SIOM — a growing node-budget manifold instead of a fixed grid; default topology=free (CHL). Requires a plan with all_algorithms (Premium/Enterprise). |
15
16
 
16
- First quick map (no separate baseline action): use \`train(action=map, preset=quick)\` (or \`normalize=mad\` with a grid sized from the preview row count); see the \`baseline_explore\` preset in **training_guidance**.
17
+ First quick map (no separate baseline action): use \`train(action=map, preset=quick)\` when n is large enough (hundreds+ rows); for n<1000 prefer omitting grid_x/grid_y for auto (~sqrt(5·√n)) or an explicit small grid see **training_guidance** / analyze next_steps. On CPU, preset=quick 15×15 is typically very fast up to ~100k rows — do not subsample first.
17
18
 
18
19
  Parameter details (grid, epochs, batch, model, presets, normalize, categorical_features, backend, FLooP max_nodes/effort, SIOM geometry) live in the **training_guidance** tool / **prepare_training** prompt — call them instead of guessing. Backend strings are "cpu" | "gpu" | "gpu_graphs" (no colon); on CPU-only hosts pass backend=cpu.
19
20
 
20
- ASYNC: every action returns a job_id immediately. Poll jobs(action=status, job_id=...) every 10–15s (do NOT poll faster). Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min — do not assume failure before 3 minutes on CPU. When status is completed, call results(action=get, job_id=...).
21
+ ASYNC: every action returns a job_id immediately. Poll jobs(action=status, job_id=...) every 10–15s (do NOT poll faster). Map training typical times: preset=quick 15×15 often tens of seconds even at ~30k–100k rows on CPU | 20×20 ~3–5 min | 40×40 ~15–30 min — do not assume failure before 3 minutes on CPU. When status is completed, call results(action=get, job_id=...).
21
22
 
22
23
  action=impute: use when many cells are missing across training columns; use inference(impute_column) instead when you already have a complete-case map and need to fill one held-out column. Supports transforms (log/sqrt/etc., not rank), temporal_features, cyclic_features, optional cyclic_pairs=[[cos,sin],...] (circular prototype refresh + atomic chord BMU; those columns get circular quality metrics only — never quote Euclidean R² on cos/sin), and normalization_methods (zscore/mad/sigmoidal/none — not sepd). Does not support auto_log_transforms, rank transforms, or normalize=sepd. Defaults model=auto (SIOM missSOM on som_siom+ plans, else plain missSOM), cv_folds=5 → quality.csv (linear: kfold_holdout_pool + resubstitution_optimistic; cyclic pairs: kfold_holdout_circular). Aggregate MAE/RMSE excludes cyclic columns. Do not treat k-fold alone as true missing-cell ground truth. Optional SIOM knobs: gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv (pool_std = neighbourhood dispersion). A completed impute job_id is a valid parent for inference(impute_column).
23
24
  action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
@@ -66,6 +67,11 @@ async function submitTrainJob(opts) {
66
67
  if (opts.gridLargeForRows) {
67
68
  data.grid_auto_warning = true;
68
69
  }
70
+ const guardrails = opts.guardrailLines ?? [];
71
+ if (guardrails.length > 0) {
72
+ data.training_guardrails = guardrails;
73
+ data.small_n_warning = opts.totalRows > 0 && opts.totalRows < SMALL_N_SOFT;
74
+ }
69
75
  const extras = buildTrainSubmitExtras({
70
76
  newJobId,
71
77
  totalRows: opts.totalRows,
@@ -89,8 +95,12 @@ async function submitTrainJob(opts) {
89
95
  catch {
90
96
  /* ignore queue enrichment */
91
97
  }
92
- if (opts.gridLargeForRows) {
93
- msg += ` Note: Grid may be large for ${opts.totalRows} rows (consider grid=auto for fewer dead nodes).`;
98
+ if (guardrails.length > 0) {
99
+ msg += ` ${guardrails.join(" ")}`;
100
+ }
101
+ else if (opts.gridLargeForRows) {
102
+ const autoSide = suggestedAutoGridSide(opts.totalRows);
103
+ msg += ` Note: Grid may be large for ${opts.totalRows} rows — prefer omitting grid_x/grid_y for auto (~${autoSide}×${autoSide}) to reduce dead nodes.`;
94
104
  }
95
105
  data.message = msg;
96
106
  data.suggested_next_step = extras.suggested_next_step;
@@ -195,6 +205,16 @@ export async function runTrain(action, args) {
195
205
  typeof transforms === "object" &&
196
206
  Object.keys(transforms).length > 0;
197
207
  const gridLargeForRows = effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75;
208
+ const nFeatures = Array.isArray(columns) ? columns.length
209
+ : Array.isArray(params.columns) ? params.columns.length
210
+ : undefined;
211
+ const guardrailLines = buildSmallNGuardrailLines({
212
+ n: totalRows,
213
+ nFeatures,
214
+ preset: typeof preset === "string" ? preset : undefined,
215
+ gridX: effectiveGrid?.[0],
216
+ gridY: effectiveGrid?.[1],
217
+ });
198
218
  const resultsHint = action === "impute" ? "view the map and imputed.csv"
199
219
  : "view the map and metrics";
200
220
  const variantPrefix = action === "siom_map" ? "variant=siom"
@@ -213,6 +233,7 @@ export async function runTrain(action, args) {
213
233
  resultsHint,
214
234
  impute: action === "impute",
215
235
  gridLargeForRows: !!gridLargeForRows,
236
+ guardrailLines,
216
237
  });
217
238
  }
218
239
  if (action === "floop_siom") {
@@ -46,7 +46,7 @@ export async function fetchTrainingConfig() {
46
46
  export const TRAINING_INPUT_SCHEMA = {
47
47
  preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
48
48
  grid_x: z.number().int().optional()
49
- .describe("Grid width. Omit grid_x AND grid_y (and preset) to auto-size the map (~5·√√n per side); the result reports hit_stats.active_node_fraction and a grid_suggestion when too many nodes are dead."),
49
+ .describe("Grid width. Omit grid_x AND grid_y (and preset) to auto-size via sqrt(5·√n) per side (clamped 5–50; ~6×6 at n≈45). Pass grid_x/grid_y — not a bare `grid` field. Results report hit_stats.active_node_fraction and grid_suggestion when many nodes are dead."),
50
50
  grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
51
51
  epochs: z.preprocess((v) => {
52
52
  if (v === undefined || v === null)
@@ -118,17 +118,17 @@ export const TRAINING_INPUT_SCHEMA = {
118
118
  emit_cell_uncertainty: z.boolean().optional()
119
119
  .describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
120
120
  gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
121
- .describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5 on worker)"),
121
+ .describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5). Prefer γ∈[0.5,1.5] on large n; γ>1.5 often diminishing returns."),
122
122
  gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
123
- .describe("siom_map / impute SIOM: final γ for exponential decay schedule (0 = constant γ)"),
123
+ .describe("siom_map / impute SIOM: final γ for exponential decay (0 = constant γ). Pattern: start strict, relax late (e.g. 1.5→0.2)."),
124
124
  siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
125
- .describe("siom_map / impute SIOM: occupation EMA decay λ (default 0.01; 0 = cumulative counting)"),
125
+ .describe("siom_map / impute SIOM: occupation EMA decay λ (default 0.01; 0 = cumulative). Avoid pairing high γ with aggressive decay (e.g. 0.05)."),
126
126
  siom_penalty: z.enum(["linear", "log", "exp"]).optional()
127
- .describe("siom_map / impute SIOM: occupation penalty shape"),
127
+ .describe("siom_map / impute SIOM: occupation penalty shape (linear default; prefer exp over log — log often worsens TE)"),
128
128
  penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
129
129
  .describe("siom_map / impute SIOM: scaling α inside log/exp penalty"),
130
130
  reset_per_epoch: z.boolean().optional()
131
- .describe("siom_map / impute SIOM: reset occupation μ at each epoch"),
131
+ .describe("siom_map / impute SIOM: reset occupation μ each epoch (default false; avoid with high γ — can collapse EV)"),
132
132
  siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional().describe("siom_map: l2 (default) = legacy L2 on cos/sin columns; mixed = cyclic torus distance when cyclic pairs exist; auto = mixed only when cyclic encodings are present"),
133
133
  siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional().describe("siom_map: backend for metric-aligned siom_qe when using mixed geometry; auto uses GPU when available"),
134
134
  siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional().describe("siom_map: batch size for GPU SIOM QE (default 256 on worker)"),
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Scale / small-n guidance shared by datasets(preview|analyze) and train submit.
3
+ * Auto-grid formula matches API/worker: clamp(round(sqrt(5*sqrt(n))), 5, 50).
4
+ */
5
+ export const SMALL_N_SOFT = 1000;
6
+ export const SMALL_N_HARD = 100;
7
+ /** Agents should not suggest sample_n below this unless the user asks. */
8
+ export const SAMPLE_N_SUGGEST_THRESHOLD = 100_000;
9
+ /** Worker/API auto grid side length. */
10
+ export function suggestedAutoGridSide(n) {
11
+ if (!Number.isFinite(n) || n < 1)
12
+ return 5;
13
+ return Math.max(5, Math.min(50, Math.round(Math.sqrt(5 * Math.sqrt(n)))));
14
+ }
15
+ export function presetGridCells(preset) {
16
+ switch (preset) {
17
+ case "quick":
18
+ return 15 * 15;
19
+ case "standard":
20
+ return 25 * 25;
21
+ case "refined":
22
+ return 40 * 40;
23
+ case "high_res":
24
+ return 60 * 60;
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Build prominent small-n / oversize-grid warnings for agents.
31
+ * Empty when n is healthy and the chosen grid is not oversized.
32
+ */
33
+ export function buildSmallNGuardrailLines(opts) {
34
+ const { n, nFeatures, preset, gridX, gridY } = opts;
35
+ if (!Number.isFinite(n) || n <= 0)
36
+ return [];
37
+ const lines = [];
38
+ const autoSide = suggestedAutoGridSide(n);
39
+ const cells = gridX !== undefined && gridY !== undefined
40
+ ? gridX * gridY
41
+ : presetGridCells(preset);
42
+ if (n < SMALL_N_HARD) {
43
+ lines.push(`JUMPBACK — small-n (${n} rows): maps are fragile below ~100 rows. Prefer grid_x/grid_y ≈ ${autoSide}×${autoSide} (omit both for auto ≈ sqrt(5·√n)), not preset=quick/standard (15×15/25×25 → mostly dead nodes). Drop redundant geo features (keep distance OR lat/lon, not all three when |r|>0.9). Check hit_stats.active_node_fraction / TE before narrating insights.`);
44
+ }
45
+ else if (n < SMALL_N_SOFT) {
46
+ lines.push(`Warning — small-n (${n} rows < 1000): oversized grids leave dead nodes. Auto grid ≈ ${autoSide}×${autoSide}; omit grid_x/grid_y for auto, or size so |features|×grid_cells is not ≫ n. preset=quick (15×15) is often too large below a few hundred rows — verify active_node_fraction after train.`);
47
+ }
48
+ if (cells !== null && cells > n * 0.75) {
49
+ lines.push(`Grid risk: ${cells} cells vs ${n} rows (cells ≫ n) → expect dead nodes / TE≈1. Use auto (~${autoSide}×${autoSide}) or a smaller explicit grid_x/grid_y.`);
50
+ }
51
+ if (nFeatures !== undefined &&
52
+ nFeatures > 0 &&
53
+ cells !== null &&
54
+ nFeatures * cells > n * 2) {
55
+ lines.push(`Capacity risk: ${nFeatures} features × ${cells} cells ≫ ${n} rows. Drop highly correlated columns (see analyze recommendations) and/or shrink the grid before train.`);
56
+ }
57
+ return lines;
58
+ }
59
+ /** CPU scale guidance: do not subsample by default below 100k. */
60
+ export function buildCpuScaleGuidanceLines(n) {
61
+ if (!Number.isFinite(n) || n <= 0)
62
+ return [];
63
+ if (n <= SAMPLE_N_SUGGEST_THRESHOLD) {
64
+ return [
65
+ `Scale: on CPU, preset=quick (15×15) is typically very fast up to ~100k rows (tens of seconds). Do NOT call datasets(subset)/sample_n for n=${n.toLocaleString()} — train the full dataset_id unless the user asks to downsample. Only suggest sample_n when n>100k or the user requests it.`,
66
+ ];
67
+ }
68
+ return [
69
+ `Scale: n=${n.toLocaleString()} > 100k — full-table train is still supported; suggest sample_n only if the user wants a faster exploratory pass or ops runbook says so. Never auto-subset silently.`,
70
+ ];
71
+ }
72
+ /** Periodicity caveat for analyze next_steps (row-order dependent). */
73
+ export const PERIODICITY_ROW_ORDER_NOTE = "Periodicity assumes meaningful row order (time/sequence). If the CSV is unordered or shuffled, ignore lag-based suggestions; only use cyclic_features when the column is genuinely cyclic (e.g. hour, month, angle).";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barsom-mcp",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "barSOM MCP proxy — connect any MCP client to the barSOM cloud API for Self-Organizing Map analytics",
5
5
  "keywords": [
6
6
  "mcp",