@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.
- package/README.md +612 -2
- package/fonts/Inter-Bold.ttf +0 -0
- package/fonts/Inter-Regular.ttf +0 -0
- package/fonts/OFL.txt +93 -0
- package/fonts/README.md +4 -0
- package/hooks/ena-privacy-gate.mjs +31 -0
- package/lines.js +349 -0
- package/manifest.json +120 -0
- package/package.json +42 -4
- package/server.js +3449 -0
- package/skill/ena-analysis/SKILL.md +348 -0
- package/webena.js +651 -0
- package/xlsx.js +130 -0
|
@@ -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.
|