formlab-mcp 0.6.2 → 0.6.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tools/analytics.js +32 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "mcpName": "io.github.juliu1980/formlab-mcp",
5
5
  "description": "Read-only Model Context Protocol server for FormLab — lets Claude (and other MCP clients) read and analyze your FormLab data, from a local export file OR your live cloud workspace.",
6
6
  "type": "module",
@@ -503,6 +503,11 @@ const _DOE_TYPE_LABELS = {
503
503
  full: 'Full-factorial', pb: 'Plackett-Burman', ccd: 'Central Composite',
504
504
  bbd: 'Box-Behnken', lhs: 'Latin Hypercube', simplex: 'Simplex-Lattice', dopt: 'D-optimal',
505
505
  };
506
+ // The model key a design was optimised for. Only D-optimal stores one; the other
507
+ // generators impose their own structure, so there is nothing to report.
508
+ function _doeModelKey(d) {
509
+ return d && d.designType === 'dopt' ? ((d.settings || {}).doptModel || null) : null;
510
+ }
506
511
  // Factor names come off the RECORD first so a design still reads correctly
507
512
  // after an ingredient is renamed or deleted.
508
513
  function _doeNamer(d, db) {
@@ -526,11 +531,28 @@ function _trimDoeDesign(d, db) {
526
531
  factors: (d.factors || []).map(f => ({ ingredient: f.name || nameOf(f.ingredientId), low: f.low, high: f.high, unit: f.unit || '%' })),
527
532
  constraints: (d.constraints || []).map(c => _doeConstraintText(c, nameOf)),
528
533
  mixtureMode: !!d.mixtureMode,
534
+ // The model the design was built for. Lives in settings, so without this a
535
+ // caller listing designs can see "D-optimal" but not whether it was fitted
536
+ // as a process or a Scheffé (mixture) model — which changes what the runs
537
+ // and the D-efficiency mean.
538
+ model: _doeModelKey(d) || null,
539
+ modelLabel: _DOE_MODEL_LABELS[_doeModelKey(d)] || _doeModelKey(d) || null,
529
540
  // Only D-optimal computes one; null here means "not applicable", not "failed".
530
541
  dEfficiency: Number.isFinite(d.dEfficiency) ? +d.dEfficiency.toFixed(2) : null,
531
542
  dEfficiencyKind: Number.isFinite(d.dEfficiency)
532
543
  ? (d.dEfficiencyKind || (/^scheffe-/.test(String((d.settings || {}).doptModel || '')) ? 'relative' : 'absolute'))
533
544
  : null,
545
+ // The relative/absolute PAIR is the point: relative is the grade ("as good as
546
+ // this region allows"), absolute is what the rules cost. Shipping only the
547
+ // relative one lets a caller read 100% on a heavily restricted region and
548
+ // conclude the restrictions were free. Null unless the two actually differ.
549
+ // Both are percentages on a 0-100 scale. The absolute one goes very small on a
550
+ // tightly restricted region (a real mixture measured 0.027), so it keeps more
551
+ // decimals below 1 — toFixed(2) alone would report 0.03, and the UI's own
552
+ // toFixed(0) renders it "0%".
553
+ dEfficiencyAbsolute: Number.isFinite(d.dEfficiencyAbsolute)
554
+ ? +d.dEfficiencyAbsolute.toFixed(d.dEfficiencyAbsolute < 1 ? 4 : 2)
555
+ : null,
534
556
  droppedRuns: d.droppedRuns || 0,
535
557
  response: d.responseText || null,
536
558
  createdAt: d.createdAt,
@@ -618,7 +640,16 @@ const get_doe_design = {
618
640
  const linked = (d.formulationIds || []).map(id => formsById.get(id)).filter(Boolean);
619
641
  const out = {
620
642
  ...base,
621
- balanceFactor: d.mixtureMode ? nameOf(d.balanceFactorId) : null,
643
+ // A balance factor is the component that absorbs the slack when the mixture
644
+ // is imposed on a design that knows nothing about it (the classical types).
645
+ // Simplex-Lattice and Scheffé D-optimal design every component on the
646
+ // simplex, so there is nothing to absorb and balanceFactorId is legitimately
647
+ // null. Passing that null to nameOf() fell through its `id || '?'` tail and
648
+ // answered "?", which reads as missing data rather than as "not applicable".
649
+ balanceFactor: d.mixtureMode && d.balanceFactorId ? nameOf(d.balanceFactorId) : null,
650
+ balanceFactorNote: d.mixtureMode && !d.balanceFactorId
651
+ ? 'No balance factor: this design places its runs on the blend simplex directly, so every component is designed and each run already sums to the mixture total. Nothing has to absorb the remainder.'
652
+ : undefined,
622
653
  settings,
623
654
  augmentedFromProjectId: d.augmentedFromProjectId || null,
624
655
  formulationsGenerated: (d.formulationIds || []).length,