@qe-mcp/server-ena 0.1.1 → 0.1.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.
package/.gitlab-ci.yml CHANGED
@@ -60,7 +60,8 @@ verify-tag-version:
60
60
  script:
61
61
  - |
62
62
  TAG_VERSION="${CI_COMMIT_TAG#v}"
63
- PKG_VERSION=$(node -e "console.log(require('./package.json').version)")
63
+ # Parse with sed — the template's alpine image has no node
64
+ PKG_VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' package.json | head -1)
64
65
  if [ "${TAG_VERSION}" != "${PKG_VERSION}" ]; then
65
66
  echo "-----------------------------------------------------------"
66
67
  echo " Tag/package.json mismatch — release blocked"
package/README.md CHANGED
@@ -30,6 +30,8 @@ Gives Claude a set of ENA tools that run **locally on your machine**. The WASM m
30
30
 
31
31
  ## Installation
32
32
 
33
+ The package is published to the public npm registry — no registry configuration needed.
34
+
33
35
  ### Option A — npx (no install needed)
34
36
 
35
37
  Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac):
@@ -39,24 +41,17 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac):
39
41
  "mcpServers": {
40
42
  "ena": {
41
43
  "command": "npx",
42
- "args": ["@qe-mcp/server-ena"]
44
+ "args": ["--yes", "@qe-mcp/server-ena"]
43
45
  }
44
46
  }
45
47
  }
46
48
  ```
47
49
 
48
- Then add the registry to your `~/.npmrc` once:
49
-
50
- ```bash
51
- echo "@qe-mcp:registry=https://gitlab.com/api/v4/projects/22522458/packages/npm/" >> ~/.npmrc
52
- ```
53
-
54
50
  Restart Claude Desktop.
55
51
 
56
52
  ### Option B — global install
57
53
 
58
54
  ```bash
59
- echo "@qe-mcp:registry=https://gitlab.com/api/v4/projects/22522458/packages/npm/" >> ~/.npmrc
60
55
  npm install -g @qe-mcp/server-ena
61
56
  ```
62
57
 
@@ -91,6 +86,17 @@ npm install
91
86
  }
92
87
  ```
93
88
 
89
+ ### Dev builds
90
+
91
+ Pre-release builds are published only to the QE GitLab registry with a `dev` tag.
92
+ To use one, add the scope mapping once and pin `@dev`:
93
+
94
+ ```bash
95
+ echo "@qe-mcp:registry=https://gitlab.com/api/v4/projects/22522458/packages/npm/" >> ~/.npmrc
96
+ echo "@qe-libs:registry=https://gitlab.com/api/v4/projects/22522458/packages/npm/" >> ~/.npmrc
97
+ npx --yes @qe-mcp/server-ena@dev
98
+ ```
99
+
94
100
  ### Verify
95
101
 
96
102
  In Claude Desktop, open a new conversation and ask:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qe-mcp/server-ena",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for Epistemic Network Analysis — WASM backend, zero Python dependency",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1145,9 +1145,69 @@ const server = new Server(
1145
1145
  { capabilities: { tools: {} } }
1146
1146
  );
1147
1147
 
1148
+ // ── rena-wasm API adapter ─────────────────────────────────────────────────────
1149
+ // rena-wasm ≥ the R-mirroring rewrite returns an R-style ENA set object
1150
+ // (lineWeights / points / rotation.nodes / model.unitLabels) instead of the
1151
+ // original flat object (networks / centroids / positions / unitLabels).
1152
+ // These shims expose the flat field names this server is written against, and
1153
+ // are idempotent no-ops on the older flat API.
1154
+
1155
+ function adaptModel(m) {
1156
+ if (!m || m.networks !== undefined || m.lineWeights === undefined) return m;
1157
+ return {
1158
+ networks: m.lineWeights, // sphere-normed adjacency (nUnits × nConn)
1159
+ centroids: m.points, // projected unit positions (nUnits × dims)
1160
+ positions: m.rotation?.nodes, // code node positions (nCodes × dims)
1161
+ connectionNames: m.connectionNames,
1162
+ unitLabels: m.model?.unitLabels ?? m.unitLabels,
1163
+ columnNames: m.rotation?.columnNames,
1164
+ eigenvalues: m.rotation?.eigenvalues,
1165
+ nUnits: m.nUnits,
1166
+ nConnections: m.nConnections,
1167
+ dims: m.dims,
1168
+ };
1169
+ }
1170
+
1171
+ function adaptAccum(a) {
1172
+ if (!a || a.networks !== undefined || a.connectionCounts === undefined) return a;
1173
+ return { ...a, networks: a.connectionCounts };
1174
+ }
1175
+
1176
+ // A WASM abort (raised by libqe on a degenerate SVD / means rotation) leaves the
1177
+ // module instance unusable — every later call would also abort. Detect it,
1178
+ // discard the cached instance so the next call reloads a fresh one, and surface
1179
+ // an actionable message instead of the raw "Aborted(undefined)".
1180
+ function isWasmAbort(e) {
1181
+ return /abort|RuntimeError|out of bounds|memory access|table index/i.test(e?.message ?? '');
1182
+ }
1183
+
1184
+ const DEGENERATE_FIT_MSG =
1185
+ 'ENA rotation could not be computed — the data is too sparse or degenerate. ' +
1186
+ 'Common causes: fewer than 2 units have any code co-occurrences within the ' +
1187
+ 'stanza window; only one code column; or all units have identical networks. ' +
1188
+ 'Use ena_accumulate to inspect the raw co-occurrence networks, and check that ' +
1189
+ 'you have 2+ code columns that actually co-occur and 2+ units with non-empty networks.';
1190
+
1148
1191
  let enaPromise = null;
1149
1192
  function getENA() {
1150
- if (!enaPromise) enaPromise = loadENA();
1193
+ if (!enaPromise) enaPromise = loadENA().then(ena => {
1194
+ const guard = (fn, adapt) => (rows, opts) => {
1195
+ try {
1196
+ return adapt(fn.call(ena, rows, opts));
1197
+ } catch (e) {
1198
+ if (isWasmAbort(e)) {
1199
+ enaPromise = null; // force a fresh WASM instance next call
1200
+ throw new Error(DEGENERATE_FIT_MSG);
1201
+ }
1202
+ throw e;
1203
+ }
1204
+ };
1205
+ return {
1206
+ ...ena,
1207
+ fit: guard(ena.fit, adaptModel),
1208
+ accumulate: guard(ena.accumulate, adaptAccum),
1209
+ };
1210
+ });
1151
1211
  return enaPromise;
1152
1212
  }
1153
1213