@barivia/barsom-mcp 0.23.3 → 0.23.5

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
@@ -115,7 +115,7 @@ All training is submitted via the `train` tool; every action is async and return
115
115
  | Action | Use when |
116
116
  |--------|----------|
117
117
  | `map` | New map training job — full control: model type, grid, epochs, cyclic/temporal features, transforms. Complete-case data only (no NaNs in training columns). |
118
- | `impute` | Sparse training: missSOM / SIOM missSOM — map + `imputed.csv`. Supports **`transforms`**, **`temporal_features`**, **`cyclic_features`**, **`normalization_methods`** (zscore/mad/sigmoidal/none — **not** sepd or rank). Defaults: `model=auto`, `cv_folds=5`. Valid parent for `inference(impute_column)`. |
118
+ | `impute` | Sparse training (SIOM entitlement): missSOM / SIOM missSOM — map + `imputed.csv`. Supports **`transforms`**, **`temporal_features`**, **`cyclic_features`**, **`normalization_methods`** (zscore/mad/sigmoidal/none — **not** sepd or rank). Defaults: `model=auto`, `cv_folds=5`. Valid parent for `inference(impute_column)`. |
119
119
  | `siom_map` | Self-interacting map — same grid-map workflow plus SIOM controls such as `gamma`, `siom_decay`, and penalty selection. |
120
120
  | `floop_siom` | FLooP-SIOM — growing node-budget manifold; default `topology=free` (CHL); optional `topology=chain` for strict 1D linked list. Requires Premium/Enterprise (`all_algorithms`). |
121
121
 
@@ -153,7 +153,7 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
153
153
  |--------|--------|--------|
154
154
  | `predict` | Score rows against the trained map. **Inputs:** `dataset_id` (defaults to the parent training dataset) **or** inline `rows` (≤500). **Output style** (`output` param): `"compact"` → `predictions.csv` (row_id, bmu_x/y, bmu_node_index, cluster_id [+ QE / qe_p95 / potential_anomaly when scoring **new** data]); `"annotated"` → `annotated.csv` (original CSV + BMU columns appended). **Regime auto-detected:** when the resolved dataset matches the training dataset, QE columns are intentionally omitted in compact output (training-set fit ≠ generalisation; the p95 anomaly flag would be circular). Prefer `dataset_id` for batches and SIOM/irregular maps. | 5–120s |
155
155
  | `batch_predict` | Fan out many predict jobs at once against one parent training `job_id`: pass `inputs` (array of `{ dataset_id? , rows? }`). Returns the list of submitted job_ids (does not auto-poll — poll each with `jobs(action=status)`). | submits N jobs |
156
- | `impute_column` | Fill a numeric **target_column** not used in training: **requires** `dataset_id` + `target_column`. Parent job: completed **train(action=map)** or **train(action=impute)**. Dataset must contain all training features plus the target. Pools observed target values from rows mapped to this row's BMU and topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders unless the map is periodic). `only_missing` (default true); `impute_aggregation`: mean or median. **Not** held-out validated — map-local estimate. Output **`imputed.csv`**. For holes across many training columns, prefer **train(action=impute)** first. | 5–120s |
156
+ | `impute_column` | Fill a numeric **target_column** not used in training: **requires** `dataset_id` + `target_column`. Parent job: completed **train(action=map)** or **train(action=impute)** (impute needs SIOM entitlement). Dataset must contain all training features plus the target. Pools observed target values from rows mapped to this row's BMU and topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders unless the map is periodic). `only_missing` (default true); `impute_aggregation`: mean or median. **Not** held-out validated — map-local estimate. Output **`imputed.csv`**. For holes across many training columns, prefer **train(action=impute)** when entitled. | 5–120s |
157
157
  | `compare` | density-diff heatmap + top gained/lost nodes — drift, A/B, cohort | 30–120s |
158
158
  | `project_columns` | Project one or more dataset columns onto the trained map (component planes) | async |
159
159
  | `transition_flow` | Temporal state-transition arrows on the trained map (time-ordered rows only). Pass the training `job_id`; returns a new result `job_id` for `results(get/download)`. `lag`/`min_transitions`/`top_k` tune it. | 30–60s |
@@ -174,7 +174,7 @@ Capacity is shared LOCAL/GKE worker pools (no per-key cloud burst lease).
174
174
  | Tool | Role |
175
175
  |------|------|
176
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
+ | `results_explorer` | Interactive explorer with a **figure dropdown above the plot** (grouped Summary → Diagnostics → Components); sidebar metrics/highlights; cross-link to training monitor; always returns a standalone URL + `ui_delivery` |
178
178
  | `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
179
179
 
180
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`.
@@ -191,6 +191,7 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
191
191
 
192
192
  ### Migration notes
193
193
 
194
+ - **Results explorer figure dropdown (0.23.2):** `results_explorer` puts a grouped `<select>` above the plot (no scrolling the sidebar to switch figures), matching barmesh explorer UX. Sidebar keeps Metrics + Highlights only.
194
195
  - **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`.
195
196
  - **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.
196
197
  - **Structured results + train hints (0.21.x, non-breaking):**
package/dist/audit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * MCP tool audit wrapper — tool name, action, latency, outcome; no secrets.
3
3
  */
4
4
  import { logAudit } from "./logger.js";
5
- import { runWithTrace } from "./shared.js";
5
+ import { currentTraceId, runWithTrace } from "./shared.js";
6
6
  /** Keys safe to include in audit param summaries (no paths, keys, or bodies). */
7
7
  const AUDIT_PARAM_KEYS = new Set([
8
8
  "action",
@@ -137,6 +137,7 @@ export async function runMcpToolAudit(tool, action, args, handler) {
137
137
  action,
138
138
  duration_ms: Date.now() - t0,
139
139
  outcome: "ok",
140
+ trace_id: currentTraceId(),
140
141
  ...ids,
141
142
  ...(extras.timeout_hit ? { timeout_hit: true } : {}),
142
143
  ...(extras.output_truncated ? { output_truncated: true } : {}),
@@ -151,6 +152,7 @@ export async function runMcpToolAudit(tool, action, args, handler) {
151
152
  duration_ms: Date.now() - t0,
152
153
  outcome: "error",
153
154
  error_code: errorCodeFrom(err),
155
+ trace_id: currentTraceId(),
154
156
  ...(Object.keys(params).length > 0 ? { params } : {}),
155
157
  });
156
158
  throw err;
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. 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);
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 c,loadViewHtml as p,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 I,TRAINING_MONITOR_URI as A}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 M=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; impute + siom_map (SIOM entitlement); floop_siom (FLooP entitlement). 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(M,w,w,{mimeType:n},async()=>{const e=await p("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(M,A,A,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:A,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),k(M),y(M),I(M),f(M),h(M),g(M,b),_(M),v(M),j(M),x(M),P(M),M.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 with SIOM entitlement; floop_siom when entitled; 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")}}]})),M.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 T=new t;(async function(){try{const e=await a(l,c,p);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=M.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=s(t);d(!!o?.mimeTypes?.includes(n))},await M.connect(T)})().catch(console.error);
package/dist/logger.js CHANGED
@@ -35,6 +35,7 @@ export function logWarn(msg, fields = {}) {
35
35
  export function logError(msg, fields = {}) {
36
36
  emit({ ...fields, level: "error", msg });
37
37
  }
38
+ /** Audit line: stable event name is `msg=mcp_tool_call` (OBSERVABILITY taxonomy). */
38
39
  export function logAudit(fields) {
39
- emit({ ...fields, event: "mcp_tool_call", level: "info", msg: "mcp_tool_call" });
40
+ emit({ ...fields, level: "info", msg: "mcp_tool_call" });
40
41
  }
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.3";
42
+ export const CLIENT_VERSION = "0.23.5";
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). */
@@ -630,9 +630,13 @@ function _newSpanId() {
630
630
  export function runWithTrace(fn) {
631
631
  return _traceStore.run(_newTraceId(), fn);
632
632
  }
633
- function _currentTraceId() {
633
+ /** Current ALS trace id, or a fresh id if called outside `runWithTrace`. */
634
+ export function currentTraceId() {
634
635
  return _traceStore.getStore() ?? _newTraceId();
635
636
  }
637
+ function _currentTraceId() {
638
+ return currentTraceId();
639
+ }
636
640
  function _traceparentHeader() {
637
641
  return `00-${_currentTraceId()}-${_newSpanId()}-01`;
638
642
  }
@@ -184,7 +184,7 @@ async function handleResultsExplorer(job_id) {
184
184
  export function registerExploreMapTool(server) {
185
185
  const toolConfig = {
186
186
  title: "Results Explorer",
187
- description: "PREFERRED way to browse a completed map's figures after a first results(get) glance. Interactive explorer for a completed map job: a responsive view of key metrics and every available figure (combined, U-matrix, hit histogram, component planes). Embeds as an MCP App or falls back to a standalone localhost page. Offer it to the user once a train job completes; results(get) remains the headless/metrics path.",
187
+ description: "PREFERRED way to browse a completed map's figures after a first results(get) glance. Interactive explorer with a grouped figure dropdown above the plot (Summary Diagnostics Components Other): combined, U-matrix, hit histogram, component planes, plus sidebar metrics/highlights. Embeds as an MCP App or falls back to a standalone localhost page. Offer it to the user once a train job completes; results(get) remains the headless/metrics path.",
188
188
  inputSchema: {
189
189
  job_id: z.string().describe("Job ID of a completed map training job"),
190
190
  },
@@ -3,7 +3,6 @@ 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";
7
6
  export function registerResultsTool(server) {
8
7
  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**.
9
8
 
@@ -13,6 +12,7 @@ export function registerResultsTool(server) {
13
12
  | export | Learning curve, raw weights, or per-node stats | instant |
14
13
  | download | Saving figures to a local folder | instant |
15
14
  | recolor | Changing colormap or output format without retraining | async (~10–30s) |
15
+ | feature_divergences | Refresh KL matrices / optional Wasserstein on component planes | async |
16
16
 
17
17
  Temporal state-transition arrows moved to **inference(action=transition_flow)** (use the new result job_id with results(get/download) to read the figure).
18
18
 
@@ -20,7 +20,7 @@ ONLY call this after jobs(action=status) returns "completed".
20
20
  ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
21
21
 
22
22
  action=get: Returns text summary with quality metrics and inline images.
23
- - figures: omit = combined only. "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, component_1..N). Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
23
+ - figures: omit = combined only. "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, divergence_kl, component_1..N). Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
24
24
  - include_individual: if true (and figures omitted), inlines every component plane.
25
25
  - After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
26
26
  - After showing results, guide the user: QE interpretation, whether to retrain, which features to explore.
@@ -40,15 +40,17 @@ action=recolor: Change colormap or output format — no retraining. Returns a ne
40
40
  AFTER: use results(action=get, job_id=NEW_JOB_ID).
41
41
  Colormaps: viridis, plasma, inferno, magma, cividis, turbo, coolwarm, balance, hsv, twilight, RdBu, Spectral. Also: thermal, hot, grays, RdYlBu, delta, curl, phase, rainbow, tol_bright, tol_muted.
42
42
 
43
+ action=feature_divergences: Recompute component-plane divergences (min-shift sum-normalized denormalized planes). Finalize already writes symmetric KL + rankings; use this for Wasserstein or a refresh. metrics default ["symmetric_kl","directed_kl"]; add "wasserstein" for W1. Auto-polls then summarizes top pairs vs Pearson top_correlations.
44
+
43
45
  NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or temporal flow (use inference(action=transition_flow)).`, {
44
46
  action: z
45
- .enum(["get", "recolor", "download", "export"])
46
- .describe("get: inline results + metrics; recolor: new colormap/format (async); download: save to disk; export: structured data (training_log/weights/nodes). Temporal flow moved to inference(action=transition_flow)."),
47
+ .enum(["get", "recolor", "download", "export", "feature_divergences"])
48
+ .describe("get: inline results + metrics; recolor: new colormap/format (async); download: save to disk; export: structured data; feature_divergences: KL/W1 on component planes (async)."),
47
49
  job_id: z.string().describe("Job ID of a completed job"),
48
50
  figures: z
49
51
  .union([z.enum(["default", "combined_only", "all", "images", "none"]), z.array(z.string())])
50
52
  .optional()
51
- .describe("action=get: omit=combined only; 'all'=all plots; 'none'=metrics text only (no images, ideal for sweeps and LLM clients); array=specific (combined,umatrix,hit_histogram,learning_curve,correlation,component_1..N). action=download: 'all'=all image files; array=specific filenames."),
53
+ .describe("action=get: omit=combined only; 'all'=all plots; 'none'=metrics text only (no images, ideal for sweeps and LLM clients); array=specific (combined,umatrix,hit_histogram,learning_curve,correlation,divergence_kl,component_1..N). action=download: 'all'=all image files; array=specific filenames."),
52
54
  include_individual: z
53
55
  .boolean()
54
56
  .optional()
@@ -80,12 +82,20 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
80
82
  recolor_figures: z
81
83
  .array(z.string())
82
84
  .optional()
83
- .describe("action=recolor: which figures to re-render (default: [combined]). Options: combined, umatrix, hit_histogram, correlation, component_1..N"),
85
+ .describe("action=recolor: which figures to re-render (default: [combined]). Options: combined, umatrix, hit_histogram, correlation, divergence_kl, component_1..N"),
84
86
  export_type: z
85
87
  .enum(["training_log", "weights", "nodes", "clusters", "transition_matrix"])
86
88
  .optional()
87
89
  .describe("action=export: training_log=learning curve; weights=weight matrix; nodes=per-node stats; clusters=regime boundaries; transition_matrix=Markov transition matrix"),
88
- }, async ({ action, job_id, figures, include_individual, include_json, folder, colormap, output_format, output_dpi, recolor_figures, export_type: exportType }) => {
90
+ metrics: z
91
+ .array(z.enum(["symmetric_kl", "directed_kl", "wasserstein"]))
92
+ .optional()
93
+ .describe("action=feature_divergences: which matrices to compute (default symmetric_kl+directed_kl). Add wasserstein for W1 (slower)."),
94
+ wasserstein_method: z
95
+ .enum(["sinkhorn", "exact"])
96
+ .optional()
97
+ .describe("action=feature_divergences: OT solver when metrics includes wasserstein (default sinkhorn)."),
98
+ }, async ({ action, job_id, figures, include_individual, include_json, folder, colormap, output_format, output_dpi, recolor_figures, export_type: exportType, metrics: divergenceMetrics, wasserstein_method: wassersteinMethod }) => {
89
99
  const figuresArg = figures;
90
100
  if (action === "get") {
91
101
  resetInlineAttachBudget();
@@ -226,10 +236,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
226
236
  hitStats.active_node_fraction !== undefined
227
237
  ? `Grid BMU occupancy: ${(Number(hitStats.active_node_fraction) * 100).toFixed(1)}% active nodes on the map grid`
228
238
  : "",
229
- ...formatSiomOccupationSummary(siom, {
230
- files: summary.files ?? [],
231
- compact: true,
232
- }),
239
+ ...(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)}`] : []),
233
240
  hitStats.grid_suggestion ? `Grid hint: ${String(hitStats.grid_suggestion)}` : "",
234
241
  ...(policy.auto_rules_applied && Array.isArray(policy.auto_rules_applied) && policy.auto_rules_applied.length > 0
235
242
  ? [`Auto policy: ${policy.auto_rules_applied.join(", ")} (${policy.viz_mode ?? "viz"}, metrics=${policy.quality_metrics ?? "?"})`]
@@ -300,21 +307,37 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
300
307
  `Coverage Assessment:`,
301
308
  ` Status: ${coverageStatus}${isDegenerate ? " *** NON-COMPETITIVE ***" : isWarning ? " * CAUTION *" : ""}`,
302
309
  ` Diagnosis: ${String(coverage.diagnosis ?? "No plain-language diagnosis available.")}`,
303
- ...formatSiomOccupationSummary(siom, { files }),
310
+ ` Utilization: ${siom.utilization !== undefined ? Number(siom.utilization).toFixed(4) : "N/A"}`,
311
+ ` SIOM dead fraction (layer coverage, not grid BMU hits): ${siom.dead_fraction !== undefined ? Number(siom.dead_fraction).toFixed(4) : "N/A"}`,
304
312
  ` Occupied nodes:${hitStats.occupied_nodes !== undefined ? ` ${hitStats.occupied_nodes}` : " N/A"}`,
305
313
  ` Top hit share: ${hitStats.top_hit_share !== undefined ? Number(hitStats.top_hit_share).toFixed(4) : "N/A"}`,
314
+ ` Gini: ${siom.gini !== undefined ? Number(siom.gini).toFixed(4) : "N/A"}`,
315
+ ` Entropy: ${siom.entropy !== undefined ? Number(siom.entropy).toFixed(4) : "N/A"}`,
316
+ ` SIOM gamma: ${siom.gamma !== undefined ? Number(siom.gamma).toFixed(3) : "N/A"} | decay: ${siom.decay !== undefined ? Number(siom.decay).toFixed(4) : "N/A"}`,
306
317
  ...(summary.elastic ? (() => {
307
318
  const el = summary.elastic;
308
319
  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
320
  })() : []),
310
321
  ``,
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.`,
322
+ `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.`,
312
323
  ``,
313
324
  `Quality Metrics:`,
314
325
  ` Quantization Error: ${summary.quantization_error !== undefined ? Number(summary.quantization_error).toFixed(4) : "N/A"}${qeNote}`,
315
326
  ` Topographic Error: ${summary.topographic_error !== undefined ? Number(summary.topographic_error).toFixed(4) : "N/A"}${teNote}`,
316
327
  "",
317
328
  "Choosing a map for presentation: Lower QE/TE often favors larger, denser graphs. For slides and interpretation, sparser graphs (fewer occupied nodes, lower top_hit_share) are often easier to read even when metrics rank them lower. Compare figures side-by-side before picking a winner from metrics alone.",
329
+ ...(() => {
330
+ const topDivs = summary.top_divergences;
331
+ const klComp = summary.feature_kl_complexity;
332
+ const out = [];
333
+ if (topDivs?.length) {
334
+ out.push(`Top component-plane divergences (symmetric KL): ${topDivs.map((t) => `${t.f1}&${t.f2}=${typeof t.symmetric_kl === "number" ? Number(t.symmetric_kl).toFixed(3) : t.symmetric_kl}`).join(", ")}`);
335
+ }
336
+ if (klComp?.length) {
337
+ out.push(`Feature KL complexity: ${klComp.map((t) => `${t.feature}=${typeof t.mean_outgoing_kl === "number" ? Number(t.mean_outgoing_kl).toFixed(3) : t.mean_outgoing_kl}`).join(", ")}`);
338
+ }
339
+ return out.length ? ["", ...out] : [];
340
+ })(),
318
341
  ...(Array.isArray(coverage.recommended_actions) && coverage.recommended_actions.length > 0
319
342
  ? ["", "Recommended next steps:", ...coverage.recommended_actions.map((step) => ` - ${String(step)}`)]
320
343
  : []),
@@ -357,7 +380,15 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
357
380
  : "";
358
381
  const topCorrs = summary.top_correlations;
359
382
  const topCorrLine = topCorrs && topCorrs.length > 0
360
- ? `Strongest feature correlations: ${topCorrs.map((t) => `${t.f1 ?? "?"} & ${t.f2 ?? "?"} (${typeof t.r === "number" ? Number(t.r).toFixed(2) : t.r})`).join(", ")}`
383
+ ? `Strongest feature correlations (Pearson, linear): ${topCorrs.map((t) => `${t.f1 ?? "?"} & ${t.f2 ?? "?"} (${typeof t.r === "number" ? Number(t.r).toFixed(2) : t.r})`).join(", ")}`
384
+ : "";
385
+ const topDivs = summary.top_divergences;
386
+ const topDivLine = topDivs && topDivs.length > 0
387
+ ? `Strongest component-plane divergences (symmetric KL, nonlinear): ${topDivs.map((t) => `${t.f1 ?? "?"} & ${t.f2 ?? "?"} (${typeof t.symmetric_kl === "number" ? Number(t.symmetric_kl).toFixed(3) : t.symmetric_kl})`).join(", ")}`
388
+ : "";
389
+ const klComplexity = summary.feature_kl_complexity;
390
+ const klComplexityLine = klComplexity && klComplexity.length > 0
391
+ ? `Feature KL complexity (mean outgoing directed KL): ${klComplexity.map((t) => `${t.feature ?? "?"} (${typeof t.mean_outgoing_kl === "number" ? Number(t.mean_outgoing_kl).toFixed(3) : t.mean_outgoing_kl})`).join(", ")}`
361
392
  : "";
362
393
  const siom = summary.siom;
363
394
  const textSummary = [
@@ -386,11 +417,14 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
386
417
  ...(compLines ? [`Component value ranges:\n${compLines}`] : []),
387
418
  ...(clusterLines ? ["", "Cluster map (defining features):", clusterLines] : []),
388
419
  ...(topCorrLine ? ["", topCorrLine] : []),
389
- ...(siom
390
- ? ["", ...formatSiomOccupationSummary(siom, {
391
- files: summary.files ?? [],
392
- })]
393
- : []),
420
+ ...(topDivLine ? [topDivLine] : []),
421
+ ...(klComplexityLine ? [klComplexityLine] : []),
422
+ ...(topDivLine || klComplexityLine ? ["(Compare Pearson vs KL: high |r| ≠ low KL; use results(action=feature_divergences, metrics=[\"wasserstein\"]) for W1.)"] : []),
423
+ ...(siom ? ["", "SIOM layer coverage (distinct from grid dead nodes):",
424
+ ` Utilization: ${siom.utilization !== undefined ? Number(siom.utilization).toFixed(4) : "N/A"}`,
425
+ ` SIOM dead fraction: ${siom.dead_fraction !== undefined ? Number(siom.dead_fraction).toFixed(4) : "N/A"}`,
426
+ ` Gini: ${siom.gini !== undefined ? Number(siom.gini).toFixed(4) : "N/A"}`,
427
+ ` Entropy: ${siom.entropy !== undefined ? Number(siom.entropy).toFixed(4) : "N/A"}`] : []),
394
428
  ``, `Features: ${features.join(", ")}`,
395
429
  summary.selected_columns ? `Selected columns: ${summary.selected_columns.join(", ")}` : "",
396
430
  summary.transforms ? `Transforms: ${Object.entries(summary.transforms).map(([k, v]) => `${k}=${v}`).join(", ")}` : "",
@@ -434,7 +468,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
434
468
  const showAvailable = files.length > 0 && !(figures === "all" || figures === "images" || figures === "none");
435
469
  if (showAvailable) {
436
470
  const logicalNames = jobType2 === "train_som" || jobType2 === "train_siom" || jobType2 === "render_variant"
437
- ? `Logical names: combined, umatrix, hit_histogram, correlation, ${featuresForLog.map((_, i) => `component_${i + 1}`).join(", ")}. `
471
+ ? `Logical names: combined, umatrix, hit_histogram, correlation, divergence_kl, ${featuresForLog.map((_, i) => `component_${i + 1}`).join(", ")}. `
438
472
  : "";
439
473
  content.push({ type: "text", text: `Available: ${files.join(", ")}. ${logicalNames}Use results(action=get, figures=[...]) for specific plots, figures=all for everything, or figures="none" for metrics-only.` });
440
474
  }
@@ -664,6 +698,53 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
664
698
  }
665
699
  return { content: [{ type: "text", text: `Recolor job ${newJobId} submitted. Poll with jobs(action=status, job_id="${newJobId}"), then results(action=get, job_id="${newJobId}").` }] };
666
700
  }
701
+ if (action === "feature_divergences") {
702
+ const body = {
703
+ metrics: divergenceMetrics ?? ["symmetric_kl", "directed_kl"],
704
+ output_format: output_format ?? "png",
705
+ output_dpi: resolveOutputDpi(output_dpi),
706
+ };
707
+ if (wassersteinMethod != null)
708
+ body.wasserstein_method = wassersteinMethod;
709
+ const data = (await apiCall("POST", `/v1/results/${job_id}/feature_divergences`, body));
710
+ const newJobId = data.id;
711
+ const poll = await pollUntilComplete(newJobId, POLL_RECOLOR_MAX_MS);
712
+ if (poll.status !== "completed") {
713
+ return {
714
+ content: [{
715
+ type: "text",
716
+ text: `Feature divergences job ${newJobId} submitted (parent ${job_id}). Poll with jobs(action=status, job_id="${newJobId}"), then results(action=get, job_id="${newJobId}").`,
717
+ }],
718
+ };
719
+ }
720
+ const res = (await apiCall("GET", `/v1/results/${newJobId}`));
721
+ const sum = (res.summary ?? {});
722
+ const topDivs = sum.top_divergences ?? [];
723
+ const klComp = sum.feature_kl_complexity ?? [];
724
+ const topW1 = sum.top_wasserstein ?? null;
725
+ const lines = [
726
+ `Feature divergences complete — job_id: ${newJobId} (parent training: ${job_id})`,
727
+ `Method: ${sum.divergence_method ?? "symmetric_kl_min_shift_sum_norm_denormalized_component_planes"}`,
728
+ `Weight space: ${sum.weight_space ?? "denormalized"}`,
729
+ `Metrics: ${JSON.stringify(sum.metrics ?? body.metrics)}`,
730
+ topDivs.length
731
+ ? `Top symmetric KL pairs: ${topDivs.map((t) => `${t.f1}&${t.f2}=${typeof t.symmetric_kl === "number" ? Number(t.symmetric_kl).toFixed(3) : t.symmetric_kl}`).join(", ")}`
732
+ : "",
733
+ klComp.length
734
+ ? `KL complexity ranking: ${klComp.map((t) => `${t.feature}=${typeof t.mean_outgoing_kl === "number" ? Number(t.mean_outgoing_kl).toFixed(3) : t.mean_outgoing_kl}`).join(", ")}`
735
+ : "",
736
+ topW1 && topW1.length
737
+ ? `Top Wasserstein pairs: ${topW1.map((t) => `${t.f1}&${t.f2}=${typeof t.wasserstein === "number" ? Number(t.wasserstein).toFixed(3) : t.wasserstein}`).join(", ")}`
738
+ : "",
739
+ `Artifacts refreshed on parent result (divergence_kl / feature_divergences.json). Compare with Pearson top_correlations from results(get) on the training job.`,
740
+ ].filter(Boolean);
741
+ const content = [{ type: "text", text: lines.join("\n") }];
742
+ const files = sum.files ?? [];
743
+ for (const f of files.filter((x) => /\.(png|svg)$/i.test(x))) {
744
+ await tryAttachImage(content, newJobId, f);
745
+ }
746
+ return { content };
747
+ }
667
748
  throw new Error("Invalid action");
668
749
  });
669
750
  }
@@ -4,23 +4,22 @@ 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";
8
7
  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.
9
8
 
10
9
  | Action | Use when |
11
10
  |--------|----------|
12
11
  | map | Standard SOM on a fixed hex grid — complete-case numeric data (no NaNs in training columns). |
13
12
  | siom_map | Self-interacting map — same grid flow plus SIOM coverage control (gamma, siom_decay, penalty) to avoid dead nodes. |
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. |
13
+ | impute | Sparse training data (requires SIOM entitlement): trains a missing-tolerant map (SIOM missSOM when model=auto/SIOM; plain missSOM when model=SOM) 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. |
15
14
  | 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). |
16
15
 
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.
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**.
18
17
 
19
18
  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.
20
19
 
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=...).
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=...).
22
21
 
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).
22
+ 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. Requires SIOM entitlement (train_impute). Defaults model=auto (SIOM missSOM; model=SOM for 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).
24
23
  action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
25
24
  NOT FOR: lifecycle (status/list/compare/cancel/delete) → use jobs; scoring data → use inference.
26
25
 
@@ -67,11 +66,6 @@ async function submitTrainJob(opts) {
67
66
  if (opts.gridLargeForRows) {
68
67
  data.grid_auto_warning = true;
69
68
  }
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
- }
75
69
  const extras = buildTrainSubmitExtras({
76
70
  newJobId,
77
71
  totalRows: opts.totalRows,
@@ -95,12 +89,8 @@ async function submitTrainJob(opts) {
95
89
  catch {
96
90
  /* ignore queue enrichment */
97
91
  }
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.`;
92
+ if (opts.gridLargeForRows) {
93
+ msg += ` Note: Grid may be large for ${opts.totalRows} rows (consider grid=auto for fewer dead nodes).`;
104
94
  }
105
95
  data.message = msg;
106
96
  data.suggested_next_step = extras.suggested_next_step;
@@ -205,16 +195,6 @@ export async function runTrain(action, args) {
205
195
  typeof transforms === "object" &&
206
196
  Object.keys(transforms).length > 0;
207
197
  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
- });
218
198
  const resultsHint = action === "impute" ? "view the map and imputed.csv"
219
199
  : "view the map and metrics";
220
200
  const variantPrefix = action === "siom_map" ? "variant=siom"
@@ -233,7 +213,6 @@ export async function runTrain(action, args) {
233
213
  resultsHint,
234
214
  impute: action === "impute",
235
215
  gridLargeForRows: !!gridLargeForRows,
236
- guardrailLines,
237
216
  });
238
217
  }
239
218
  if (action === "floop_siom") {