@qe-mcp/ena 0.0.0-stage → 0.1.18

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
@@ -1,3 +1,649 @@
1
- # Temporary Holding Version
1
+ # @qe-mcp/ena
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An MCP server that lets Claude and other AI assistants run
4
+ [Epistemic Network Analysis (ENA)](https://www.epistemicnetwork.org/), ordered
5
+ network analysis (ONA) and transmodal analysis (TMA) on your coded data. The
6
+ analysis runs **on your machine** — no R or Python — and only results go to
7
+ the assistant.
8
+
9
+ ## Get started
10
+
11
+ In Claude Desktop, open the **Code** tab (or Claude Code in a terminal) and
12
+ send:
13
+
14
+ ```
15
+ Install ENA from https://qe-libs.org/mcp/qe-mcp-ena/
16
+ ```
17
+
18
+ It downloads the bundle and opens the install dialog; click **Install**, then
19
+ restart Claude Desktop. In a new chat, ask *"What ENA tools do you have?"* —
20
+ it should list 14 — and then ask about your data, by file path: *"Is
21
+ /Users/me/data/coded.csv suitable for ENA?"*
22
+
23
+ **By hand** (no Claude Code): download
24
+ [`qe-mcp-ena.mcpb`](https://qe-libs.org/mcp/qe-mcp-ena/qe-mcp-ena.mcpb),
25
+ double-click it (or drag it into **Settings → Extensions**), click
26
+ **Install**, and restart Claude Desktop.
27
+
28
+ Using another app? See [Other apps](#other-apps).
29
+
30
+ ### The install warning
31
+
32
+ ![Claude Desktop's install dialog: "Installing will grant this extension access to everything on your computer…"](https://qe-libs.org/mcp/qe-mcp-ena/install-warning.png)
33
+
34
+ Claude Desktop shows this for every extension outside Anthropic's reviewed
35
+ directory. An extension is a program that runs under your account, so you are
36
+ trusting its source — as with an R package from GitHub. This one reads the data
37
+ files you name, writes files only when you ask for an export, makes no network
38
+ connections and starts no other programs. Its code is on
39
+ [npm](https://www.npmjs.com/package/@qe-mcp/ena).
40
+
41
+ ### Working with it
42
+
43
+ - **Give a file path, not pasted data.** The server reads the file itself; the
44
+ rows stay on your machine. See [Privacy](#privacy).
45
+ - **Describe the analysis in words.** No tool or parameter names needed — see
46
+ [Prompts](#prompts).
47
+ - **The analysis guide is built in.** The assistant reads it (`ena_guide`)
48
+ before the first analysis. Apps with MCP prompts (Claude Desktop's **+**
49
+ menu, `/` in Claude Code) also offer `analyze_data`, `compare_groups` and
50
+ `order_and_sequence` as starting points.
51
+ - **Optional skill.** The same guide as a Claude skill, `ena-analysis.zip`, is
52
+ on the ENA page. It lets Claude pick up the workflow from the first
53
+ mention of coded data, before any tool is called: upload it under
54
+ **Customize → Skills**, or unzip it into `~/.claude/skills/` for Claude Code.
55
+ It doesn't update itself, so download it again when you upgrade.
56
+
57
+ ## Other apps
58
+
59
+ Every app except the Claude Desktop extension runs
60
+ `npx --yes @qe-mcp/ena` and needs **Node.js 18 or later**. Name the
61
+ server `ena`. Afterwards, open a new conversation and ask *"What ENA tools do
62
+ you have?"* (in VS Code and Cursor, in agent mode).
63
+
64
+ ### Claude Code
65
+
66
+ ```bash
67
+ claude mcp add --scope user ena -- npx --yes @qe-mcp/ena
68
+ ```
69
+
70
+ `--scope user` makes it available in every project; leave it out for the
71
+ current project only. Check with `claude mcp list`.
72
+
73
+ ### Claude Desktop, without the extension
74
+
75
+ **Settings → Developer → Edit Config**, or edit the file directly — macOS:
76
+ `~/Library/Application Support/Claude/claude_desktop_config.json`; Windows:
77
+ `%APPDATA%\Claude\claude_desktop_config.json`:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "ena": {
83
+ "command": "npx",
84
+ "args": ["--yes", "@qe-mcp/ena"]
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ Quit Claude Desktop completely (on Windows, from the system tray too) and
91
+ reopen it. If Windows can't find `npx`, use `"command": "cmd"` and
92
+ `"args": ["/c", "npx", "--yes", "@qe-mcp/ena"]`.
93
+
94
+ ### Cursor and Windsurf
95
+
96
+ The same `mcpServers` block, in `~/.cursor/mcp.json` (or `.cursor/mcp.json`
97
+ for one project) or `~/.codeium/windsurf/mcp_config.json`. Then enable `ena`
98
+ in the app's MCP settings.
99
+
100
+ ### VS Code
101
+
102
+ For GitHub Copilot in agent mode: `.vscode/mcp.json`, or **MCP: Add Server**
103
+ from the Command Palette. The top-level key is `servers`:
104
+
105
+ ```json
106
+ {
107
+ "servers": {
108
+ "ena": {
109
+ "type": "stdio",
110
+ "command": "npx",
111
+ "args": ["--yes", "@qe-mcp/ena"]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Anything else
118
+
119
+ Any client that runs local (stdio) MCP servers: command `npx`, arguments
120
+ `--yes @qe-mcp/ena`. Where a client doesn't show images, plots come back
121
+ as file paths. The guide and the privacy instructions reach every client; only
122
+ the privacy hook is Claude-specific.
123
+
124
+ ### Dev builds and local installs
125
+
126
+ Dev builds are on the QE GitLab registry under the `dev` tag:
127
+
128
+ ```bash
129
+ echo "@qe-mcp:registry=https://gitlab.com/api/v4/projects/22522458/packages/npm/" >> ~/.npmrc
130
+ echo "@qe-libs:registry=https://gitlab.com/api/v4/projects/22522458/packages/npm/" >> ~/.npmrc
131
+ npx --yes @qe-mcp/ena@dev
132
+ ```
133
+
134
+ Pinned: `npm install -g @qe-mcp/ena`, then command `qe-mcp-ena`. From
135
+ a clone: `npm install`, then command `node` with
136
+ `/absolute/path/to/qe-mcp-ena/server.js`.
137
+
138
+ ### Installing with an assistant
139
+
140
+ If you've been asked to install this from the page: in Claude Code, run the
141
+ `claude mcp add` command under [Claude Code](#claude-code). For Claude
142
+ Desktop, download the current bundle and open it — the install dialog
143
+ appears, and the user clicks **Install**:
144
+
145
+ ```bash
146
+ curl -L -o ~/Downloads/qe-mcp-ena.mcpb https://qe-libs.org/mcp/qe-mcp-ena/qe-mcp-ena.mcpb && open ~/Downloads/qe-mcp-ena.mcpb
147
+ ```
148
+
149
+ ```powershell
150
+ Invoke-WebRequest https://qe-libs.org/mcp/qe-mcp-ena/qe-mcp-ena.mcpb -OutFile "$HOME\Downloads\qe-mcp-ena.mcpb"; Start-Process "$HOME\Downloads\qe-mcp-ena.mcpb"
151
+ ```
152
+
153
+ Then have the user restart Claude Desktop and ask *"What ENA tools do you
154
+ have?"* — 14 tools means it worked. Without a shell, walk them through the
155
+ manual install under [Get started](#get-started). Don't edit
156
+ `claude_desktop_config.json` for the extension, and never ask for the data to
157
+ be pasted. A plain-text copy of this page is at
158
+ [llms.txt](https://qe-libs.org/mcp/qe-mcp-ena/llms.txt).
159
+
160
+ ### Hosting it (remote MCP)
161
+
162
+ Everything above runs the server on the user's own machine, which is what
163
+ keeps their data there. For web clients that can only reach remote MCP
164
+ endpoints (Claude.ai custom connectors, ChatGPT connectors, Gemini custom
165
+ apps), the same package can serve MCP over streamable HTTP instead:
166
+
167
+ ```bash
168
+ npx --yes @qe-mcp/ena --http --port 8000 --host 0.0.0.0
169
+ ```
170
+
171
+ The endpoint is `/mcp`; `GET /healthz` answers for probes. `--port` and
172
+ `--host` fall back to `$PORT` and `$HOST`; `--max-body-mb` caps request size
173
+ (default 25).
174
+
175
+ A hosted server has no access to anyone's files, so this mode is a different
176
+ privacy promise, and it says so to the model. In `--http` mode:
177
+
178
+ - Data arrives inline as `data_csv`; `data_path` is refused. The server
179
+ processes it in memory and stores nothing.
180
+ - `ena_profile`, `ena_tables`, `ena_lines` and `ena_export` are not offered
181
+ (they read or write files), and the prompts are not listed (they take a
182
+ path). Plots come back as the inline PNG plus the SVG as text; no files are
183
+ written.
184
+ - The server's instructions tell the model to say that the data leaves the
185
+ user's machine and must be de-identified, and to get their OK first.
186
+ - `model_id`s carry a random suffix so one conversation cannot plot another's
187
+ model.
188
+
189
+ Each request is answered statelessly, so any replica can take any request,
190
+ but fitted models are cached in the process that fitted them: run one
191
+ replica, or use sticky sessions, for `model_id`s to stay valid across calls.
192
+ Put TLS and whatever authentication or rate limiting you need in front of it;
193
+ the server itself does neither. `node test-http.mjs` starts it on a free port
194
+ and checks all of the above.
195
+
196
+ ## Prompts
197
+
198
+ Describe what you want; the assistant picks the tools. These use the columns
199
+ of rENA's `RS.data` sample — units `Condition` and `UserName`, conversations
200
+ `Condition` and `GroupName`, codes such as `Data` and `Design Reasoning`, and
201
+ the unit-level variable `CONFIDENCE.Change`.
202
+
203
+ ### Getting started
204
+
205
+ ```
206
+ I have coded discourse data at /path/to/data.csv. Is it suitable for ENA?
207
+ ```
208
+
209
+ ```
210
+ What columns are in /path/to/data.csv? It's confidential — don't read the rows.
211
+ ```
212
+
213
+ ```
214
+ Analyze /path/to/data.csv with ENA.
215
+ ```
216
+
217
+ ### Fitting a model
218
+
219
+ ```
220
+ Fit ENA with units Condition and UserName, conversations Condition and GroupName, and codes Data, Technical Constraints and Design Reasoning.
221
+ ```
222
+
223
+ ```
224
+ What window size does this data support?
225
+ ```
226
+
227
+ ```
228
+ Refit with a window of 6.
229
+ ```
230
+
231
+ ```
232
+ Model only the students, but keep the mentors' lines as context.
233
+ ```
234
+
235
+ ```
236
+ Leave out the Data & Technical Constraints connection — it's an artifact of the task.
237
+ ```
238
+
239
+ ```
240
+ Can this model be simplified? Drop the codes it doesn't need.
241
+ ```
242
+
243
+ ```
244
+ Fit the post-test data in the same space as the pre-test model.
245
+ ```
246
+
247
+ ### Comparing groups
248
+
249
+ ```
250
+ Compare FirstGame and SecondGame.
251
+ ```
252
+
253
+ ```
254
+ Is the difference on dimension 1 significant?
255
+ ```
256
+
257
+ ```
258
+ Show me the subtraction network.
259
+ ```
260
+
261
+ ### Rotating by a variable
262
+
263
+ ```
264
+ Fit ENA with dimension 1 representing CONFIDENCE.Change.
265
+ ```
266
+
267
+ ```
268
+ Put CONFIDENCE.Change on the x-axis, controlling for Condition.
269
+ ```
270
+
271
+ ### Order and sequence
272
+
273
+ ```
274
+ Fit an ordered network model — which codes tend to follow which?
275
+ ```
276
+
277
+ ```
278
+ Fit TMA with Modality as the mode column: chat lines look back 4 lines, tool actions only 1.
279
+ ```
280
+
281
+ ```
282
+ Measure the window in time: 2 minutes, using the Timestamp column.
283
+ ```
284
+
285
+ ### Plots and files
286
+
287
+ ```
288
+ Show unit FirstGame__steven z.
289
+ ```
290
+
291
+ ```
292
+ Hide the weak connections and make the labels bigger — it's for a slide.
293
+ ```
294
+
295
+ ```
296
+ Save the points and line weights as CSV so I can load them in R.
297
+ ```
298
+
299
+ ```
300
+ Export this model so I can open it in the ENA Web Tool.
301
+ ```
302
+
303
+ ### Back to the data
304
+
305
+ ```
306
+ Show me the lines behind Data & Design Reasoning for FirstGame, with the text column.
307
+ ```
308
+
309
+ ## Privacy
310
+
311
+ The analysis runs in the server, on your machine. What reaches the AI model —
312
+ and the provider behind your app — depends on how the data gets there:
313
+
314
+ | How the data gets there | What the model sees |
315
+ |---|---|
316
+ | A file path (`data_path`) | No raw rows — only results |
317
+ | Inline CSV (`data_csv`) | The whole CSV. For small demo data only |
318
+ | Pasted into the chat, or opened with the app's own file tools | Everything pasted or read |
319
+
320
+ **Results still contain** the unit labels (the values of your unit columns —
321
+ pseudonymize the file first if they identify people), the group values you
322
+ name, `ena_profile`'s values for columns with 10 or fewer distinct values, and
323
+ derived statistics. Errors never quote the data file. Two tools write files
324
+ that do contain your data — `ena_export` (a ZIP with the data file, for the ENA
325
+ Web Tool) and `ena_lines` (a viewer with rows of it) — so treat those like the
326
+ data file.
327
+
328
+ **The server tells the model** to use file paths, never to open the data file
329
+ with other tools, and to ask for a path instead of pasted data: in its
330
+ instructions to every MCP client, in the tool descriptions and in the guide.
331
+ These are instructions, not enforcement. To enforce them, keep the assistant
332
+ out of your data folder with the app's permissions — in Claude Code, a deny
333
+ rule such as `Read(./data/**)` in `.claude/settings.json`, plus the hook below.
334
+
335
+ **Results come only from the server.** Every number and plot should come from
336
+ this server, never from code the assistant writes; the server and every tool
337
+ error say so. Each result carries `provenance` (e.g.
338
+ `@qe-mcp/ena 0.1.18 (rena-wasm 0.1.14)`), the interactive plot names it
339
+ in its footer and the SVG in its `<desc>`. A plot or table without it didn't
340
+ come from this server.
341
+
342
+ ### Privacy gate hook (Claude Code)
343
+
344
+ The hook blocks any `data_csv` payload over 50 rows. Copy the script (it ships
345
+ in the npm package and the repository):
346
+
347
+ ```bash
348
+ mkdir -p ~/.claude/hooks
349
+ cp hooks/ena-privacy-gate.mjs ~/.claude/hooks/
350
+ ```
351
+
352
+ Then add to `~/.claude/settings.json`, replacing `YOUR_USERNAME` (if you
353
+ already have a `PreToolUse` list, add the entry to it):
354
+
355
+ ```json
356
+ {
357
+ "hooks": {
358
+ "PreToolUse": [
359
+ {
360
+ "matcher": "mcp__ena__.*",
361
+ "hooks": [
362
+ {
363
+ "type": "command",
364
+ "command": "node /Users/YOUR_USERNAME/.claude/hooks/ena-privacy-gate.mjs"
365
+ }
366
+ ]
367
+ }
368
+ ]
369
+ }
370
+ }
371
+ ```
372
+
373
+ The matcher covers every tool of a server named `ena`. It takes effect without
374
+ a restart. If you installed the hook before 0.1.12, copy it again: earlier
375
+ versions only warned instead of blocking.
376
+
377
+ ## Reference
378
+
379
+ ### Tools
380
+
381
+ | Tool | What it does |
382
+ |---|---|
383
+ | `ena_guide` | The analysis guide — workflow, rotations, ONA/TMA, reading results. Called before the first analysis |
384
+ | `ena_profile` | Column names, counts and binary detection for a local CSV — no row data |
385
+ | `ena_inspect` | Is the data ENA-shaped? Suggests codes, units and conversations |
386
+ | `ena_window` | Estimates the window size from the data (Cross-Covariance Decay, = rENA `ena.ccd`) |
387
+ | `ena_fit` | Fits an ENA model and plots it; returns a `model_id` |
388
+ | `ena_compare_groups` | Two-group model, rotated through the groups as the ENA Web Tool does, with per-dimension tests |
389
+ | `tma_fit` | Fits a directed ONA or TMA model and plots it |
390
+ | `ena_plot` | Another view of a fitted model: all units, a group, two groups overlaid, subtraction, one unit |
391
+ | `ena_accumulate` / `tma_accumulate` | The accumulation step only: raw per-unit networks |
392
+ | `ena_export` | Writes a fitted model as an ENA Web Tool project ZIP, to open there as the same model and plot |
393
+ | `ena_simplify` | Simplifies a model with PRIA, the web tool's Simplify Model: removes the codes it can do without, and refits |
394
+ | `ena_lines` | Shows the lines behind a connection in a local viewer — the web tool's Data View — with every window the model counted |
395
+ | `ena_tables` | Writes a fitted model's points, node positions, line weights and connection counts as CSV files |
396
+
397
+ ### Fit options
398
+
399
+ All fit and accumulate tools take `data_path` — a `.csv` or `.xlsx` file (the
400
+ first worksheet, as in the ENA Web Tool) — or `data_csv`, with `units`,
401
+ `conversations` and `codes`. Options:
402
+
403
+ | Option | Meaning |
404
+ |---|---|
405
+ | `window_size` | Lines to look back, or `"Inf"` for every earlier line in the conversation. Omitted: the `ena_window` estimate (as a new set in the ENA Web Tool takes), or 4 when the data is too sparse for one. |
406
+ | `binary` | Count each line's co-occurrences as present/absent (default `true`). |
407
+ | `weight_model` | Weight each line's counts before summing per unit: `product` (raw), `sqrt`, `log` (= rENA `weight.by`). Overrides `binary`. |
408
+ | `units_used` | Model only these units (labels as in the results); other units' lines still count as context (= rENA `units.used`). |
409
+ | `mask_connections` | Connections to switch off, e.g. `["Data & Technical Constraints"]` — the ENA Web Tool's Code Masking Matrix. They count as 0 for every unit before normalization and rotation; for ordered models, `A & B` masks A → B only. Exported with `ena_export`. |
410
+ | `exclude_conversations` | Conversations to leave out, named by their conversation values joined by `::` (e.g. `FirstGame::Electric`). Their lines are removed before anything else — not modeled, not context — as the web tool's former R backend did for excluded horizons. |
411
+ | `order_by` | Order the lines by a column before windowing (the web tool's *Order*): ascending and stable, numeric when every value is a number. Row numbers in results still refer to the file. |
412
+ | `sphere_norm` | Normalize each unit network to length 1 (default `true`); `false` scales all by the longest, so network size counts. |
413
+ | `center_align_to_origin` | Keep units with empty networks at the origin and out of the centering (default `true`). Unordered models only. |
414
+ | `dims` | Dimensions to return (default 2). |
415
+ | `group_col`, `group1_value`, `group2_value` | Colour the plot by group, and test two groups on each dimension. `group_col` defaults to the first unit column when there are two or more. |
416
+
417
+ Repeated `ena_fit` / `ena_compare_groups` calls with the same options return
418
+ the cached model. `ena_plot` draws more views of a model by `model_id` without
419
+ refitting; `x_dim` / `y_dim` put any of the model's dimensions on the axes
420
+ (fits keep up to 6, as the ENA Web Tool does), and `flip_x` / `flip_y` mirror
421
+ them.
422
+
423
+ ### Simplifying a model
424
+
425
+ `ena_simplify` runs PRIA — the ENA Web Tool's Simplify Model, the same
426
+ search as R's `PRIA::pria` — on a fitted model. It finds the largest set of
427
+ codes, up to `remove_num` (default 3) and never leaving fewer than 3, whose
428
+ removal keeps the model within `threshold` (default 0.95; 0.80–0.99) of the
429
+ full one. If codes can go, it refits without them, with the window and every
430
+ other setting unchanged, and returns the new model with `removed_codes`.
431
+
432
+ ### Rotations
433
+
434
+ | `rotation` | Axes |
435
+ |---|---|
436
+ | *(omitted)* | As the ENA Web Tool rotates: with two or more unit columns and two or more values in the first (e.g. `Condition` in `["Condition", "UserName"]`), a GMR through that column's first two groups in data order — or `group1_value` / `group2_value` when `group_col` is that column — and the group tests on them; otherwise `svd`. `ena_compare_groups` always rotates this way through its two groups. |
437
+ | `svd` | The directions of most variance — exploratory. |
438
+ | `mean` | Dimension 1 runs through two group means (`group_col`, `group1_value`, `group2_value`). |
439
+ | `generalized` | Dimension 1 represents a unit-level variable (= rENA `ena.rotate.by.generalized`). |
440
+ | `rotation_from: <model_id>` | Project into another fitted model's space — the web tool's *Use Rotation Matrix From* (= rENA `rotation.set`). Same codes and network type required. |
441
+
442
+ With one categorical target and two groups, the web tool's GMR gives the same
443
+ axes as a means rotation.
444
+
445
+ **Projecting into another model** (`ena_fit`, `tma_fit`): `rotation_from`
446
+ takes a fitted model's `model_id`. The new model's units are centred and
447
+ rotated with the source's rotation and drawn with its node positions, so
448
+ the two share one space — a second cohort, a post-test, a held-out sample.
449
+ Points match rENA's `ena.make.set(..., rotation.set = source$rotation)`.
450
+ Exporting a projected model is refused: in the web tool the rotation comes
451
+ from another set in the same project.
452
+
453
+ **Generalized rotation** (`ena_fit`, `tma_fit`):
454
+
455
+ - `x_var` — the target column, then optional covariates, which are controlled
456
+ for by Lasso (with pairwise `interactions` unless set to `false`).
457
+ A categorical target puts dimension 1 (GMR1) through the group means; a
458
+ numeric one puts it along the variable.
459
+ - `y_var` — optional second target, for dimension 2 (GMR2).
460
+ - `select_2_groups` — fit GMR1 on two values of a categorical target; the plot
461
+ and group tests then use those two groups.
462
+
463
+ `x_var` and `y_var` must be unit-level — one value per unit, none missing. A
464
+ unit with a missing value is rejected (rENA errors there too); leave it out
465
+ with `units_used`. Results match rENA's to six decimal places, up to the sign
466
+ of each axis. GMR1 explains no more variance than the first SVD dimension
467
+ would: it's chosen for meaning, not variance.
468
+
469
+ ### ONA and TMA
470
+
471
+ `tma_fit` builds **directed** networks: a connection A → B means code A was in
472
+ the window — the *ground* — when code B appeared — the *response*.
473
+
474
+ | Use | When |
475
+ |---|---|
476
+ | **ONA** (= R's `ona`) | Order matters: responses, sequences, what leads to what. Just units, conversations, codes and `window_size`. |
477
+ | **TMA** (= R's `tma`) | Lines differ in kind — chat vs logged actions, mentor vs student — and each kind should look back a different distance or count with a different weight. |
478
+ | `ena_fit` | Neither: undirected models are simpler to explain. |
479
+
480
+ | Option | Meaning |
481
+ |---|---|
482
+ | `ordered` | Directed networks (default `true`); `false` gives undirected networks from the same windows. |
483
+ | `stanza_modality` | Column whose value is the line's mode (e.g. `chat`, `log`). |
484
+ | `stanza_ground` | Column(s) whose value on the *ground* (earlier) line selects the window — e.g. who said it. |
485
+ | `stanza_response` | Column(s) whose value on the *responding* line selects the window. |
486
+ | `stanza_overrides` | Per-combination `{ path, window, weight }`; others use `window_size` and weight 1. |
487
+ | `window_type`, `time_column`, `time_unit` | `"time"` measures windows in time from `time_column`, in `secs` (default), `mins`, `hours`, `days` or `weeks`. Times can be numbers, `MM:SS`, `HH:MM:SS` or date-times. |
488
+ | `horizons` | `{ by, rules }`: each unit gets its own horizon of observation instead of the conversations (tma's HOO rules). |
489
+
490
+ **Override paths** list one value per factor: modality columns, then ground,
491
+ then response. With `stanza_modality: "Modality"` and `stanza_ground: ["Role"]`:
492
+
493
+ ```json
494
+ "stanza_overrides": [
495
+ { "path": ["log", "Student"], "window": 1 },
496
+ { "path": ["chat", "Mentor"], "window": 6, "weight": 2 }
497
+ ]
498
+ ```
499
+
500
+ A path with the wrong number of values, or a value not in its column, is an
501
+ error naming the expected order.
502
+
503
+ **Horizons** — `{ "by": "Modality", "rules": { "chat": ["GroupName"], "log":
504
+ ["UserName"] } }` puts a chat line in the horizon of every unit in the same
505
+ group, and a logged action only in its author's. Rows whose `by` value has no
506
+ rule are left out.
507
+
508
+ The rotations, group tests, `weight_model`, `units_used` and `sphere_norm` work
509
+ as in `ena_fit`. To compare two groups with ordered networks, use `tma_fit`
510
+ with `rotation: "mean"` — `ena_compare_groups` is undirected only.
511
+
512
+ **Reading directed results:** n codes give n² connections, named `"A & B"` for
513
+ A → B, including self-connections (A → A). Plots draw directed edges as tapered
514
+ shapes with direction marks, each direction weighted separately; read exact
515
+ weights from the results. In subtraction plots, an edge's colour shows which
516
+ group has more of that direction.
517
+
518
+ ### Output
519
+
520
+ Every fit and plot returns:
521
+
522
+ - **`svg_plot`** — the network plot as SVG.
523
+ - **`interactive_plot`** — a self-contained interactive HTML version, with
524
+ live controls for the web tool's Plot Settings: minimum edge weight, edge
525
+ scale, text size, unconnected codes, code / unit / group labels, unit
526
+ points, and confidence and outlier boxes. Open it in a browser to fine-tune
527
+ a plot without going back through the assistant.
528
+ - **`png_plot`** and an **inline image**, shown in the chat by apps that
529
+ display tool images. PNGs come from the native `@resvg/resvg-js` where
530
+ `npm install` provides it, and otherwise — as in the Claude Desktop
531
+ extension — from `@resvg/resvg-wasm`, drawing labels with the bundled Inter
532
+ font.
533
+
534
+ Files go to the system temp folder under `ena-viz/<process id>/` and last until
535
+ the next restart.
536
+
537
+ `ena_plot` takes the same settings, for plots that come back in the chat or
538
+ go into a paper: `min_edge_weight` (the result reports `max_edge_weight` to
539
+ scale it by), `hide_unconnected_codes`, `text_size`, `colors`
540
+ (`{ "<group>": "<colour>" }`), `confidence_intervals` and `outlier_intervals`.
541
+ They change only the drawing, never the model.
542
+
543
+ Every fit also reports, per dimension, the **variance explained** and the
544
+ **goodness of fit**: Pearson and Spearman correlations between the distances of
545
+ the unit points and the distances of their network centroids (= rENA
546
+ `ena.correlations`, the ENA Web Tool's Goodness of Fit). Below 0.9, the plotted
547
+ points and the networks don't line up well enough to interpret the positions
548
+ of points by the nodes near them; the summary flags it.
549
+
550
+ ### Lines behind a connection
551
+
552
+ `ena_lines` writes a local HTML viewer of the lines that produced a connection
553
+ for the units or group you choose, like the ENA Web Tool's Data View. Each line
554
+ that contributed is shown with the window of lines it was connected to, grouped
555
+ by conversation. The connection's codes are highlighted, and ground and
556
+ response roles are marked for ordered models. Lines outside any window collapse
557
+ into "… N lines …", which you can expand.
558
+
559
+ - **Exact.** The lines come from the model's own per-line counts, and the
560
+ windows follow libtma's window rule, for ENA, ONA and TMA (modes, overrides,
561
+ time windows, horizons). Every line of the selection is re-checked against
562
+ the model, and the result reports the check.
563
+ - **Columns.** By default the viewer shows what the web tool's Data View shows:
564
+ unit, conversation and code columns. Add others, such as the text, with
565
+ `columns`; any column can be switched on in the viewer.
566
+ - **Private.** The tool returns counts and data-row numbers (`window_rows`;
567
+ add 1 for the spreadsheet row) — never text. The viewer contains rows of your
568
+ data file, so treat it like the data file. The assistant is told not to open
569
+ it.
570
+
571
+ ### Data tables
572
+
573
+ `ena_tables` writes a model's tables as CSV, as the ENA Web Tool's Download
574
+ Model does, into a new folder (by default next to the data file):
575
+
576
+ | File | Rows | Columns |
577
+ |---|---|---|
578
+ | `nodes.csv` | codes | `code`, then one column per dimension (`SVD1`, `GMR1`, ...) |
579
+ | `points.csv` | units | `ENA_UNIT`, then one column per dimension |
580
+ | `line-weights.csv` | units | `ENA_UNIT`, the unit columns, then one `A & B` column per connection (normalized) |
581
+ | `connection-counts.csv` | units | the same, with the raw accumulated counts |
582
+
583
+ They hold up to 6 dimensions, unscaled, with the same values as rENA's
584
+ `set$points`, `set$rotation$nodes`, `set$line.weights` and
585
+ `set$connection.counts`. For ordered models, `A & B` is A → B.
586
+
587
+ ### Matching the ENA Web Tool
588
+
589
+ Defaults, plots and exports follow the ENA Web Tool, so a model fit here is
590
+ the same model, drawn the same way, once it is opened there. The conventions
591
+ are copied from the web tool into `webena.js` and kept in step with it.
592
+
593
+ - **Defaults**: the window (Cross-Covariance Decay) and the rotation (by the
594
+ first unit column's groups) above; units are grouped by the first unit
595
+ column, in the order the web tool lists them (integer-like values first,
596
+ then order of appearance); a single unit column makes one group of every
597
+ unit, named after it.
598
+ - **Plots** are the web tool's comparison plot: unit points scaled to the
599
+ network as its *Scale Units* does, group means with 95% confidence boxes,
600
+ group colours `#cc423a`, `#218ebf`, `#9d5dbb`, … in group order, axes
601
+ labelled with their variance (`GMR1 (30.91%)`), and the network of one
602
+ group, one unit, or group 1 − group 2. Group networks are the mean line
603
+ weights rescaled to 0.1–1, as the web tool draws them, so a subtraction is
604
+ the difference of those; the numbers in the results (`group1_networks`,
605
+ `subtraction_network`, `top_differential`) are the plain means. A
606
+ subtraction's strongest edge is drawn 4 px wide; `magnify` is the web tool's
607
+ *Scale edge weights* (default 1).
608
+ - **Export**: `ena_export` writes a project ZIP with the data file and a set
609
+ holding the units, conversations, codes, window, rotation, weight model,
610
+ sphere normalization, group colours and the plot to open with. In the web
611
+ tool, start a local session (*Run Offline*), click **Import** in the Sets
612
+ panel and open *Folder › &lt;set name&gt;*. Units left out with `units_used`
613
+ arrive excluded in the web tool's Units tab, `exclude_conversations` as
614
+ excluded horizons and `order_by` as the window Order; the web tool applies
615
+ them the same way.
616
+ A model the web tool has no setting for — transmodal or time windows, a GMR
617
+ on `x_var`, a rotation by a column other than the first unit column,
618
+ `center_align_to_origin: false` — is refused with the reason. The ZIP goes
619
+ next to the data file by default and never overwrites one.
620
+
621
+ ### Group statistics
622
+
623
+ When two groups are named, results include a `stats` block for each dimension:
624
+ group n, mean, SD and median; a Welch t-test with Cohen's d; and a
625
+ Mann-Whitney U test with rank-biserial r — computed by libqe, the same code as
626
+ rENA's group tests.
627
+
628
+ - The Mann-Whitney p uses the normal approximation with continuity correction;
629
+ R's `wilcox.test()` is exact for small samples without ties, so it can differ
630
+ slightly.
631
+ - `effect_r = 1 − 2U/(n1·n2)`: negative when group 1 tends higher.
632
+ - Under a rotation through the groups (the default for grouped units, means
633
+ or GMR), dimension 1 is built to separate them, so its test confirms the
634
+ separation rather than testing it independently, and dimension 2's test is
635
+ always t = 0, p = 1.
636
+
637
+ ### Developing
638
+
639
+ Tests drive the server over stdio, as an MCP client does:
640
+
641
+ ```bash
642
+ node test-mcp.mjs /path/to/data.csv UnitColumn ConversationColumn Code1 Code2 Code3
643
+ node test-tma.mjs
644
+ node test-render.mjs
645
+ ```
646
+
647
+ `bash scripts/build-mcpb.sh` builds `dist/qe-mcp-ena.mcpb`;
648
+ `cd skill && zip -r ena-analysis.zip ena-analysis -x '*.DS_Store'` builds the
649
+ skill. Releases build both automatically.
Binary file
Binary file