@barivia/barsom-mcp 0.21.0 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { registerAuditedTool } from "../audit.js";
3
3
  import { apiCall, textResult } from "../shared.js";
4
4
  import { buildTrainSubmitExtras } from "../train_submit_message.js";
5
- import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, } from "./training_core.js";
5
+ import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
6
6
  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.
7
7
 
8
8
  | Action | Use when |
@@ -18,12 +18,13 @@ Parameter details (grid, epochs, batch, model, presets, normalize, categorical_f
18
18
 
19
19
  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=...).
20
20
 
21
- 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, 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 with two labeled numbers per column (kfold_holdout_pool = honest held-out; resubstitution_optimistic = in-sample, optimistic). Optional SIOM coverage knobs: gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch (same names as siom_map). Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv, summary.siom when SIOM path runs. A completed impute job_id is a valid parent for inference(impute_column).
21
+ action=impute: use when many cells are missing across training columns; use inference(impute_column) instead when you already have a complete-case map and need to fill one held-out column. Supports transforms (log/sqrt/etc., not rank), temporal_features, cyclic_features, optional cyclic_pairs=[[cos,sin],...] (circular prototype refresh + atomic chord BMU; those columns get circular quality metrics only — never quote Euclidean R² on cos/sin), and normalization_methods (zscore/mad/sigmoidal/none — not sepd). Does not support auto_log_transforms, rank transforms, or normalize=sepd. Defaults model=auto (SIOM missSOM on som_siom+ plans, else plain missSOM), cv_folds=5 → quality.csv (linear: kfold_holdout_pool + resubstitution_optimistic; cyclic pairs: kfold_holdout_circular). Aggregate MAE/RMSE excludes cyclic columns. Do not treat k-fold alone as true missing-cell ground truth. Optional SIOM knobs: gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv (pool_std = neighbourhood dispersion). A completed impute job_id is a valid parent for inference(impute_column).
22
22
  action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
23
23
  NOT FOR: lifecycle (status/list/compare/cancel/delete) → use jobs; scoring data → use inference.
24
24
 
25
25
  Rank transform (map/siom/floop only): training-only — batch predict, project_columns, and impute_column cannot score rank-transformed columns.`;
26
26
  function assertImputeTrainingArgs(args) {
27
+ assertValidNormalizationArgs(args);
27
28
  const normalize = args.normalize;
28
29
  if (normalize === "sepd") {
29
30
  throw new Error("train(impute) does not support normalize=sepd");
@@ -48,6 +49,48 @@ function assertImputeTrainingArgs(args) {
48
49
  throw new Error("train(impute) does not support auto_log_transforms yet.");
49
50
  }
50
51
  }
52
+ /** Shared POST /v1/jobs + train submit message extras (map/siom/impute and floop). */
53
+ async function submitTrainJob(opts) {
54
+ const submitBody = { dataset_id: opts.dataset_id, params: opts.params };
55
+ if (opts.label && opts.label.trim() !== "")
56
+ submitBody.label = opts.label;
57
+ const data = (await apiCall("POST", "/v1/jobs", submitBody));
58
+ const newJobId = data.id;
59
+ data.effective_params = `${opts.variantPrefix}, ${opts.paramSummary}`;
60
+ data.results_hint = opts.resultsHint;
61
+ if (opts.gridLargeForRows) {
62
+ data.grid_auto_warning = true;
63
+ }
64
+ const extras = buildTrainSubmitExtras({
65
+ newJobId,
66
+ totalRows: opts.totalRows,
67
+ hasTransforms: opts.hasTransforms,
68
+ prepareJobId: data.prepare_job_id ?? null,
69
+ variantPrefix: opts.variantPrefix,
70
+ paramSummary: opts.paramSummary,
71
+ impute: opts.impute,
72
+ resultsHint: opts.resultsHint,
73
+ });
74
+ let msg = extras.message;
75
+ try {
76
+ const sys = (await apiCall("GET", "/v1/system/info"));
77
+ const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
78
+ const totalEta = Number(sys.training_time_estimates_seconds?.total ??
79
+ (sys.gpu_available ? 45 : 120));
80
+ const waitMinutes = Math.round((pending * totalEta) / 60);
81
+ if (waitMinutes > 1)
82
+ msg = `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. ${msg}`;
83
+ }
84
+ catch {
85
+ /* ignore queue enrichment */
86
+ }
87
+ if (opts.gridLargeForRows) {
88
+ msg += ` Note: Grid may be large for ${opts.totalRows} rows (consider grid=auto for fewer dead nodes).`;
89
+ }
90
+ data.message = msg;
91
+ data.suggested_next_step = extras.suggested_next_step;
92
+ return textResult(data);
93
+ }
51
94
  /**
52
95
  * Core training dispatcher. Shared by the `train` tool and the deprecated
53
96
  * `jobs(train_*)` wrappers. Every branch posts to the same /v1/jobs route.
@@ -55,7 +98,7 @@ function assertImputeTrainingArgs(args) {
55
98
  export async function runTrain(action, args) {
56
99
  const dataset_id = args.dataset_id;
57
100
  if (action === "map" || action === "siom_map" || action === "impute") {
58
- const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
101
+ const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
59
102
  let PRESETS = {};
60
103
  try {
61
104
  PRESETS = await fetchTrainingPresets();
@@ -72,11 +115,14 @@ export async function runTrain(action, args) {
72
115
  if (action === "impute") {
73
116
  assertImputeTrainingArgs(args);
74
117
  }
118
+ else {
119
+ assertValidNormalizationArgs(args);
120
+ }
75
121
  const { params, paramSummary, effectiveGrid } = buildTrainMapParams({
76
- preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features,
122
+ preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs,
77
123
  temporal_features, feature_weights, transforms, auto_log_transforms,
78
124
  time_delay_embeddings, categorical_features,
79
- normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend,
125
+ normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend,
80
126
  output_format, output_dpi, colormap, row_range,
81
127
  }, PRESETS);
82
128
  if (action === "impute") {
@@ -134,62 +180,34 @@ export async function runTrain(action, args) {
134
180
  totalRows = Number(preview?.total_rows ?? 0);
135
181
  }
136
182
  catch { /* ignore */ }
137
- const submitBody = { dataset_id, params };
138
- if (label && label.trim() !== "")
139
- submitBody.label = label;
140
- const data = (await apiCall("POST", "/v1/jobs", submitBody));
141
- const newJobId = data.id;
142
- const variantPrefix = action === "siom_map" ? "variant=siom"
143
- : action === "impute" ? "variant=train_impute"
144
- : "variant=som";
145
- data.effective_params = `${variantPrefix}, ${paramSummary}`;
146
183
  const hasTransforms = transforms != null &&
147
184
  typeof transforms === "object" &&
148
185
  Object.keys(transforms).length > 0;
149
- const extras = buildTrainSubmitExtras({
150
- newJobId,
186
+ const gridLargeForRows = effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75;
187
+ const resultsHint = action === "impute" ? "view the map and imputed.csv"
188
+ : "view the map and metrics";
189
+ const variantPrefix = action === "siom_map" ? "variant=siom"
190
+ : action === "impute" ? "variant=train_impute"
191
+ : "variant=som";
192
+ return submitTrainJob({
193
+ dataset_id,
194
+ params,
195
+ label,
151
196
  totalRows,
152
197
  hasTransforms,
153
- prepareJobId: data.prepare_job_id ?? null,
154
198
  variantPrefix,
155
199
  paramSummary,
200
+ resultsHint,
156
201
  impute: action === "impute",
202
+ gridLargeForRows: !!gridLargeForRows,
157
203
  });
158
- try {
159
- const sys = (await apiCall("GET", "/v1/system/info"));
160
- const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
161
- const totalEta = Number(sys.training_time_estimates_seconds?.total ??
162
- (sys.gpu_available ? 45 : 120));
163
- const waitMinutes = Math.round((pending * totalEta) / 60);
164
- let msg = extras.message;
165
- if (waitMinutes > 1)
166
- msg = `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. ${msg}`;
167
- if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
168
- msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
169
- }
170
- data.message = msg;
171
- data.suggested_next_step = extras.suggested_next_step;
172
- }
173
- catch {
174
- let msg = extras.message;
175
- if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
176
- msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
177
- }
178
- data.message = msg;
179
- data.suggested_next_step = extras.suggested_next_step;
180
- }
181
- return textResult(data);
182
204
  }
183
205
  if (action === "floop_siom") {
184
206
  if (!dataset_id)
185
207
  throw new Error("train(floop_siom) requires dataset_id");
208
+ assertValidNormalizationArgs(args);
186
209
  const { transforms, label } = args;
187
210
  const { params, paramSummary } = buildFloopParams(args);
188
- const submitBody = { dataset_id, params };
189
- if (label && label.trim() !== "")
190
- submitBody.label = label;
191
- const data = (await apiCall("POST", "/v1/jobs", submitBody));
192
- const newJobId = data.id;
193
211
  let totalRows = 0;
194
212
  try {
195
213
  const preview = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
@@ -197,19 +215,16 @@ export async function runTrain(action, args) {
197
215
  }
198
216
  catch { /* ignore */ }
199
217
  const hasTransforms = transforms != null && typeof transforms === "object" && Object.keys(transforms).length > 0;
200
- data.effective_params = paramSummary;
201
- const extras = buildTrainSubmitExtras({
202
- newJobId,
218
+ return submitTrainJob({
219
+ dataset_id,
220
+ params,
221
+ label,
203
222
  totalRows,
204
223
  hasTransforms,
205
- prepareJobId: data.prepare_job_id ?? null,
206
224
  variantPrefix: "variant=floop",
207
225
  paramSummary,
208
226
  resultsHint: "view FLooP-SIOM figures and metrics",
209
227
  });
210
- data.message = extras.message;
211
- data.suggested_next_step = extras.suggested_next_step;
212
- return textResult(data);
213
228
  }
214
229
  throw new Error("Invalid train action");
215
230
  }
@@ -4,11 +4,30 @@
4
4
  * the `train` tool reuses a single set of definitions (no drift).
5
5
  */
6
6
  import { z } from "zod";
7
- import { apiCall } from "../shared.js";
7
+ import { apiCall, resolveOutputDpi } from "../shared.js";
8
8
  export async function fetchTrainingPresets() {
9
9
  const configData = (await apiCall("GET", "/v1/training/config"));
10
10
  return configData?.presets || {};
11
11
  }
12
+ const ALLOWED_NORM_METHODS = new Set(["zscore", "mad", "sigmoidal", "sepd", "none"]);
13
+ const ALLOWED_NORMALIZE_GLOBALS = new Set(["all", "auto", "mad", "sigmoidal", "sepd", "zscore"]);
14
+ /**
15
+ * Fail closed on unknown normalize / normalization_methods values before POST /v1/jobs.
16
+ */
17
+ export function assertValidNormalizationArgs(args) {
18
+ const normalize = args.normalize;
19
+ if (typeof normalize === "string" && normalize.length > 0 && !ALLOWED_NORMALIZE_GLOBALS.has(normalize)) {
20
+ throw new Error(`Unknown normalize=${normalize}. Use auto|mad|sigmoidal|sepd|zscore|all, or pass a string[] of feature names.`);
21
+ }
22
+ const nm = args.normalization_methods;
23
+ if (nm && typeof nm === "object" && !Array.isArray(nm)) {
24
+ for (const [col, method] of Object.entries(nm)) {
25
+ if (!ALLOWED_NORM_METHODS.has(String(method))) {
26
+ throw new Error(`Unknown normalization_methods.${col}=${method}. Allowed: zscore|mad|sigmoidal|sepd|none.`);
27
+ }
28
+ }
29
+ }
30
+ }
12
31
  /**
13
32
  * Fetch presets + the plan-scoped allowed_job_types in one /v1/training/config call.
14
33
  * Used to scope train-action selection to entitled modes.
@@ -49,6 +68,8 @@ export const TRAINING_INPUT_SCHEMA = {
49
68
  feature: z.string(),
50
69
  period: z.number(),
51
70
  })).optional(),
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²)."),
52
73
  temporal_features: z.array(z.object({
53
74
  columns: z.array(z.string()),
54
75
  format: z.string(),
@@ -87,13 +108,13 @@ export const TRAINING_INPUT_SCHEMA = {
87
108
  colormap: z.string().optional(),
88
109
  row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
89
110
  cv_folds: z.number().int().min(0).max(20).optional()
90
- .describe("impute only: per-column imputation quality → quality.csv with two labeled methods (kfold_holdout_pool = held-out; resubstitution_optimistic = in-sample). 0=off, default 5."),
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."),
91
112
  viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
92
113
  .describe("Visualization density; auto-capped when feature count > 40"),
93
114
  viz_top_components: z.number().int().min(0).max(64).optional()
94
115
  .describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
95
116
  emit_cell_uncertainty: z.boolean().optional()
96
- .describe("impute: write imputation_uncertainty.csv (pool_std per imputed cell)"),
117
+ .describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
97
118
  gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
98
119
  .describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5 on worker)"),
99
120
  gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
@@ -159,9 +180,8 @@ export function buildFloopParams(args) {
159
180
  params.auto_log_transforms = auto_log_transforms;
160
181
  if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
161
182
  params.row_range = row_range;
162
- const dpiMap = { standard: 1, retina: 2, print: 4 };
163
183
  if (output_dpi && output_dpi !== "retina")
164
- params.output_dpi = dpiMap[output_dpi] ?? 2;
184
+ params.output_dpi = resolveOutputDpi(output_dpi);
165
185
  if (colormap)
166
186
  params.colormap = colormap;
167
187
  if (n_initial !== undefined)
@@ -225,7 +245,7 @@ export function buildFloopParams(args) {
225
245
  return { params, paramSummary };
226
246
  }
227
247
  export function buildTrainMapParams(args, presets) {
228
- const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
248
+ const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
229
249
  const p = preset ? presets[preset] : undefined;
230
250
  const params = {
231
251
  model: model ?? "SOM",
@@ -245,6 +265,8 @@ export function buildTrainMapParams(args, presets) {
245
265
  params.epochs = p.epochs;
246
266
  if (cyclic_features?.length)
247
267
  params.cyclic_features = cyclic_features;
268
+ if (cyclic_pairs?.length)
269
+ params.cyclic_pairs = cyclic_pairs;
248
270
  if (columns?.length)
249
271
  params.columns = columns;
250
272
  if (auto_log_transforms)
@@ -274,9 +296,8 @@ export function buildTrainMapParams(args, presets) {
274
296
  else if (p?.backend)
275
297
  params.backend = p.backend;
276
298
  params.output_format = output_format ?? "png";
277
- const dpiMap = { standard: 1, retina: 2, print: 4 };
278
299
  if (output_dpi && output_dpi !== "retina")
279
- params.output_dpi = dpiMap[output_dpi] ?? 2;
300
+ params.output_dpi = resolveOutputDpi(output_dpi);
280
301
  if (colormap)
281
302
  params.colormap = colormap;
282
303
  if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
@@ -1,7 +1,7 @@
1
1
  import { registerAuditedTool } from "../audit.js";
2
2
  import { fetchTrainingGuidanceFromApi } from "../shared.js";
3
3
  export function registerTrainingGuidanceTool(server) {
4
- registerAuditedTool(server, "training_guidance", "Structured parameter guidance for training (presets, grid/epochs/batch, model, periodic, SIOM/FLooP fields where your plan allows, categorical baselines, normalization MAD vs SEPD guide). Served from the API and filtered to allowed_job_types. Prep ladder: prepare_training prompt = narrative checklist; this tool = JSON/hints + normalization decision ladder — review, then submit with train. For full orientation and SOP, call guide_barsom_workflow first. Optional UI: results_explorer—not required; jobs + results suffice.", {}, async () => {
4
+ registerAuditedTool(server, "training_guidance", "Structured presets, parameter hints, and normalization decisions for train. Served from the API, filtered to your plan. Review, then submit with train.", {}, async () => {
5
5
  const text = await fetchTrainingGuidanceFromApi();
6
6
  return { content: [{ type: "text", text }] };
7
7
  });
@@ -1,9 +1,9 @@
1
- import { apiCall, pollUntilComplete } from "./shared.js";
1
+ import { apiCall, pollUntilComplete, POLL_FINALIZE_MAX_MS } from "./shared.js";
2
2
  /**
3
3
  * When train compute finishes, finalize_training may still be rendering figures on worker-io.
4
4
  * Poll it before fetching results.
5
5
  */
6
- export async function pollFinalizeIfPresent(jobId, data, timeoutMs = 600_000) {
6
+ export async function pollFinalizeIfPresent(jobId, data, timeoutMs = POLL_FINALIZE_MAX_MS) {
7
7
  const finalizeJobId = data.finalize_job_id;
8
8
  if (!finalizeJobId)
9
9
  return { finalizeJobId: null, note: null };
@@ -16,7 +16,7 @@ export function isKernelTrainingComplete(data, status) {
16
16
  return true;
17
17
  return status === "completed";
18
18
  }
19
- export function teCurveLabel(base, _kernelComplete = false) {
19
+ export function teCurveLabel(base) {
20
20
  return `${base} (panel)`;
21
21
  }
22
22
  export function formatCurveSourceNote(data) {