@barivia/barsom-mcp 0.20.3 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -162,7 +162,7 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
162
162
 
163
163
  | Tool | Role |
164
164
  |------|------|
165
- | `training_monitor` | Live QE + topographic-error curves (ordering/convergence, dual y-axis), FLooP step progress when applicable; optional — `jobs(action=status)` is enough |
165
+ | `training_monitor` | Live QE + **panel** topographic-error curves (ordering/convergence, dual y-axis); **Panel TE** and **Map TE** scalars at kernel end; FLooP step progress when applicable; optional — `jobs(action=status)` is enough |
166
166
  | `results_explorer` | Browse metrics and figures after training completes |
167
167
  | `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
168
168
 
@@ -180,6 +180,7 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
180
180
 
181
181
  ### Migration notes
182
182
 
183
+ - **Fixed-panel live TE (0.20.4):** mid-training TE curves use a fixed evaluation panel (`te_panel_size` on `train`). Monitors show **Panel TE** and **Map TE** separately; curve tail no longer snaps to map TE.
183
184
  - **Features (0.20.0, non-breaking):**
184
185
  - **FLooP-SIOM maps are now projectable.** `inference(action=project_columns)`, `inference(action=predict)`, and `datasets(action=add_expression, project_onto_job=<floop_job>)` work on FLooP-SIOM (free/chain) maps — values render onto the FLooP Voronoi layout instead of failing. Grid-only ops (`inference(action=transition_flow | impute_column | render_variant)`) return a clear `unsupported_topology_for_inference` message; use a fixed-grid SOM/SIOM for those. No client change required.
185
186
  - **`datasets(action=reduce_spectral)` column selectors.** New optional `columns_range` (`[first, last]`, the server expands the inclusive numeric block — preferred for wide spectra over sending thousands of names) and `columns_except` (drop non-frequency columns like id/label/target from the block, or use alone for "all numeric except these"). `columns_except` composes with `columns_block` or `columns_range`; `columns_block` and `columns_range` are mutually exclusive. Existing `columns_block` callers are unaffected.
package/dist/audit.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * MCP tool audit wrapper — tool name, action, latency, outcome; no secrets.
3
3
  */
4
4
  import { logAudit } from "./logger.js";
5
+ import { runWithTrace } from "./shared.js";
5
6
  /** Keys safe to include in audit param summaries (no paths, keys, or bodies). */
6
7
  const AUDIT_PARAM_KEYS = new Set([
7
8
  "action",
@@ -160,7 +161,9 @@ export function registerAuditedTool(server, name, description, schema, handler)
160
161
  server.tool(name, description, schema, (async (args) => {
161
162
  const rec = args;
162
163
  const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
163
- return runMcpToolAudit(name, action, rec, () => handler(args));
164
+ // One trace per tool invocation: every apiCall inside this handler shares one
165
+ // trace_id, so the API + job chain reconstruct the whole logical action.
166
+ return runWithTrace(() => runMcpToolAudit(name, action, rec, () => handler(args)));
164
167
  }));
165
168
  }
166
169
  /** Attach audit flags to a tool result (stripped before returning to MCP client). */
package/dist/shared.js CHANGED
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import { createReadStream, createWriteStream } from "node:fs";
6
6
  import { createGzip } from "node:zlib";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
+ import { AsyncLocalStorage } from "node:async_hooks";
8
9
  import { Readable } from "node:stream";
9
10
  import { pipeline } from "node:stream/promises";
10
11
  import os from "node:os";
@@ -26,7 +27,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
26
27
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
27
28
  * wrapper version each action requires. Keep in sync with package.json on bump.
28
29
  */
29
- export const CLIENT_VERSION = "0.20.3";
30
+ export const CLIENT_VERSION = "0.21.0";
30
31
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
31
32
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
32
33
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -342,6 +343,30 @@ function throwApiError(status, bodyText, requestId) {
342
343
  /**
343
344
  * @param requestTimeoutMs Optional per-request timeout (default `BARIVIA_FETCH_TIMEOUT_MS`).
344
345
  */
346
+ // ---- Distributed-trace context (W3C traceparent) ----
347
+ //
348
+ // One logical user action (a tool call that may make several API requests, e.g.
349
+ // upload + analyze + train + poll) shares ONE trace_id so the API and the whole job
350
+ // chain (stage -> prepare -> train -> finalize) can be reconstructed end-to-end. The
351
+ // trace id is scoped per tool invocation via AsyncLocalStorage (see registerAuditedTool);
352
+ // each API call mints a fresh span id. Falls back to a per-call trace if no scope is set.
353
+ const _traceStore = new AsyncLocalStorage();
354
+ function _newTraceId() {
355
+ return randomUUID().replace(/-/g, ""); // 32 hex
356
+ }
357
+ function _newSpanId() {
358
+ return randomUUID().replace(/-/g, "").slice(0, 16); // 16 hex
359
+ }
360
+ /** Run `fn` within a fresh trace scope so all apiCall()s inside share one trace_id. */
361
+ export function runWithTrace(fn) {
362
+ return _traceStore.run(_newTraceId(), fn);
363
+ }
364
+ function _currentTraceId() {
365
+ return _traceStore.getStore() ?? _newTraceId();
366
+ }
367
+ function _traceparentHeader() {
368
+ return `00-${_currentTraceId()}-${_newSpanId()}-01`;
369
+ }
345
370
  export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs) {
346
371
  const url = `${API_URL}${path}`;
347
372
  const contentType = extraHeaders?.["Content-Type"] ?? "application/json";
@@ -350,6 +375,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
350
375
  Authorization: `Bearer ${API_KEY}`,
351
376
  "Content-Type": contentType,
352
377
  "X-Request-ID": requestId,
378
+ traceparent: _traceparentHeader(),
353
379
  "X-Barsom-Client-Version": CLIENT_VERSION,
354
380
  ...extraHeaders,
355
381
  };
@@ -424,6 +450,7 @@ export async function apiRawCall(path, requestTimeoutMs) {
424
450
  headers: {
425
451
  Authorization: `Bearer ${API_KEY}`,
426
452
  "X-Request-ID": requestId,
453
+ traceparent: _traceparentHeader(),
427
454
  },
428
455
  }, effectiveTimeout);
429
456
  if (!resp.ok) {
@@ -78,6 +78,8 @@ export const TRAINING_INPUT_SCHEMA = {
78
78
  convergence: z.tuple([z.number(), z.number()]),
79
79
  })]).optional()),
80
80
  batch_size: z.number().int().optional(),
81
+ te_panel_size: z.number().int().optional()
82
+ .describe("Fixed evaluation-panel size for live topographic error during training (default clamp(grid_nodes*6, 500, 10000))"),
81
83
  quality_metrics: z.union([z.enum(["fast", "standard", "full"]), z.array(z.string())]).optional(),
82
84
  backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
83
85
  output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { runMcpToolAudit } from "../audit.js";
4
4
  import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
5
- import { lastEpochTeFromCurves, snapTeCurvesToMapTe } from "../training_monitor_curve.js";
5
+ import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
6
6
  export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
7
7
  export const TRAINING_MONITOR_REFRESH_MS = 5000;
8
8
  function hasCurveArrays(data) {
@@ -72,13 +72,13 @@ async function enrichWithTrainingLog(job_id, data) {
72
72
  if (status === "completed" || status === "failed" || status === "cancelled") {
73
73
  merged.topographic_error = mapTe;
74
74
  }
75
- return snapTeCurvesToMapTe(merged);
75
+ return merged;
76
76
  }
77
77
  catch {
78
78
  if (epochTe != null) {
79
- return snapTeCurvesToMapTe({ ...data, epoch_topographic_error: epochTe });
79
+ return { ...data, epoch_topographic_error: epochTe };
80
80
  }
81
- return data.kernel_complete === true ? snapTeCurvesToMapTe(data) : data;
81
+ return data;
82
82
  }
83
83
  }
84
84
  export function registerTrainingMonitorTool(server) {
@@ -16,25 +16,8 @@ export function isKernelTrainingComplete(data, status) {
16
16
  return true;
17
17
  return status === "completed";
18
18
  }
19
- /** Snap the last TE curve point to authoritative full-map TE when kernel training is done. */
20
- export function snapTeCurvesToMapTe(data) {
21
- const mapTe = data.map_topographic_error ??
22
- (data.kernel_complete === true ? data.topographic_error : null);
23
- if (mapTe == null || !Number.isFinite(Number(mapTe)))
24
- return data;
25
- const out = { ...data };
26
- for (const key of ["ordering_topographic_errors", "convergence_topographic_errors"]) {
27
- const arr = out[key];
28
- if (Array.isArray(arr) && arr.length > 0) {
29
- const copy = arr.slice();
30
- copy[copy.length - 1] = Number(mapTe);
31
- out[key] = copy;
32
- }
33
- }
34
- return out;
35
- }
36
- export function teCurveLabel(base, kernelComplete) {
37
- return kernelComplete ? `${base} (→ map TE)` : `${base} (sampled)`;
19
+ export function teCurveLabel(base, _kernelComplete = false) {
20
+ return `${base} (panel)`;
38
21
  }
39
22
  export function formatCurveSourceNote(data) {
40
23
  const src = data.training_curve_source_batches;
@@ -49,17 +32,20 @@ export function formatCurveSourceNote(data) {
49
32
  if (convTotal != null && convTotal > convShown) {
50
33
  parts.push(`${convShown} of ${convTotal.toLocaleString()} convergence batch samples`);
51
34
  }
52
- const mapTe = data.map_topographic_error;
53
- const epochTe = data.epoch_topographic_error;
54
- if (mapTe != null &&
55
- epochTe != null &&
56
- Number.isFinite(Number(mapTe)) &&
57
- Number.isFinite(Number(epochTe)) &&
58
- Math.abs(Number(epochTe) - Number(mapTe)) > 0.0005) {
59
- parts.push(`Live TE is a subsampled batch estimate during training; final point snaps to map TE ${Number(mapTe).toFixed(4)} (last sampled ${Number(epochTe).toFixed(4)})`);
35
+ const teEval = data.te_evaluation;
36
+ const panelM = teEval?.te_panel_size;
37
+ const panelN = teEval?.te_panel_n_train;
38
+ if (typeof panelM === "number" && typeof panelN === "number" && panelN > 0) {
39
+ const strat = teEval?.te_panel_stratified === true ? ", stratified by mesh" : "";
40
+ parts.push(`TE curve uses a fixed panel of ${panelM.toLocaleString()} of ${panelN.toLocaleString()} training rows${strat}`);
60
41
  }
61
- else if (mapTe != null && Number.isFinite(Number(mapTe))) {
62
- parts.push(`TE curve final point is full-map topographic error (${Number(mapTe).toFixed(4)}).`);
42
+ const panelTe = data.panel_topographic_error;
43
+ const mapTe = data.map_topographic_error;
44
+ if (panelTe != null &&
45
+ mapTe != null &&
46
+ Number.isFinite(Number(panelTe)) &&
47
+ Number.isFinite(Number(mapTe))) {
48
+ parts.push(`Panel TE ${Number(panelTe).toFixed(4)} · Map TE ${Number(mapTe).toFixed(4)} (full training set)`);
63
49
  }
64
50
  if (parts.length === 0)
65
51
  return null;
@@ -82,8 +68,7 @@ export function alignTeToQeAxis(te, qeLen) {
82
68
  return [...Array(pad).fill(null), ...te];
83
69
  }
84
70
  /**
85
- * Last sampled TE point from the (batch-aligned) live TE curve. This is the most
86
- * recent per-epoch TE estimate, distinct from the final trained-map TE in the summary.
71
+ * Last panel TE point from the (batch-aligned) live TE curve.
87
72
  */
88
73
  export function lastEpochTeFromCurves(data) {
89
74
  const conv = data.convergence_topographic_errors;