@barivia/barsom-mcp 0.23.8 → 0.23.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 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);
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 r,registerAppResource as s,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 m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as _}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as h}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingMonitorTool as P,TRAINING_MONITOR_URI as A}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as S}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:d,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics via the Barivia API.\n\n## Bootstrap (do this first)\n\nCall `guide_barsom_workflow` for the plan-scoped tool map, training modes, async rules, and SOP (API when online). Call `training_guidance` / `prepare_training` for parameter and quality hints before guessing knobs.\n\n## Workflow (short)\n\n`datasets(upload)` → preview/analyze → `train(action=...)` (only modes your plan allows) → poll `jobs(status)` every 10–15s → `results(get)` then optional `results_explorer`. Scoring and frozen-map ops live in `inference`.\n\n## Tool taxonomy (one tool per stage)\n\n| Stage | Tool(s) |\n|-------|---------|\n| Data | `datasets` |\n| Train submit | `train` (map / siom_map / impute / floop_siom where entitled) |\n| Lifecycle | `jobs` (status/list/compare/cancel/delete — no train/score) |\n| Artifacts | `results` (get/export/download/recolor) |\n| Frozen-map ops | `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report) |\n| Account | `account` (status/inventory/health) |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` |\n| Explore (UI) | `results_explorer`, `training_monitor` |\n| Feedback | `send_feedback` (after user agrees) |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics across **training runs**. `inference(compare)` = density-diff vs a **second dataset**.\n- Post-training analysis is `inference`, not `results` (presentation/artifacts only).\n- Project columns → `inference(project_columns)`; formula → `datasets(add_expression, project_onto_job=...)`.\n\n## Async & constraints\n\n- Training returns `job_id` immediately — poll `jobs(status)` every 10–15s; running is not failed.\n- **Do not auto-subset** unless the user asks. Prefer `results(get, figures="none")` for sweeps.\n- Column names are case-sensitive. After recolor/transition_flow/project_columns, use the **new** job_id when returned.\n- On `api_unreachable`: one calm sentence, honor `retry_after_sec`; use `account(health)`. Timeouts are not auto-retried.\n\n## UI delivery\n\nTool results lead with a localhost viz URL and a structured `ui_delivery` block — follow `hint_for_agent`. Env: `BARIVIA_UI_DELIVERY`, `BARIVIA_VIZ_PORT`. Details: `account(status)`.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,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>"}]}}),x(T),w(T),P(T),f(T),_(T),g(T,h),b(T),v(T),j(T),k(T),I(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 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")}}]})),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 S(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const C=new t;(async function(){try{const e=await a(l,c,p);m(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=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(C)})().catch(console.error);
@@ -1,15 +1,9 @@
1
1
  import { apiCall } from "./shared.js";
2
- export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07f-offline";
2
+ export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07h-offline";
3
3
  const FALLBACK_LINES = [
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 — 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.",
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.',
8
- "Choose the training path:",
9
- '- train(action=map, dataset_id="{id}", ...) for complete-case data',
10
- '- train(action=impute, dataset_id="{id}", ...) for sparse matrices',
11
- '- train(action=siom_map, dataset_id="{id}", ...) for a fixed-grid SIOM',
12
- '- train(action=floop_siom, dataset_id="{id}", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain)',
4
+ "API unreachable cannot load the tier-scoped prepare_training checklist.",
5
+ "Retry when BARIVIA_API_KEY / BARIVIA_API_URL reconnect. Meanwhile: datasets(preview/analyze) on dataset_id={id} is still useful locally if those tools work; do not auto-subset unless the user asks.",
6
+ "When the API returns, call prepare_training again (or guide_barsom_workflow / training_guidance) for plan-scoped train actions and parameter hints.",
13
7
  ];
14
8
  /** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
15
9
  export async function resolvePrepareTrainingPrompt(datasetId) {
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.8";
42
+ export const CLIENT_VERSION = "0.23.10";
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). */
@@ -8,6 +8,10 @@ import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, s
8
8
  import { GZIP_UPLOAD_HINT } from "../job_status_format.js";
9
9
  import { formatDatasetInventoryLine, formatTags } from "../inventory_format.js";
10
10
  import { PERIODICITY_ROW_ORDER_NOTE, SMALL_N_SOFT, buildCpuScaleGuidanceLines, buildSmallNGuardrailLines, } from "../training_scale_guidance.js";
11
+ /** Percent-encode header values so Node fetch accepts Unicode (ByteString limit). */
12
+ function encodeHeaderValue(s) {
13
+ return encodeURIComponent(s);
14
+ }
11
15
  /**
12
16
  * Normalize a nullable string field from the API. Returns "" for absent values,
13
17
  * empty strings, and the literal SQL/serialization sentinels "missing"/"null"
@@ -368,15 +372,15 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
368
372
  if (isGzipInput) {
369
373
  const gzBytes = await fs.readFile(resolved);
370
374
  const gzHeaders = {
371
- "X-Dataset-Name": name,
375
+ "X-Dataset-Name": encodeHeaderValue(name),
372
376
  "Content-Type": uploadContentType,
373
377
  "Content-Encoding": "gzip",
374
378
  "Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
375
379
  };
376
380
  if (description !== undefined)
377
- gzHeaders["X-Dataset-Description"] = description;
381
+ gzHeaders["X-Dataset-Description"] = encodeHeaderValue(description);
378
382
  if (tags !== undefined)
379
- gzHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
383
+ gzHeaders["X-Dataset-Tags"] = encodeHeaderValue(JSON.stringify(tags));
380
384
  const data = await awaitDatasetStageIfNeeded((await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS)));
381
385
  const gid = data.id ?? data.dataset_id;
382
386
  if (gid != null)
@@ -395,16 +399,16 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
395
399
  // cap) small; the API transparently decompresses. Small bodies stay plain.
396
400
  const GZIP_THRESHOLD = 1024 * 1024; // 1 MB
397
401
  const uploadHeaders = {
398
- "X-Dataset-Name": name,
402
+ "X-Dataset-Name": encodeHeaderValue(name),
399
403
  "Content-Type": uploadContentType,
400
404
  // Deterministic key so a timed-out retry of the SAME upload reconciles to
401
405
  // the original dataset server-side instead of creating a duplicate.
402
406
  "Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
403
407
  };
404
408
  if (description !== undefined)
405
- uploadHeaders["X-Dataset-Description"] = description;
409
+ uploadHeaders["X-Dataset-Description"] = encodeHeaderValue(description);
406
410
  if (tags !== undefined)
407
- uploadHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
411
+ uploadHeaders["X-Dataset-Tags"] = encodeHeaderValue(JSON.stringify(tags));
408
412
  let uploadBody = body;
409
413
  if (Buffer.byteLength(body, "utf-8") > GZIP_THRESHOLD) {
410
414
  uploadBody = gzipSync(Buffer.from(body, "utf-8"));
@@ -53,42 +53,20 @@ export async function runBatchPredict(args) {
53
53
  };
54
54
  }
55
55
  export function registerInferenceTool(server) {
56
- registerAuditedTool(server, "inference", `Use a trained map as a persistent inference artifact — score data, annotate the source CSV, compare datasets, project columns, or generate a report manifest.
56
+ registerAuditedTool(server, "inference", `Use a trained map as a persistent inference artifact — score data, annotate CSVs, compare cohorts, project columns, or fetch a report manifest. Domain details (impute pooling, regimes, FLooP limits): **guide_barsom_workflow** / **training_guidance**.
57
57
 
58
- | Action | Use when | Timing |
59
- |--------|----------|--------|
60
- | predict | Scoring rows against the trained map (new data OR the training set itself) | 5–120s |
61
- | batch_predict | Fan out many predict jobs at once against one trained map (several datasets/row-sets in one call) | submits N jobs |
62
- | impute_column | Fill a numeric column (not used in training) by pooling observed values on the BMU plus topology neighbors (typically 6 on hex; periodic maps wrap) | 5–120s |
63
- | compare | Comparing hit distributions of a second dataset against training (drift, A/B); needs one trained job_id + dataset B. NOT jobs(compare), which tabulates metrics across multiple training runs. | 30–120s |
64
- | project_columns | Project one or more dataset columns onto the map (component planes); dataset can be training set or partial-feature set | 10–90s |
65
- | transition_flow | Temporal state-transition arrows on the trained map. Only meaningful when the scored rows are in chronological order. | 30–60s |
66
- | report | Get a report manifest (artifact keys + URLs) to build your own report in Quarto/Notebook/script | Immediate (sync) |
58
+ | Action | Use when |
59
+ |--------|----------|
60
+ | predict | Score rows (new data or training set); output compact|annotated |
61
+ | batch_predict | Fan out many predict jobs against one map |
62
+ | impute_column | Fill a non-training numeric column on a frozen map (not sparse train+impute) |
63
+ | compare | Density-diff of dataset B vs training on ONE map NOT jobs(compare) |
64
+ | project_columns | Project dataset columns onto the map (component planes) |
65
+ | transition_flow | Temporal arrows; time-ordered rows only |
66
+ | report | Sync report manifest for custom notebooks |
67
67
 
68
- Sync/async: predict, impute_column, compare, and transition_flow are async jobs that the proxy auto-polls and usually returns on completion; if one returns a job_id instead (e.g. timeout), poll jobs(action=status, job_id=...) then results(action=download, job_id=...). batch_predict does NOT auto-poll it returns a list of submitted job_ids; poll each with jobs(action=status).
69
- Artifacts: When complete, use results(action=download, job_id=<returned_job_id>) to get: predict (output="compact")predictions.csv; predict (output="annotated") → annotated.csv; impute_column imputed.csv; compare → density-diff figure (e.g. density_diff.png).
70
- report is the only synchronous inference action — returns manifest immediately; no job to poll.
71
- NOT FOR: Retraining or changing the map — all actions treat the trained map as frozen.
72
- ESCALATION: If any action returns "missing column", verify column names with datasets(action=preview). Column names are case-sensitive and must match the training feature set exactly.
73
-
74
- action=predict: Score rows against the trained map.
75
- Inputs (one of):
76
- - dataset_id (default = parent training job's dataset). Use a different dataset for new/unseen data; omit it (or pass the training dataset_id) to score the training set itself.
77
- - rows (≤500 inline). Always treated as new data.
78
- Output style (output param, default "compact"):
79
- - "compact" → predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id [, quantization_error, potential_anomaly]).
80
- - "annotated" → annotated.csv (full source CSV with bmu_x, bmu_y, bmu_node_index, cluster_id appended). Requires a dataset (no inline rows).
81
- Regime (auto): if the resolved dataset is the parent training dataset, regime="training" and QE / qe_p95 / potential_anomaly are omitted (QE on training data is fitting error, not generalisation — use a held-out dataset for quality). Otherwise regime="new" and full QE columns are returned.
82
- Schema rules: prefer dataset_id for scoring. Inline rows may use raw cyclic keys (e.g. hour) — the API/worker expands them from training config to hour_cos/hour_sin; pre-expanded cos/sin columns also work. For a single inline row, the proxy uses the stateless model endpoint (same cyclic expansion). Raw categorical strings are allowed for baseline categorical_features models on that single-row path.
83
- Routing: prefer dataset_id for many rows or whenever the map uses irregular SIOM / GeneralTopology layouts — the async worker path is the supported batch scorer. Single-row rows take a fast stateless path that may return invalid_inference_input on some topologies; if so, retry with dataset_id (a one-row dataset is fine). FLooP-SIOM: use dataset_id predict first.
84
- When the scored set has at most ${PREDICT_PREVIEW_ROW_CAP} rows, completed responses include a short per-line preview in the tool text for chat agents.
85
-
86
- action=impute_column: Map-local imputation on the frozen trained map (read-only; not a held-out validity claim). Requires dataset_id + target_column; the dataset must contain all training features (same names + cyclic expansion as predict) plus the target column. target_column must NOT have been a training feature. Pools finite target values from rows on this row's BMU and its topology neighbors (BMU + neighbors; ~7 on hex interior, fewer on non-periodic borders), neighbourhood-distance-weighted by default (weighting="uniform" for a flat pool); excludes the current row. only_missing (default true) keeps observed values; impute_aggregation mean|median. Optional cv_folds (2-20) → quality.csv (held-out MAE/RMSE/R2). target_column_kind: categorical (mode) / cumulative (warns). Output imputed.csv columns: row_id, target_original, target_imputed, impute_source (observed|imputed|insufficient_data), bmu_node_index, n_patch_nodes, n_pool_rows, pool_std, pool_p5, pool_p95.
87
-
88
- action=compare: density-diff of a cohort dataset against the training set on ONE trained map — NOT jobs(action=compare) (which tabulates QE/TE/EV metrics across multiple training runs to pick the best). dataset_id must refer to a dataset with the same feature set as training (same column names and preprocessing, including cyclic expansion). A = training dataset; B = cohort to compare. Density-diff: positive = B gained vs A; negative = A had more. Returns density-diff heatmap (e.g. density_diff.png).
89
- action=project_columns: Project one or more columns from a dataset onto the trained map. Pass dataset_id (the dataset containing the columns) and columns (array of column names). Uses cached BMUs when dataset is the training set; supports partial-feature mapping when dataset has only a subset of training features. Returns one component plane image per column. Get files via results(action=download, job_id=<returned_job_id>).
90
- action=transition_flow: Temporal state-transition arrows drawn on the trained map grid (U-matrix background). Post-training analysis of how observations move across the map over time — use ONLY when rows are in chronological order; on unordered data the arrows are meaningless. You pass the training job_id; the API returns a NEW result job_id (use it for results(action=get/download)). lag=1 (default) = consecutive rows; lag=N = N-step horizon (e.g. 24 for daily cycles in hourly data). min_transitions filters noisy arrows (increase for large datasets). Auto-polled.
91
- action=report: Returns a report manifest for the given job_id (job must be completed). Includes figure_manifest (logical names → filenames), download_urls for all artifacts, cluster_summary when available, and summary metrics. Stakeholder report PDF (if generated) is available via results(action=download, job_id=<training_job_id>), filename e.g. report.pdf.`, {
68
+ ASYNC: predict / impute_column / compare / transition_flow auto-poll; if you get a job_id, poll jobs(status) then results(download). batch_predict returns job_ids (poll each). Prefer dataset_id for batch / irregular topologies; single-row rows may need retry with dataset_id.
69
+ NOT FOR: Retraining. ESCALATION: missing columndatasets(preview); column names are case-sensitive.`, {
92
70
  action: z
93
71
  .enum(["predict", "batch_predict", "impute_column", "compare", "project_columns", "transition_flow", "report"])
94
72
  .describe("predict: score rows; batch_predict: fan out many predict jobs at once; impute_column: topology-neighbor pool imputation for a column not in training; compare: drift/cohort diff heatmap; project_columns: project dataset columns onto map; transition_flow: temporal state-transition arrows on the map (time-ordered rows only); report: manifest of primitives for custom report."),
@@ -23,8 +23,7 @@ action=get: Returns text summary with quality metrics and inline images.
23
23
  - figures: omit = combined only. Prefer string "all" / "none" (not an array). "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) or download filenames. Hosts that stringify arrays are coerced. 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
- - After showing results, guide the user: QE interpretation, whether to retrain, which features to explore.
27
- - METRIC INTERPRETATION: QE lower=better (<1.5 good) | TE<0.1 good | Explained variance>0.7 good | Silhouette higher=better | Davies-Bouldin lower=better.
26
+ - After showing results, guide the user using **training_guidance** metric_interpretation / quality_targets (whether to retrain, which features to explore).
28
27
 
29
28
  action=export: Structured data exports (not the same as download). Use export_type= to choose what to export.
30
29
  - export_type=training_log: learning curve sparklines + plot. Diagnose convergence/plateau/divergence. Prefer this over download for the training log.
@@ -9,21 +9,15 @@ export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Ret
9
9
  | Action | Use when |
10
10
  |--------|----------|
11
11
  | map | Standard SOM on a fixed hex grid — complete-case numeric data (no NaNs in training columns). |
12
- | siom_map | Self-interacting map — same grid flow plus SIOM coverage control (gamma, siom_decay, penalty) to avoid dead nodes. |
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. |
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). |
12
+ | siom_map | Fixed-grid SIOM (coverage control) requires SIOM entitlement. |
13
+ | impute | Sparse training matrix (map + dense imputed.csv) requires SIOM entitlement. Prefer inference(impute_column) when you already have a complete-case map and need one held-out column. |
14
+ | floop_siom | Growing node-budget manifold (not a fixed grid) requires FLooP entitlement. |
15
15
 
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**.
16
+ Call **guide_barsom_workflow**, **training_guidance**, and/or the **prepare_training** prompt for plan-scoped parameter details, timing heuristics, quality.csv semantics, SIOM/FLooP knobs, and impute constraints do not guess. Backend: "cpu" | "gpu" | "gpu_graphs" (on CPU-only hosts pass backend=cpu).
17
17
 
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.
19
-
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
-
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).
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.
24
- NOT FOR: lifecycle (status/list/compare/cancel/delete) → use jobs; scoring data → use inference.
25
-
26
- Rank transform (map/siom/floop only): training-only — batch predict, project_columns, and impute_column cannot score rank-transformed columns.`;
18
+ ASYNC: every action returns a job_id immediately. Poll jobs(action=status) every 10–15s (not faster). When completed, call results(action=get). Timing and quality heuristics: **training_guidance**.
19
+ NOT FOR: lifecycle → jobs; scoring → inference.
20
+ Rank transform (map/siom/floop only) is train-onlysee training_guidance.`;
27
21
  function assertImputeTrainingArgs(args) {
28
22
  assertValidNormalizationArgs(args);
29
23
  const normalize = args.normalize;
@@ -46,8 +46,8 @@ 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 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
- grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
49
+ .describe("Grid width. Omit grid_x and grid_y (and preset) for API auto-size — see training_guidance."),
50
+ grid_y: z.number().int().optional().describe("Grid height. See grid_x / training_guidance."),
51
51
  epochs: z.preprocess((v) => {
52
52
  if (v === undefined || v === null)
53
53
  return v;
@@ -69,7 +69,7 @@ export const TRAINING_INPUT_SCHEMA = {
69
69
  period: z.number(),
70
70
  })).optional(),
71
71
  cyclic_pairs: z.array(z.array(z.string()).min(2).max(2)).optional()
72
- .describe("impute only: [[cos_col, sin_col], ...] for circular prototype means + atomic chord BMU. Those columns get circular quality metrics only (no Euclidean R²)."),
72
+ .describe("impute only: [[cos_col, sin_col], ...] see training_guidance / prepare_training for circular metrics."),
73
73
  temporal_features: z.array(z.object({
74
74
  columns: z.array(z.string()),
75
75
  format: z.string(),
@@ -79,7 +79,7 @@ export const TRAINING_INPUT_SCHEMA = {
79
79
  })).optional(),
80
80
  feature_weights: z.record(z.number()).optional(),
81
81
  transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional()
82
- .describe("Per-column transform before normalization. rank is train-only (no batch predict/project_columns). Not for train(impute)."),
82
+ .describe("Per-column transform before normalization see training_guidance for train-only / impute constraints."),
83
83
  auto_log_transforms: z.boolean().optional().default(false),
84
84
  time_delay_embeddings: z.array(z.object({
85
85
  feature: z.string(),
@@ -92,7 +92,7 @@ export const TRAINING_INPUT_SCHEMA = {
92
92
  })).optional(),
93
93
  normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal", "sepd"]), z.array(z.string())]).optional().default("auto"),
94
94
  normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional()
95
- .describe("Per-feature normalization override. Use MAD for heavy tails; SEPD for skewed financial/policy columns (see training_guidance). Default auto=zscore on non-cyclics. train(impute) rejects sepd."),
95
+ .describe("Per-feature normalization override see training_guidance and GET /v1/docs/normalization."),
96
96
  sigma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
97
97
  learning_rate: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.union([z.number(), z.object({
98
98
  ordering: z.tuple([z.number(), z.number()]),
@@ -100,7 +100,7 @@ export const TRAINING_INPUT_SCHEMA = {
100
100
  })]).optional()),
101
101
  batch_size: z.number().int().optional(),
102
102
  te_panel_size: z.number().int().optional()
103
- .describe("Fixed evaluation-panel size for live topographic error during training (default clamp(grid_nodes*6, 500, 10000))"),
103
+ .describe("Fixed evaluation-panel size for live topographic error see training_guidance"),
104
104
  quality_metrics: z.union([z.enum(["fast", "standard", "full"]), z.array(z.string())]).optional(),
105
105
  backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
106
106
  output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
@@ -108,30 +108,30 @@ export const TRAINING_INPUT_SCHEMA = {
108
108
  colormap: z.string().optional(),
109
109
  row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
110
110
  cv_folds: z.number().int().min(0).max(20).optional()
111
- .describe("impute only: quality.csv — linear cols: kfold_holdout_pool + resubstitution_optimistic; cyclic_pairs cols: circular metrics only (no Euclidean R²). Aggregate MAE/RMSE excludes cyclic columns. 0=off, default 5."),
111
+ .describe("impute only: quality.csv folds see training_guidance / prepare_training (default 5; 0=off)"),
112
112
  viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
113
- .describe("Visualization density; auto-capped when feature count > 40"),
113
+ .describe("Visualization density see training_guidance"),
114
114
  viz_top_components: z.number().int().min(0).max(64).optional()
115
- .describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
115
+ .describe("With viz_mode=summary_plus_top: top-N component maps"),
116
116
  viz_upload_cyclic_components: z.boolean().optional()
117
- .describe("When true with individual component PNGs, also publish cos/sin cyclic expansion planes (default false; recovered hour/month panels stay in combined.png)"),
117
+ .describe("Publish cos/sin cyclic expansion planes (default false)"),
118
118
  emit_cell_uncertainty: z.boolean().optional()
119
- .describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
119
+ .describe("impute: write imputation_uncertainty.csv see training_guidance"),
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). Prefer γ∈[0.5,1.5] on large n; γ>1.5 often diminishing returns."),
121
+ .describe("siom_map / impute SIOM γ see training_guidance (siom_gamma)"),
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 (0 = constant γ). Pattern: start strict, relax late (e.g. 1.5→0.2)."),
123
+ .describe("siom_map / impute SIOM final γ see training_guidance (siom_gamma_f)"),
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). Avoid pairing high γ with aggressive decay (e.g. 0.05)."),
125
+ .describe("siom_map / impute SIOM occupation decay see training_guidance"),
126
126
  siom_penalty: z.enum(["linear", "log", "exp"]).optional()
127
- .describe("siom_map / impute SIOM: occupation penalty shape (linear default; prefer exp over log log often worsens TE)"),
127
+ .describe("siom_map / impute SIOM penalty shape — see training_guidance"),
128
128
  penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
129
- .describe("siom_map / impute SIOM: scaling α inside log/exp penalty"),
129
+ .describe("siom_map / impute SIOM penalty α see training_guidance"),
130
130
  reset_per_epoch: z.boolean().optional()
131
- .describe("siom_map / impute SIOM: reset occupation μ each epoch (default false; avoid with high γ can collapse EV)"),
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
- 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
- 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)"),
131
+ .describe("siom_map / impute SIOM reset occupation each epoch — see training_guidance"),
132
+ siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional().describe("siom_map feature geometry see training_guidance"),
133
+ siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional().describe("siom_map QE backend see training_guidance"),
134
+ siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional().describe("siom_map GPU QE batch size"),
135
135
  topology: z.enum(["free", "chain"]).optional().default("free"),
136
136
  max_nodes: z.number().int().min(2).optional(),
137
137
  effort: z.enum(["quick", "standard", "thorough"]).optional().default("standard"),
@@ -40,19 +40,19 @@ export function buildSmallNGuardrailLines(opts) {
40
40
  ? gridX * gridY
41
41
  : presetGridCells(preset);
42
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.`);
43
+ lines.push(`JUMPBACK — small-n (${n} rows): prefer auto grid ≈ ${autoSide}×${autoSide} (omit grid_x/grid_y); oversized presets leave dead nodes. See training_guidance / prepare_training. Check hit_stats.active_node_fraction / TE before narrating insights.`);
44
44
  }
45
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.`);
46
+ lines.push(`Warning — small-n (${n} rows < 1000): auto grid ≈ ${autoSide}×${autoSide}; omit grid_x/grid_y or shrink oversized presets. See training_guidance. Verify active_node_fraction after train.`);
47
47
  }
48
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.`);
49
+ lines.push(`Grid risk: ${cells} cells vs ${n} rows prefer auto (~${autoSide}×${autoSide}) or a smaller grid. See training_guidance.`);
50
50
  }
51
51
  if (nFeatures !== undefined &&
52
52
  nFeatures > 0 &&
53
53
  cells !== null &&
54
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.`);
55
+ lines.push(`Capacity risk: ${nFeatures} features × ${cells} cells vs ${n} rows shrink grid and/or drop correlated columns (analyze). See training_guidance.`);
56
56
  }
57
57
  return lines;
58
58
  }
@@ -62,11 +62,11 @@ export function buildCpuScaleGuidanceLines(n) {
62
62
  return [];
63
63
  if (n <= SAMPLE_N_SUGGEST_THRESHOLD) {
64
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.`,
65
+ `Scale: n=${n.toLocaleString()} — do NOT auto-subset; train the full dataset_id unless the user asks. Timing/preset guidance: training_guidance (scale_cpu_quick).`,
66
66
  ];
67
67
  }
68
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.`,
69
+ `Scale: n=${n.toLocaleString()} > 100k — full-table train is supported; suggest sample_n only if the user asks. Never auto-subset silently. See training_guidance.`,
70
70
  ];
71
71
  }
72
72
  /** Periodicity caveat for analyze next_steps (row-order dependent). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barsom-mcp",
3
- "version": "0.23.8",
3
+ "version": "0.23.10",
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",