formlab-mcp 0.6.26 → 0.6.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.26",
3
+ "version": "0.6.28",
4
4
  "mcpName": "io.github.juliu1980/formlab-mcp",
5
5
  "description": "Read-only Model Context Protocol server for FormLab \u2014 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",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.juliu1980/formlab-mcp",
4
4
  "description": "Read-only MCP for FormLab — let Claude query your formulation lab. Free reads a local export; Pro connects to your live cloud workspace with a dedicated read-only token.",
5
- "version": "0.6.26",
5
+ "version": "0.6.28",
6
6
  "websiteUrl": "https://formvix.com/mcp",
7
7
  "repository": {
8
8
  "url": "https://github.com/juliu1980/FormLab",
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "formlab-mcp",
16
- "version": "0.6.26",
16
+ "version": "0.6.28",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -983,24 +983,30 @@ function _shelfLife(points, spec) {
983
983
  for (let tt = lastX; tt <= lastX + 120; tt += 0.25) {
984
984
  if (fit.slope < 0 ? bound(tt) <= limit : bound(tt) >= limit) { ciRemaining = tt - lastX; break; }
985
985
  }
986
- return { edge, limit, remainingMonths: remaining, ciRemainingMonths: ciRemaining, r2: fit.r2, elapsedMonths: lastX };
986
+ return { edge, limit, remainingMonths: remaining, ciRemainingMonths: ciRemaining, r2: fit.r2, elapsedMonths: lastX, lastDate: points[points.length - 1].date };
987
987
  }
988
988
 
989
989
  // Same verdict rule as the app (js/views/stability.js stabShelfLifeAssess):
990
990
  // months from T0 vs the target shelf life — short (trend crosses first),
991
991
  // marginal (only the 95% bound does), meets; no target → by time left
992
992
  // (<3 mo high, <12 mo medium). Nothing past the 5-year horizon is reported.
993
- function _shelfLifeAssess(sl, targetMonths) {
993
+ function _shelfLifeAssess(sl, targetMonths, now) {
994
994
  if (!sl || !(sl.r2 >= 0.3) || !(sl.remainingMonths > 0.05)) return null;
995
995
  const pointTotal = sl.elapsedMonths + sl.remainingMonths;
996
996
  const ciTotal = sl.ciRemainingMonths != null ? sl.elapsedMonths + sl.ciRemainingMonths : null;
997
+ // Overdue: the projected crossing (last test + remaining) is already past.
998
+ const crossMs = sl.lastDate ? sl.lastDate.getTime() + sl.remainingMonths * _MS_PER_MONTH : null;
999
+ const nowMs = now != null ? +now : Date.now();
1000
+ const overdue = crossMs != null && crossMs < nowMs;
1001
+ const crossDate = crossMs != null ? new Date(crossMs).toISOString().slice(0, 10) : null;
997
1002
  if (targetMonths) {
998
1003
  const verdict = pointTotal < targetMonths ? 'short' : (ciTotal != null && ciTotal < targetMonths) ? 'marginal' : 'meets';
999
1004
  if (verdict === 'meets' && sl.remainingMonths > 60) return null;
1000
- return { verdict, level: verdict === 'short' ? 'high' : verdict === 'marginal' ? 'medium' : 'info', pointTotal, ciTotal };
1005
+ const level = (overdue && verdict !== 'meets') ? 'high' : verdict === 'short' ? 'high' : verdict === 'marginal' ? 'medium' : 'info';
1006
+ return { verdict, level, pointTotal, ciTotal, overdue, crossDate };
1001
1007
  }
1002
1008
  if (sl.remainingMonths > 60) return null;
1003
- return { verdict: null, level: sl.remainingMonths < 3 ? 'high' : sl.remainingMonths < 12 ? 'medium' : 'info', pointTotal, ciTotal };
1009
+ return { verdict: null, level: (overdue || sl.remainingMonths < 3) ? 'high' : sl.remainingMonths < 12 ? 'medium' : 'info', pointTotal, ciTotal, overdue, crossDate };
1004
1010
  }
1005
1011
 
1006
1012
  const get_stability = {
@@ -1075,10 +1081,14 @@ const get_stability = {
1075
1081
  const n = ys.length;
1076
1082
  const out = { points: n, from: pts[0].date.toISOString().slice(0, 10), to: pts[n - 1].date.toISOString().slice(0, 10), values: ys.map(v => _round(v, 6)), latest: _round(ys[n - 1], 6) };
1077
1083
  if (n < 2) { out.note = 'Fewer than 2 points — no trend/shelf-life.'; return out; }
1078
- const fit = _ols(pts.map((_, i) => i), ys);
1084
+ // Drift on real elapsed months (not point order), like the shelf-life
1085
+ // projection and the app's Stability view: uneven ICH schedules
1086
+ // (0/1/3/6/12 mo) read true.
1087
+ const t0 = pts[0].date.getTime();
1088
+ const days = (pts[n - 1].date - pts[0].date) / 86400000;
1089
+ const fit = days > 0 ? _ols(pts.map(p => (p.date.getTime() - t0) / 86400000 / 30.4375), ys) : null;
1079
1090
  if (fit) {
1080
- const days = (pts[n - 1].date - pts[0].date) / 86400000;
1081
- const perMonth = days > 0 ? (fit.slope * (n - 1) / days) * 30 : 0;
1091
+ const perMonth = fit.slope;
1082
1092
  out.drift = { perMonth: _round(perMonth, 4), r2: _round(fit.r2, 3), direction: perMonth > 0 ? 'increasing' : perMonth < 0 ? 'decreasing' : 'flat' };
1083
1093
  }
1084
1094
  const ic = _iChart(ys);
@@ -1095,6 +1105,8 @@ const get_stability = {
1095
1105
  r2: _round(sl.r2, 2), basis: 'linear extrapolation to spec crossing',
1096
1106
  verdict: as ? as.verdict : null,
1097
1107
  concern: as ? as.level : null,
1108
+ projectedCrossDate: as ? as.crossDate : null,
1109
+ overdue: as ? as.overdue : false,
1098
1110
  reportable: !!as,
1099
1111
  };
1100
1112
  return out;
@@ -1114,7 +1126,7 @@ const get_stability = {
1114
1126
  entity: { type: entityType, uid: entity.uid, name: entity.name || entity.uid },
1115
1127
  parameter: args.parameter, unit, spec, pointCount: points.length, byCondition: split, series,
1116
1128
  targetShelfLife: target ? { months: target.months, source: target.source } : null,
1117
- note: 'shelfLife.fromT0Months / ci95FromT0Months are the projected shelf life counted from the first test (T0): point estimate and the conservative (ICH Q1E) 95%-bound crossing. remainingMonths / ci95Months are the same, counted from the last test. verdict compares against targetShelfLife: "short" = the trend crosses spec before the target, "marginal" = the trend clears it but the 95% bound does not, "meets" = both clear it; null = no target set (then concern is by time left: high <3 mo, medium <12 mo). reportable=false means the app shows no projection (R² < 0.3, crossing already past, or beyond 5 years). Numbers are computed here — narrate them, do not recompute.',
1129
+ note: 'shelfLife.fromT0Months / ci95FromT0Months are the projected shelf life counted from the first test (T0): point estimate and the conservative (ICH Q1E) 95%-bound crossing. remainingMonths / ci95Months are the same, counted from the last test. verdict compares against targetShelfLife: "short" = the trend crosses spec before the target, "marginal" = the trend clears it but the 95% bound does not, "meets" = both clear it; null = no target set (then concern is by time left: high <3 mo, medium <12 mo). overdue=true means projectedCrossDate is already past today (time left runs from the last test), so the sample is probably out of spec now — recommend a re-test. reportable=false means the app shows no projection (R² < 0.3, crossing already past, or beyond 5 years). Numbers are computed here — narrate them, do not recompute.',
1118
1130
  };
1119
1131
  },
1120
1132
  };