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