@qe-mcp/ena 0.0.0-stage → 0.1.17

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.
@@ -0,0 +1,348 @@
1
+ ---
2
+ name: ena-analysis
3
+ description: >
4
+ Run Epistemic Network Analysis (ENA) on tabular coded data using the
5
+ @qe-mcp/ena connector. Use whenever the user references tabular data
6
+ with binary presence/absence codes and asks for analysis, exploration,
7
+ co-occurrence patterns, group comparison, choosing a window size, or
8
+ relating network structure to a unit-level variable (generalized rotation),
9
+ or order and sequence — directed ONA / multimodal TMA networks
10
+ — especially discourse data,
11
+ behavioral coding, learning analytics, or clinical observation data.
12
+ Includes a privacy gate for sensitive datasets: raw data can stay entirely
13
+ on the user's machine.
14
+ ---
15
+
16
+ # ENA Analysis
17
+
18
+ Epistemic Network Analysis models patterns of co-occurrence between coded
19
+ events (discourse moves, behaviors, features) within conversations, projecting
20
+ each unit of analysis into a low-dimensional space where distances are
21
+ interpretable. It reveals structural differences that summary statistics and
22
+ regression miss.
23
+
24
+ This guide drives the **ena** MCP connector (`@qe-mcp/ena`). All model
25
+ computation runs locally on the user's machine via WebAssembly — only results
26
+ (node positions, plots, summary statistics) are returned to Claude.
27
+
28
+ ## Privacy gate — always ask first
29
+
30
+ Raw data stays on the user's machine only if it never enters the
31
+ conversation. The server does the analysis locally from a file path; your job
32
+ is to never pull the rows into the conversation yourself.
33
+
34
+ Before calling any ENA tool on new data, ask:
35
+
36
+ > "Is this data sensitive or confidential? If so, give me the file path
37
+ > instead of pasting the data — raw data stays on your machine when you use
38
+ > a file path."
39
+
40
+ Rules, whatever the answer:
41
+
42
+ - **Never open the data file with other tools** — no file readers, `cat`,
43
+ `head`, shells, or code that prints rows. Learn the structure from
44
+ `ena_profile` or `ena_inspect`, which read the file locally.
45
+ - **Never ask the user to paste data.** If they offer, ask for the path.
46
+ `data_csv` is for small demo or toy data only.
47
+ - If the user pastes data anyway, don't repeat it back; suggest saving it to
48
+ a file and using the path next time.
49
+
50
+ If the data is sensitive (or the user hasn't answered yet):
51
+
52
+ 1. Call `ena_profile` with only `data_path`. It returns column names, unique
53
+ counts and binary detection, plus the values of columns with 10 or fewer
54
+ distinct values (e.g. condition names) — no row-level data.
55
+ 2. Use the profile to determine parameters (units, conversations, codes).
56
+ 3. Fit with `data_path`. Only model results reach the conversation.
57
+
58
+ `data_path` can be a `.csv` or an `.xlsx` workbook; for `.xlsx` the first
59
+ worksheet is read, as the ENA Web Tool does. Old `.xls` files must be saved
60
+ as `.xlsx` or `.csv` first.
61
+
62
+ What results still contain — tell the user before fitting if any of it is
63
+ identifying:
64
+
65
+ - **Unit labels**: the values of the unit columns, often participant names
66
+ or IDs. Suggest pseudonymizing the file first.
67
+ - **Group and category values** the analysis names, and derived statistics.
68
+
69
+ Tool errors withhold data values (shown as `[value withheld]`) and give the
70
+ column and row instead; don't try to recover the value by reading the file —
71
+ ask the user to check that row.
72
+
73
+ If the data is not sensitive, or is toy/demo data, proceed with `ena_inspect`
74
+ directly.
75
+
76
+ ## Results come only from the tools
77
+
78
+ When the user is expecting ENA, every result they see must come from these
79
+ tools: accumulated networks, unit points, rotations, statistics, tables and
80
+ plots. A result that looks like ENA but was computed in the conversation is
81
+ worse than no result, because the user can't tell it apart from the real
82
+ thing and may publish it.
83
+
84
+ - **Never compute any part of an ENA analysis yourself**: no co-occurrence
85
+ counting, SVD, means rotation, projection or group statistics in code (a
86
+ notebook, Python, R, JavaScript) or by hand.
87
+ - **Never draw your own network plot**, even "for illustration". Plots come
88
+ back from the tools as an image, an SVG and an interactive HTML file.
89
+ - **Never fill a gap with estimates.** No approximate weights, "typical"
90
+ positions or numbers inferred from earlier output.
91
+
92
+ ### When a tool fails
93
+
94
+ 1. Read the error. Most name the problem: a missing column, a code that
95
+ never occurs, too few units, a group that doesn't exist.
96
+ 2. Fix the call and retry. Use `ena_profile` / `ena_inspect` to check column
97
+ names and values; for a degenerate fit, `ena_accumulate` shows whether the
98
+ networks are empty.
99
+ 3. If it still fails, tell the user what failed and why, in plain words,
100
+ and stop. Offer the next step: different codes, a larger window, more
101
+ units, or rENA in R or the ENA Web Tool for what this server can't do.
102
+
103
+ If a plot fails to render but the fit succeeded, the result says so
104
+ (`render_note`); give the user `interactive_plot` or report it. Don't draw a
105
+ replacement.
106
+
107
+ Every result carries `provenance` (the server and engine versions), and every
108
+ plot names them: the interactive HTML in its footer, the SVG in its `<desc>`.
109
+ Anything without it didn't come from this server.
110
+
111
+ ## When ENA fits the data
112
+
113
+ - Rows are utterances, events, or observations
114
+ - Columns include binary (0/1) presence/absence codes
115
+ - A column identifies who produced each row (unit of analysis)
116
+ - A column groups rows into episodes or conversations
117
+ - Optionally: two or more conditions/groups to compare
118
+
119
+ ## Workflow
120
+
121
+ 1. **Triage** — `ena_inspect` (or `ena_profile` for sensitive data). Returns
122
+ `ena_recommended`, suggested codes/units/conversations, and a pre-filled
123
+ `next_step` call. If `ena_recommended` is true, propose ENA as the primary
124
+ analysis and briefly explain what it will reveal.
125
+ 2. **Window** — if the user has no theoretically motivated window size, call
126
+ `ena_window` (codes + conversations; no units needed). It estimates the
127
+ window by Cross-Covariance Decay (= rENA `ena.ccd`) and returns the decay
128
+ curve. Report the estimate and pass it as `window_size`. Left out, the fit
129
+ tools use that same estimate (as a new set in the ENA Web Tool does), or 4
130
+ when the data is too sparse for one; the result reports the window used.
131
+ `window_size: "Inf"` gives an infinite window (each line connects to every
132
+ earlier line in its conversation).
133
+ 3. **Fit** — `ena_fit` for a single model, or `ena_compare_groups` when two
134
+ groups exist; see [Choosing a rotation](#choosing-a-rotation) for axes
135
+ built around a variable. Use `tma_fit` instead when order matters or
136
+ different kinds of lines need different windows — see
137
+ [Ordered and transmodal networks](#ordered-and-transmodal-networks-ona--tma). Both return a `model_id` and an inline network plot.
138
+ With two groups named, the result also has a `stats` block: per dimension,
139
+ a Welch t-test (Cohen's d) and a Mann-Whitney U test (effect size r).
140
+ Report these rather than running your own tests; the Mann-Whitney p is a
141
+ normal approximation, and under a rotation through the groups dim1 is
142
+ built to separate them.
143
+ Do not generate your own visualization — the plot comes back as an image.
144
+ 4. **Explore** — `ena_plot` with the cached `model_id` renders additional
145
+ views without refitting:
146
+ - `all` — all units, with the mean network of every unit
147
+ - `group` — one group's mean network (`group_value`)
148
+ - `comparison` — two group networks overlaid (`group1`, `group2`)
149
+ - `subtraction` — difference network (`group1`, `group2`)
150
+ - `unit` — a single unit's network (`unit_label`)
151
+ - Any view takes `x_dim` / `y_dim` (1-based; fits keep up to 6
152
+ dimensions) to plot other dimensions, e.g. SVD1 against SVD3, and
153
+ `flip_x` / `flip_y` to mirror an axis — as in the web tool's Plot
154
+ Settings and Flip X / Flip Y. Flipping changes the picture, not the model.
155
+ - Display options, matching the web tool's Plot Settings; they change only
156
+ the drawing:
157
+ - `min_edge_weight` hides connections below a weight. The result reports
158
+ `max_edge_weight` on the plotted network's scale (group means run
159
+ 0.1–1), so pick a fraction of it. Use it on your own when a network is
160
+ too dense to read, and say so.
161
+ - `hide_unconnected_codes` drops codes left with no drawn connection.
162
+ - `text_size` (px, default 12): 16–20 for slides or papers.
163
+ - `colors`: `{ "<group>": "<colour>" }`, as #hex or a CSS colour name.
164
+ - `confidence_intervals: false` hides the CI boxes;
165
+ `outlier_intervals: true` draws the outlier boxes (1.5 × IQR).
166
+
167
+ Map plain requests onto them: "hide the weak connections" is
168
+ `min_edge_weight`, "bigger labels" is `text_size`, "make the treatment
169
+ group orange" is `colors`. For trial-and-error tuning ("a bit less…"),
170
+ point the user to the `interactive_plot` HTML instead of re-plotting: it
171
+ has live controls for the same settings, plus edge scale and labels.
172
+
173
+ Plots are drawn as the ENA Web Tool's comparison plot: unit points, group
174
+ means with 95% CI boxes, and group colours in the web tool's order.
175
+ 5. **Interpret** — strong connections indicate codes that co-occur within
176
+ the stanza window; in subtraction plots, edge color/direction shows which
177
+ group each connection favors. Node positions are fixed across views of the
178
+ same model, so plots are directly comparable.
179
+ Report the goodness of fit with the variance. Below 0.9 on a dimension
180
+ (flagged in the summary), say that points can't be read against nearby
181
+ nodes on that dimension, as the ENA Web Tool warns.
182
+ 6. **Close the loop** — before claiming what a connection means, look at
183
+ the lines behind it: call `ena_lines` with the `model_id`, the
184
+ `connection` (as in `connection_names`) and `units` or `group`, adding the
185
+ text column with `columns` (find it with `ena_profile`). It writes a local
186
+ viewer and returns its path, the counts and data-row numbers. Give the user
187
+ the path to open in a browser, and ask what they see. **Never open, read or
188
+ summarize the viewer or the data file yourself** — it holds the raw lines.
189
+ If the result's check fails, say so; don't interpret those lines.
190
+ 7. **Tables** — when the user wants the numbers (for R, Excel, a paper),
191
+ call `ena_tables` with the `model_id`. It writes `nodes.csv`, `points.csv`,
192
+ `line-weights.csv` and `connection-counts.csv` to a new folder next to the
193
+ data file (the ENA Web Tool's Download Model tables) and returns the paths.
194
+ Don't read the CSVs back into the conversation — give the user the paths.
195
+ 8. **Export** — when the user wants the model in the ENA Web Tool, call
196
+ `ena_export` with the `model_id`. It writes a project ZIP (the data file
197
+ plus a set with every setting and the plot to open with), by default next
198
+ to the data file. Tell the user the path and how to open it: a local
199
+ session ("Run Offline") → Sets → **Import** → *Folder › set name*. Say that
200
+ the ZIP holds their data file. If it refuses a model, relay the reason (the
201
+ web tool has no setting for it) rather than working around it.
202
+
203
+ ## Excluding conversations and ordering lines
204
+
205
+ - `exclude_conversations` (conversation values joined by `::`) removes those
206
+ conversations' lines entirely before the model is built.
207
+ - `order_by` sorts the lines by a column before windowing — use it when the
208
+ file isn't in conversational order (e.g. by a timestamp or turn number).
209
+ Results' row numbers still refer to the file.
210
+
211
+ `ena_export` carries both: they arrive as excluded horizons and the window
212
+ Order in the ENA Web Tool, which applies them the same way.
213
+
214
+ ## Simplifying a model
215
+
216
+ When the user wants fewer codes, or a model looks over-specified, call
217
+ `ena_simplify` with the `model_id`. It runs PRIA (the web tool's Simplify
218
+ Model) with the model's own settings and, if codes can go, refits without
219
+ them and returns the new model. Report which codes were removed and the
220
+ threshold, and continue with the new `model_id`. "No codes can be removed"
221
+ is a result, not a failure: every code is needed at that threshold.
222
+
223
+ ## Masking connections
224
+
225
+ When the user wants a connection left out of the model — for example one
226
+ that is an artifact of the task or the coding scheme — pass
227
+ `mask_connections` (names as in `connection_names`) to the fit. It is the ENA
228
+ Web Tool's Code Masking Matrix: the connection counts as 0 for every unit,
229
+ and the export carries it. For ordered models `A & B` masks A → B only.
230
+ Say in the write-up which connections were masked and why. `ena_lines`
231
+ can't show lines for a masked connection, as none count toward it.
232
+
233
+ ## Choosing a rotation
234
+
235
+ The rotation decides what the axes mean. Pick it from the research question:
236
+
237
+ - **Default** (no `rotation`) — as the ENA Web Tool rotates: with two or more
238
+ unit columns and two or more values in the first (e.g. `Condition` in
239
+ `["Condition", "UserName"]`), a GMR through that column's first two groups,
240
+ with the group tests on them; otherwise SVD. `ena_compare_groups` always
241
+ rotates this way through its two groups — the same axes as a means rotation.
242
+ - **SVD** (`rotation: "svd"`) — exploratory: the axes capture the most
243
+ variance in the networks, whatever drives it. Pass it explicitly for
244
+ grouped units when the question is exploratory.
245
+ - **Means** (`rotation: "mean"` with `group_col`, `group1_value`,
246
+ `group2_value`) — dimension 1 runs through the two group means.
247
+ - **Generalized** (`rotation: "generalized"` on `ena_fit` or `tma_fit`, = rENA
248
+ `ena.rotate.by.generalized`) — dimension 1 (GMR1) is built to represent a
249
+ unit-level variable. Use when the question is how network structure relates
250
+ to an outcome or attribute ("how do networks differ with confidence gain?"),
251
+ or for a group comparison that controls for other variables.
252
+
253
+ - **Projected** (`rotation_from: <model_id>` on `ena_fit` / `tma_fit`) — no
254
+ rotation of its own: the model is placed in another fitted model's space
255
+ (the web tool's Use Rotation Matrix From). Use it to compare a new sample,
256
+ cohort or time point against an existing model, with the same codes and
257
+ network type. Fit the source first; say which model defines the space.
258
+
259
+ ### Generalized rotation (GMR)
260
+
261
+ - `x_var`: the target column first, then any covariates. A categorical
262
+ target puts GMR1 through the group means; a numeric target puts it along
263
+ the variable. Covariates are controlled for by Lasso, with pairwise
264
+ interaction terms unless `interactions: false`.
265
+ - `y_var` (optional, same format) builds dimension 2 (GMR2) for a second
266
+ target; without it, dimension 2 is the next SVD dimension.
267
+ - `select_2_groups: [a, b]` (categorical target) fits GMR1 on those two
268
+ values only; the other units are still projected. It also colours the plot
269
+ and runs the group `stats` on those two groups unless `group_col` is given.
270
+ - Target and covariates must be **unit-level** — one value per unit. Use
271
+ `ena_profile` / `ena_inspect` to find such columns (e.g. pre/post scores,
272
+ condition, demographics), not columns that change line to line.
273
+ - A unit with a missing value is rejected. Tell the user which units, and
274
+ offer `units_used` to model the others (the excluded units' lines still
275
+ count as context) rather than deleting rows.
276
+ - Reading the result: the summary names the axes (`GMR1`, `SVD2` or `GMR2`)
277
+ and the target. GMR1 explains no more variance than SVD1 would (SVD1 is
278
+ the maximum-variance axis) — it is chosen for meaning, not variance; say so rather than treating it as a
279
+ worse fit. Interpret node positions along GMR1 as the connections
280
+ associated with higher vs lower values of the target. Axis signs are
281
+ arbitrary (they can differ from rENA in R). With a categorical target, the
282
+ group test on GMR1 confirms a separation the rotation was built for — like
283
+ means rotation, it is not an independent test.
284
+
285
+ ## Ordered and transmodal networks (ONA / TMA)
286
+
287
+ `tma_fit` (and `tma_accumulate` for the accumulation step alone) builds
288
+ **directed** networks: a connection A → B means code A was in the window
289
+ (the *ground*) when code B appeared (the *response*). Use it when:
290
+
291
+ - **Order matters** — the question is about responses, sequences or
292
+ what leads to what ("do design proposals follow data checks, or precede
293
+ them?"). This is Ordered Network Analysis (ONA, = R's `ona` package):
294
+ `tma_fit` with just units, conversations, codes and `window_size`.
295
+ - **Lines differ in kind** — chat vs logged actions, teacher vs student,
296
+ speech vs gesture — and each kind should look back a different distance or
297
+ count with a different weight. This is Transmodal Analysis (TMA, = R's
298
+ `tma`): the context tensor below.
299
+ - **Windows are in time**, not lines.
300
+
301
+ Otherwise prefer `ena_fit`: undirected models are simpler to explain.
302
+
303
+ ### Parameters beyond `ena_fit`
304
+
305
+ - `ordered` (default true). false gives undirected networks from the same
306
+ tensor — TMA windows without direction.
307
+ - `stanza_modality`: a column whose value (e.g. `chat`, `log`) is the
308
+ line's mode; `stanza_ground` / `stanza_response`: columns (e.g. `Role`)
309
+ whose values on the ground line / the responding line select the window.
310
+ - `stanza_overrides`: per-combination `{ path, window, weight }`. `path`
311
+ lists one value per factor, in the order modality columns, then ground,
312
+ then response — e.g. with `stanza_modality: "Modality"` and
313
+ `stanza_ground: ["Role"]`, `{ "path": ["log", "Student"], "window": 1 }`
314
+ makes students' logged actions look back one line. Combinations not
315
+ listed use `window_size` and weight 1. `window` may be `"Inf"`.
316
+ - `window_type: "time"` with `time_column` and `time_unit` (secs, mins,
317
+ hours, days, weeks; default secs) measures windows in time.
318
+ - `horizons`: `{ by, rules }` gives each unit its own horizon of observation
319
+ instead of the conversations (tma's HOO rules) — e.g.
320
+ `{ "by": "Modality", "rules": { "chat": ["GroupName"], "log": ["UserName"] } }`:
321
+ chat is seen by the whole group, a log line only by its author.
322
+ - Rotation (`svd`, `mean`, `generalized`), group stats, `weight_model`,
323
+ `units_used` and `sphere_norm` work as in `ena_fit`. For a two-group
324
+ comparison of ordered networks use `tma_fit` with `rotation: "mean"` —
325
+ `ena_compare_groups` is undirected only.
326
+
327
+ Before fitting a TMA, confirm the design with the user in words ("chat lines
328
+ look back 4 lines; students' logged actions look back 1") — the tensor is
329
+ easy to get subtly wrong and hard to see from the output. Use `ena_profile`
330
+ or `ena_inspect` to find the modality and role columns and their values.
331
+
332
+ ### Reading directed results
333
+
334
+ - A model with n codes has n² directed connections, named `"A & B"` for
335
+ A → B, including self-connections (A → A: a code following itself).
336
+ - Plots draw directed edges as tapered shapes with direction marks, each
337
+ direction weighted separately. Read the exact weights from
338
+ `connection_names` and the model results rather than estimating them from
339
+ the picture.
340
+ - Report direction explicitly ("Data → Design Reasoning is stronger in
341
+ SecondGame"), not just which codes are connected. In subtraction plots, an
342
+ edge's colour shows which group has more of that direction.
343
+
344
+ Each fit/plot response also includes `interactive_plot` — a local HTML file
345
+ path with the interactive visualization the user can open in a browser.
346
+
347
+ Repeated calls with identical parameters return the cached model instantly,
348
+ so iterating on views is cheap.