ArchitSharma commited on
Commit
1bb3265
·
1 Parent(s): 4a79e5b

Release FeatureLens v0.6.0

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
  # Changelog
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## v0.5.0
4
 
5
  ### Causal specificity
 
1
  # Changelog
2
 
3
+ ## v0.6.0
4
+
5
+ ### UX / navigation
6
+ - Added a plain-language **Start here** tab with a three-step workflow and glossary for non-specialist reviewers.
7
+ - Added a persistent **Current Workbench context** banner so inherited prompt/layer/token state is visible from every tab.
8
+ - Added explicit editable feature selectors for **scale dose-response** and **contrastive continuation preference** instead of silently reusing the single-feature selector.
9
+ - Clarified state provenance in Feature Sets and Feature Evidence; experiment text now states whether it inherits Workbench context or uses an independent prompt set.
10
+ - Normalized heading hierarchy and increased table/header typography for readability.
11
+ - Added native plot **fullscreen** and **export PNG** controls to every BarPlot/LinePlot.
12
+
13
+ ### Live research tools
14
+ - Added **concept-guided candidate feature discovery**: rank features for a selected controlled concept using prompt-wide target-minus-other mean maximum activation, with selectivity and activation-rate diagnostics.
15
+ - Added a **completion-cue sensitivity scan** that appends controlled suffixes to a prompt stem and measures the selected feature at the resulting final token.
16
+ - Added a one-click action to reuse a discovered candidate across single-feature, dose-response, contrastive, and evidence feature selectors.
17
+ - Candidate discovery and cue scans are explicitly exploratory; neither creates semantic labels or overwrites `Offline concept hint`.
18
+
19
+ ### Validation
20
+ - Expanded toy-runtime coverage to **40 tests**, including candidate-feature discovery and completion-cue scans.
21
+ - Retained the actual Gradio `launch()` smoke gate, compile gate, release checker, batched-null regression tests, and final-release adversarial-test deferral.
22
+
23
  ## v0.5.0
24
 
25
  ### Causal specificity
README.md CHANGED
@@ -13,7 +13,7 @@ license: mit
13
 
14
  # FeatureLens — Causal Interpretability Workbench
15
 
16
- > **v0.5:** contrastive continuation preference tests, feature-token activation traces, SAE decoder-geometry diagnostics, prompt-wide concept contrast, a wider centered research UI, copy confirmation, and dynamic-height reflow safeguards—while retaining v0.4's batched null references and 8-direction random ensembles.
17
 
18
  **Research question:**
19
 
@@ -28,14 +28,16 @@ FeatureLens is independent of thesis code and thesis datasets.
28
  1. **Reconstruction** — does the SAE represent the residual stream reasonably well?
29
  2. **Prediction** — does a feature distinguish a controlled concept on held-out paraphrase groups?
30
  3. **Robustness** — does the representation survive rewording locally and prompt-wide?
31
- 4. **Controlled contrast** — does a selected feature prefer one concept group in a balanced live mini-batch?
32
- 5. **Single-feature causality** — does ablation/scaling change downstream probability?
33
- 6. **Dose-response** — does effect size vary coherently with feature coefficient?
34
- 7. **Feature-set causality** — do small sparse subspaces matter more than one feature?
35
- 8. **Non-additivity** — does a joint edit differ from the sum of individual effects?
36
- 9. **Specificity** — are targeted effects larger than norm-matched random residual perturbations?
37
- 10. **Contrastive specificity** — does an edit change preference between a desired and comparison continuation?
38
- 11. **Geometry** — are selected SAE decoder directions aligned, orthogonal, or cancelling before downstream model non-linearity?
 
 
39
 
40
  A high AUROC, high paraphrase overlap, or a large activation is still correlational evidence. Causal evidence comes from downstream change under controlled intervention.
41
 
@@ -60,7 +62,7 @@ The UI also reports single-forward → batched-null execution drift as a diagnos
60
 
61
  ## Random-control ensemble
62
 
63
- A single random residual direction can be unusually weak or unusually disruptive by chance. FeatureLens uses with **8 deterministic norm-matched random directions**.
64
 
65
  For a targeted residual delta `Δh`, each control satisfies:
66
 
@@ -125,10 +127,12 @@ The difference is reported as an **interaction excess**. A non-zero excess means
125
 
126
  ### Feature evidence
127
 
128
- The **Feature evidence** tab now provides two complementary live views:
129
 
130
- 1. **Feature-token activation trace** — shows where the selected feature enters the SAE TopK support across every token of the current Workbench prompt.
131
- 2. **Prompt-wide controlled concept contrast** — evaluates the selected feature on a small balanced batch drawn from the repository's seven controlled concept groups:
 
 
132
 
133
  - code;
134
  - mathematics;
@@ -138,9 +142,9 @@ The **Feature evidence** tab now provides two complementary live views:
138
  - factual entities;
139
  - uncertainty.
140
 
141
- One wording per paraphrase pair is used to reduce near-duplicate inflation. Each prompt is summarized by the feature's **maximum activation across non-padding prompt tokens**. The live scan reports mean/median prompt-wide maxima, prompt activation rate, mean activation when active, and maximum activation by concept. If the feature is inactive in every sampled prompt, the UI reports that explicitly and does not fabricate a leading concept.
142
 
143
- This is an exploratory live feature-evidence tool, **not an automatic semantic label**. The held-out offline AUROC/F1 benchmark remains the stronger claim.
144
 
145
  ### Contrastive continuation preference
146
 
@@ -169,11 +173,20 @@ The prompt-wide view reduces sensitivity to accidentally comparing semantically
169
 
170
  The trajectory view compares SAE reconstruction and sparsity/concentration diagnostics at the same prompt token across layers 4, 14 and 26. It does **not** equate feature IDs across layer-specific SAE dictionaries.
171
 
172
- ## v0.5 interface and embedded-Space behavior
173
 
174
- The app uses `gr.Blocks(fill_width=True)` together with a centered 1600 px research canvas so wide result tables make better use of desktop screens. Result tables use bounded heights to reduce runaway page growth in the Hugging Face iframe. A lightweight browser-side resize/mutation observer requests a layout reflow when dynamic outputs change height, addressing the intermittent case where the embedded Space stopped scrolling until the user switched tabs.
175
 
176
- All explicit action/copy buttons share the same muted teal treatment. Copy actions now briefly change their own label to ** Copied with headers** before returning to the normal label. Typography and control sizes were normalized around a serif research-document style.
 
 
 
 
 
 
 
 
 
177
 
178
  ## Copy tables with headers
179
 
@@ -291,7 +304,7 @@ FEATURELENS_CONTRAST_PROMPTS_PER_CONCEPT=4
291
 
292
  ## UI design
293
 
294
- v0.5 keeps the interface deliberately closer to a conventional research instrument than a rounded dashboard:
295
 
296
  - Times / Liberation Serif-style formal typography;
297
  - restrained teal, umber, stone, plum, and muted red chart accents;
@@ -343,12 +356,13 @@ python scripts/ui_smoke.py
343
  python scripts/release_check.py
344
  ```
345
 
346
- See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.5 validation guide uses the **exact labels shown in the UI**, focuses on new-version acceptance/regression tests, and intentionally defers the comprehensive adversarial suite to the final release.
347
 
348
  ## Limitations
349
 
350
  - SAE features are sparse directions, not guaranteed monosemantic concepts.
351
- - The live concept contrast is a small controlled diagnostic, not a universal ontology.
 
352
  - Prompt-wide max pooling measures feature presence anywhere in the prompt and discards token order.
353
  - Joint residual edits can be non-additive downstream without implying direct feature-feature interaction.
354
  - Eight live random controls give only a coarse empirical tail probability.
@@ -358,7 +372,7 @@ See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.5 validation guide uses t
358
  ## Resume-ready description
359
 
360
  > **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
361
- > Built an SAE-based interpretability system for Qwen3-1.7B with held-out concept discovery, token/prompt-wide feature evidence, reconstruction-preserving single and multi-feature interventions, full-continuation and contrastive preference scoring, dose-response analysis, decoder-geometry/non-additivity diagnostics, and norm-matched random-control ensembles.
362
 
363
  ## Acknowledgements
364
 
 
13
 
14
  # FeatureLens — Causal Interpretability Workbench
15
 
16
+ > **v0.6:** a plain-language **Start here** workflow, persistent experiment context, explicit per-experiment feature selectors, plot fullscreen/export controls, concept-guided candidate-feature discovery, and completion-cue sensitivity tests—while retaining the v0.5 causal, geometry, robustness, and prompt-wide evidence tools.
17
 
18
  **Research question:**
19
 
 
28
  1. **Reconstruction** — does the SAE represent the residual stream reasonably well?
29
  2. **Prediction** — does a feature distinguish a controlled concept on held-out paraphrase groups?
30
  3. **Robustness** — does the representation survive rewording locally and prompt-wide?
31
+ 4. **Candidate discovery** — which features are more active for a chosen controlled concept than for the other groups?
32
+ 5. **Controlled contrast** — does a selected feature prefer one concept group in a balanced live mini-batch?
33
+ 6. **Cue sensitivity** — is a feature responding to a lexical/completion cue rather than the underlying concept?
34
+ 7. **Single-feature causality** — does ablation/scaling change downstream probability?
35
+ 8. **Dose-response** — does effect size vary coherently with feature coefficient?
36
+ 9. **Feature-set causality** — do small sparse subspaces matter more than one feature?
37
+ 10. **Non-additivity** — does a joint edit differ from the sum of individual effects?
38
+ 11. **Specificity** — are targeted effects larger than norm-matched random residual perturbations?
39
+ 12. **Contrastive specificity** — does an edit change preference between a desired and comparison continuation?
40
+ 13. **Geometry** — are selected SAE decoder directions aligned, orthogonal, or cancelling before downstream model non-linearity?
41
 
42
  A high AUROC, high paraphrase overlap, or a large activation is still correlational evidence. Causal evidence comes from downstream change under controlled intervention.
43
 
 
62
 
63
  ## Random-control ensemble
64
 
65
+ A single random residual direction can be unusually weak or unusually disruptive by chance. FeatureLens uses **8 deterministic norm-matched random directions**.
66
 
67
  For a targeted residual delta `Δh`, each control satisfies:
68
 
 
127
 
128
  ### Feature evidence
129
 
130
+ The **Feature evidence** tab now supports four complementary live questions:
131
 
132
+ 1. **Concept-guided candidate discovery** — choose one of the seven controlled concept groups and rank SAE features by the difference between that concept's prompt-wide mean maximum activation and the mean across the other six groups. The result is a candidate list, **not a semantic label**.
133
+ 2. **Feature-token activation trace** — show exactly where a selected feature enters the SAE TopK support across every token of the current Workbench prompt.
134
+ 3. **Completion-cue sensitivity** — append controlled suffixes/cues to one prompt stem and measure the selected feature at the final token. This is useful for separating concept evidence from lexical/structural completion cues.
135
+ 4. **Prompt-wide controlled concept contrast** — evaluate one selected feature on a small balanced batch from:
136
 
137
  - code;
138
  - mathematics;
 
142
  - factual entities;
143
  - uncertainty.
144
 
145
+ Each controlled prompt is summarized by the feature's **maximum activation across non-padding tokens**. Candidate discovery ranks by a transparent target-minus-other mean difference and reports activation rates/selectivity alongside it. The controlled contrast reports mean/median prompt-wide maxima, prompt activation rate, mean activation when active, and maximum activation by concept.
146
 
147
+ Both tools are exploratory live evidence. They never overwrite `Offline concept hint`; the held-out offline AUROC/F1 benchmark remains the stronger feature/concept claim.
148
 
149
  ### Contrastive continuation preference
150
 
 
173
 
174
  The trajectory view compares SAE reconstruction and sparsity/concentration diagnostics at the same prompt token across layers 4, 14 and 26. It does **not** equate feature IDs across layer-specific SAE dictionaries.
175
 
176
+ ## v0.6 interface and navigation
177
 
178
+ v0.6 is designed so a reviewer does not need prior mechanistic-interpretability vocabulary to find the controls:
179
 
180
+ - a **Start here** tab explains the evidence workflow and glossary in plain language;
181
+ - a persistent **Current Workbench context** banner states the active prompt, layer and token after inspection;
182
+ - experiments that reuse that state explicitly say **Uses current Workbench context**;
183
+ - single-feature causal, dose-response, contrastive-preference, feature-set and evidence panels expose their own editable feature selectors instead of silently borrowing a hidden value;
184
+ - concept-guided discovery gives users a route to candidate features without knowing feature IDs in advance;
185
+ - headings now follow a conventional descending hierarchy and table headers use the same readable base scale as the surrounding interface;
186
+ - every native plot exposes Gradio's **fullscreen** control for zoomed reading and **export** control for PNG output;
187
+ - all action/copy buttons retain one muted-teal visual language and copy actions provide visible confirmation.
188
+
189
+ The centered 1600 px research canvas, bounded result-table heights, dynamic resize observer and explicit bottom padding from v0.5 remain in place for Hugging Face's embedded Space layout.
190
 
191
  ## Copy tables with headers
192
 
 
304
 
305
  ## UI design
306
 
307
+ v0.6 keeps the interface deliberately closer to a conventional research instrument than a rounded dashboard:
308
 
309
  - Times / Liberation Serif-style formal typography;
310
  - restrained teal, umber, stone, plum, and muted red chart accents;
 
356
  python scripts/release_check.py
357
  ```
358
 
359
+ See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.6 validation guide uses the **exact labels shown in the UI**, focuses on new-version acceptance/regression tests, and intentionally defers the comprehensive adversarial suite to the final release.
360
 
361
  ## Limitations
362
 
363
  - SAE features are sparse directions, not guaranteed monosemantic concepts.
364
+ - Live candidate discovery and concept contrast are small controlled diagnostics, not a universal ontology or semantic labeling system.
365
+ - Completion-cue sensitivity diagnoses lexical/structural dependence but does not establish a feature's full semantics.
366
  - Prompt-wide max pooling measures feature presence anywhere in the prompt and discards token order.
367
  - Joint residual edits can be non-additive downstream without implying direct feature-feature interaction.
368
  - Eight live random controls give only a coarse empirical tail probability.
 
372
  ## Resume-ready description
373
 
374
  > **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
375
+ > Built an SAE-based interpretability system for Qwen3-1.7B with held-out concept discovery, concept-guided candidate discovery, token/prompt-wide and completion-cue feature evidence, reconstruction-preserving single and multi-feature interventions, full-continuation and contrastive preference scoring, dose-response analysis, decoder-geometry/non-additivity diagnostics, and norm-matched random-control ensembles.
376
 
377
  ## Acknowledgements
378
 
app.py CHANGED
@@ -38,12 +38,14 @@ CSS = r"""
38
  .gradio-container input,
39
  .gradio-container textarea,
40
  .gradio-container select { font-size: 15.5px !important; }
41
- .gradio-container table { font-size: 14.5px !important; }
 
 
42
  .gradio-container label, .gradio-container .label-wrap { font-size: 15px !important; }
43
  .gradio-container p, .gradio-container li { font-size: 16px; }
44
- .gradio-container h2 { font-size: 1.55rem; }
45
- .gradio-container h3 { font-size: 1.30rem; }
46
- .gradio-container h4 { font-size: 1.08rem; }
47
  .hero {
48
  padding: 15px 2px 7px;
49
  border-bottom: 1px solid var(--border-color-primary);
@@ -71,8 +73,9 @@ CSS = r"""
71
  border-top: 1px solid var(--border-color-primary);
72
  font-variant: small-caps;
73
  letter-spacing: .045em;
74
- font-size: .98rem;
75
- opacity: .78;
 
76
  }
77
  .token-wrap { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 2px 14px; line-height: 1.9; }
78
  .token {
@@ -115,6 +118,25 @@ CSS = r"""
115
  min-height: 110px;
116
  }
117
  .tabs, .tabitem { padding-bottom: 26px !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  @media (max-width: 900px) {
119
  .gradio-container { width: 100% !important; padding-left: 12px !important; padding-right: 12px !important; }
120
  }
@@ -409,6 +431,42 @@ def _contrastive_metrics_markdown(result) -> str:
409
  )
410
 
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  @gpu(duration=30)
413
  def analyze_prompt(prompt: str, layer: int, token_index: int, top_n: int):
414
  try:
@@ -437,12 +495,15 @@ def analyze_prompt(prompt: str, layer: int, token_index: int, top_n: int):
437
  table,
438
  chart_df,
439
  feature_update,
 
 
440
  feature_set_update,
441
  contrast_update,
442
  gr.update(value=int(layer)),
443
  _analysis_metrics_markdown(result),
444
  location,
445
  location,
 
446
  _tsv(table),
447
  )
448
  except Exception as exc:
@@ -853,6 +914,65 @@ def run_contrastive_causal(
853
  _raise_ui_error(exc)
854
 
855
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
  def mode_help(mode: str):
857
  if mode == "ablate":
858
  return gr.update(value=0.0, interactive=False, label="Coefficient (unused for ablation)")
@@ -870,7 +990,7 @@ def set_mode_help(mode: str):
870
  with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
871
  gr.HTML(
872
  '<header class="hero">'
873
- '<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.5</span></h1>'
874
  '<div class="subtitle">Causal Interpretability Workbench</div>'
875
  '<div class="metadata">Qwen3-1.7B-Base · Qwen-Scope residual SAEs · layers 4 / 14 / 26 · '
876
  '32,768 features · TopK=50 · ZeroGPU</div>'
@@ -879,6 +999,45 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
879
  'also causally influence the model’s behaviour?</div>'
880
  )
881
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
882
  with gr.Tab("Workbench"):
883
  gr.HTML('<div class="section-rule">I. Inspect a prompt location</div>')
884
  with gr.Row(equal_height=False):
@@ -942,6 +1101,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
942
  x_title="Feature id",
943
  y_title="Activation",
944
  x_label_angle=-35,
 
945
  height=330,
946
  )
947
 
@@ -956,6 +1116,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
956
  choices=[],
957
  allow_custom_value=True,
958
  label="Single feature id",
 
959
  )
960
  mode = gr.Dropdown(
961
  choices=["ablate", "scale", "inject"],
@@ -1005,7 +1166,15 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1005
  target_token_copy = _copy_button()
1006
 
1007
  gr.HTML('<div class="section-rule">III. Single-feature scale dose-response</div>')
1008
- with gr.Accordion("Scale dose-response", open=False):
 
 
 
 
 
 
 
 
1009
  gr.Markdown(
1010
  "Always a **scale** experiment: 0× = ablation, 1× = numerical no-edit reference, 2× = double "
1011
  "the native coefficient. The six conditions are evaluated together, and all deltas are measured "
@@ -1033,11 +1202,20 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1033
  title="Scale dose-response",
1034
  x_title="Feature multiplier",
1035
  y_title="Δ mean log p/token",
 
1036
  height=330,
1037
  )
1038
 
1039
  gr.HTML('<div class="section-rule">IV. Contrastive continuation preference</div>')
1040
- with gr.Accordion("Contrastive causal preference test", open=False):
 
 
 
 
 
 
 
 
1041
  gr.Markdown(
1042
  "Absolute target probability can move because an intervention broadly perturbs the distribution. "
1043
  "This test asks the stricter question: **does the edit shift preference between two exact continuations?** "
@@ -1083,14 +1261,15 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1083
  title="Preference between exact continuations",
1084
  x_title="Execution condition",
1085
  y_title="Sequence log-odds A−B",
 
1086
  height=320,
1087
  )
1088
 
1089
  with gr.Tab("Feature sets"):
1090
  gr.Markdown(
1091
  "### Distributed sparse representations\n"
1092
- "Run **Inspect sparse features** in the Workbench first. The prompt, layer, token index, and active "
1093
- "feature selector below reuse that Workbench state."
1094
  )
1095
  feature_set_location = gr.Markdown("No Workbench inspection has been run yet.")
1096
  feature_set_ids = gr.Dropdown(
@@ -1182,6 +1361,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1182
  title="Effect vs feature-set size",
1183
  x_title="Number of jointly ablated features",
1184
  y_title="Δ mean log p/token",
 
1185
  height=330,
1186
  )
1187
 
@@ -1215,12 +1395,14 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1215
  x_title="Intervention condition",
1216
  y_title="Δ mean log p/token",
1217
  x_label_angle=-25,
 
1218
  height=330,
1219
  )
1220
 
1221
 
1222
  gr.HTML('<div class="section-rule">Decoder geometry</div>')
1223
- with gr.Accordion("Feature-set decoder geometry", open=False):
 
1224
  gr.Markdown(
1225
  "Joint causal effects can reflect both **decoder-direction geometry** and downstream non-linearity. "
1226
  "This zero-extra-generation diagnostic measures pairwise decoder cosines and compares the actual "
@@ -1251,6 +1433,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1251
  x_title="Feature pair",
1252
  y_title="Cosine similarity",
1253
  x_label_angle=-30,
 
1254
  height=320,
1255
  )
1256
 
@@ -1261,7 +1444,48 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1261
  "across FeatureLens's seven controlled concept groups. The concept scan uses the feature's **maximum "
1262
  "activation across non-padding tokens in each prompt**, avoiding arbitrary final-token comparisons."
1263
  )
1264
- contrast_location = gr.Markdown("No Workbench inspection has been run yet.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1265
  with gr.Row(equal_height=True):
1266
  contrast_feature_id = gr.Dropdown(
1267
  choices=[],
@@ -1284,7 +1508,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1284
  scale=2,
1285
  )
1286
 
1287
- gr.Markdown("#### Activation trace across the current Workbench prompt")
1288
  gr.Markdown(
1289
  "Uses the prompt currently entered in **Workbench** and shows exactly which prompt tokens include the "
1290
  "selected feature in the SAE TopK support."
@@ -1312,10 +1536,35 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1312
  x_title="Prompt token",
1313
  y_title="Activation",
1314
  x_label_angle=-35,
 
1315
  height=320,
1316
  )
1317
 
1318
- gr.Markdown("#### Prompt-wide controlled concept contrast")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1319
  contrast_btn = gr.Button("Run controlled concept contrast", variant="primary", elem_classes=["action-btn"])
1320
  contrast_metrics = gr.Markdown()
1321
  with gr.Row(equal_height=False):
@@ -1339,6 +1588,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1339
  x_title="Concept",
1340
  y_title="Mean max activation",
1341
  x_label_angle=-25,
 
1342
  height=330,
1343
  )
1344
 
@@ -1346,7 +1596,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1346
  gr.Markdown(
1347
  "### Local and prompt-wide robustness\n"
1348
  "The selected-token comparison is strict but can be misleading if the two chosen tokens play different "
1349
- "semantic roles. v0.5 reports a **prompt-wide max-pooled feature profile**: for every SAE "
1350
  "feature, take its maximum activation across all prompt tokens, then compare the two sparse profiles."
1351
  )
1352
  with gr.Row():
@@ -1399,6 +1649,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1399
  x_title="Feature id",
1400
  y_title="Activation",
1401
  x_label_angle=-35,
 
1402
  height=330,
1403
  )
1404
 
@@ -1448,6 +1699,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1448
  title="Representation trajectory",
1449
  x_title="Layer",
1450
  y_title="Normalized value",
 
1451
  height=330,
1452
  )
1453
 
@@ -1456,7 +1708,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
1456
  gr.Markdown(
1457
  "The offline pipeline remains the source of held-out feature/concept AUROC + F1, reconstruction "
1458
  "quality, paraphrase stability, dense residual linear probes, and causal intervention conclusions. "
1459
- "v0.5 strengthens the live workbench with batch-context null references and random-control ensembles; "
1460
  "the saved report should be generated only from real experiment artifacts."
1461
  )
1462
 
@@ -1477,7 +1729,7 @@ $$h' = h + \sum_{i \in S}\Delta z_i d_i.$$
1477
 
1478
  FeatureLens patches the delta into the **original residual**; it never replaces the residual with the complete SAE reconstruction.
1479
 
1480
- ### v0.5 control discipline
1481
 
1482
  Batched experiments include an explicit **zero-edit row**. Causal effects are measured against that row rather than a separately executed baseline, which removes batch-vs-single floating-point drift from the measured effect. Random specificity uses an ensemble of norm-matched residual directions rather than one arbitrary seed.
1483
 
@@ -1485,13 +1737,14 @@ Batched experiments include an explicit **zero-edit row**. Causal effects are me
1485
 
1486
  1. SAE reconstruction quality.
1487
  2. Held-out feature/concept prediction.
1488
- 3. Token-local activation traces and prompt-wide feature evidence.
1489
- 4. Local and prompt-wide paraphrase robustness.
1490
- 5. Single-feature causal intervention and dose-response.
1491
- 6. Contrastive continuation preference under intervention.
1492
- 7. Joint feature-set intervention and set-size sensitivity.
1493
- 8. Decoder-direction geometry and individual-vs-joint non-additivity.
1494
- 9. Specificity relative to norm-matched random controls.
 
1495
 
1496
  Association, robustness, geometry, and intervention evidence remain separate claims.
1497
  """
@@ -1514,12 +1767,15 @@ Association, robustness, geometry, and intervention evidence remain separate cla
1514
  feature_table,
1515
  feature_plot,
1516
  feature_id,
 
 
1517
  feature_set_ids,
1518
  contrast_feature_id,
1519
  contrast_layer,
1520
  analysis_metrics,
1521
  feature_set_location,
1522
  contrast_location,
 
1523
  feature_tsv,
1524
  ],
1525
  )
@@ -1539,13 +1795,13 @@ Association, robustness, geometry, and intervention evidence remain separate cla
1539
  )
1540
  dose_btn.click(
1541
  run_dose_response,
1542
- inputs=[prompt, layer, token_index, feature_id, target_text],
1543
  outputs=[dose_table, dose_plot, dose_metrics, dose_tsv],
1544
  )
1545
  contrastive_mode.change(mode_help, inputs=[contrastive_mode], outputs=[contrastive_coefficient])
1546
  contrastive_btn.click(
1547
  run_contrastive_causal,
1548
- inputs=[prompt, layer, token_index, feature_id, contrastive_mode, contrastive_coefficient, contrastive_a, contrastive_b],
1549
  outputs=[contrastive_metrics, contrastive_table, contrastive_plot, contrastive_tsv],
1550
  )
1551
  set_mode.change(set_mode_help, inputs=[set_mode], outputs=[set_coefficient])
@@ -1579,6 +1835,22 @@ Association, robustness, geometry, and intervention evidence remain separate cla
1579
  inputs=[contrast_feature_id, contrast_layer, contrast_n],
1580
  outputs=[contrast_metrics, contrast_table, contrast_plot, contrast_tsv],
1581
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1582
  para_btn.click(
1583
  run_paraphrase_compare,
1584
  inputs=[para_a, para_b, para_layer, para_idx_a, para_idx_b, para_top_n],
@@ -1603,6 +1875,8 @@ Association, robustness, geometry, and intervention evidence remain separate cla
1603
  (geometry_copy, geometry_tsv),
1604
  (trace_copy, trace_tsv),
1605
  (contrast_copy, contrast_tsv),
 
 
1606
  (para_copy, para_tsv),
1607
  (trajectory_copy, trajectory_tsv),
1608
  ]:
 
38
  .gradio-container input,
39
  .gradio-container textarea,
40
  .gradio-container select { font-size: 15.5px !important; }
41
+ .gradio-container table { font-size: 15.5px !important; }
42
+ .gradio-container table thead th, .gradio-container [role="columnheader"] { font-size: 16px !important; font-weight: 700 !important; }
43
+ .gradio-container table tbody td, .gradio-container [role="gridcell"] { font-size: 15.5px !important; }
44
  .gradio-container label, .gradio-container .label-wrap { font-size: 15px !important; }
45
  .gradio-container p, .gradio-container li { font-size: 16px; }
46
+ .gradio-container h2 { font-size: 1.72rem; line-height: 1.25; }
47
+ .gradio-container h3 { font-size: 1.46rem; line-height: 1.28; }
48
+ .gradio-container h4 { font-size: 1.24rem; line-height: 1.30; }
49
  .hero {
50
  padding: 15px 2px 7px;
51
  border-bottom: 1px solid var(--border-color-primary);
 
73
  border-top: 1px solid var(--border-color-primary);
74
  font-variant: small-caps;
75
  letter-spacing: .045em;
76
+ font-size: 1.12rem;
77
+ font-weight: 700;
78
+ opacity: .88;
79
  }
80
  .token-wrap { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 2px 14px; line-height: 1.9; }
81
  .token {
 
118
  min-height: 110px;
119
  }
120
  .tabs, .tabitem { padding-bottom: 26px !important; }
121
+ .context-card {
122
+ border: 1px solid var(--border-color-primary);
123
+ border-left: 4px solid #708B86;
124
+ background: var(--background-fill-secondary);
125
+ padding: 10px 14px;
126
+ margin: 8px 0 16px;
127
+ border-radius: 2px;
128
+ }
129
+ .context-card p { margin: 0 !important; }
130
+ .start-card {
131
+ border: 1px solid var(--border-color-primary);
132
+ background: var(--background-fill-secondary);
133
+ padding: 14px 16px;
134
+ min-height: 150px;
135
+ border-radius: 2px;
136
+ }
137
+ .start-card h3 { margin-top: 0; }
138
+ .experiment-accordion > button { font-size: 1.18rem !important; font-weight: 700 !important; }
139
+ .graph-note { font-size: .95rem; opacity: .72; margin-top: 2px; }
140
  @media (max-width: 900px) {
141
  .gradio-container { width: 100% !important; padding-left: 12px !important; padding-right: 12px !important; }
142
  }
 
431
  )
432
 
433
 
434
+
435
+
436
+ def _discovery_metrics_markdown(result) -> str:
437
+ if not result.candidate_ids:
438
+ return (
439
+ f"No positively selective candidate features were found for **{result.concept}** at layer "
440
+ f"**{result.layer}** in this small live batch. Increase the sample or try another layer."
441
+ )
442
+ return (
443
+ f"Concept **{result.concept}** · layer **{result.layer}** · "
444
+ f"{result.prompts_per_concept} prompts/concept. \n"
445
+ f"Showing **{len(result.candidate_ids)}** features ranked by target-minus-other prompt-wide mean activation. \n\n"
446
+ "This is **candidate discovery**, not a semantic label. Use the offline held-out AUROC/F1 pipeline "
447
+ "before describing a feature as concept-selective."
448
+ )
449
+
450
+
451
+ def _cue_metrics_markdown(result) -> str:
452
+ return (
453
+ f"Feature **{result.feature_id}**, layer **{result.layer}** · active for "
454
+ f"**{result.active_cue_count}/{result.cue_count}** tested completion cues at the final token. \n"
455
+ "This helps distinguish a concept-associated feature from a lexical/structural completion-cue feature. "
456
+ "It is a controlled diagnostic, not a feature label."
457
+ )
458
+
459
+
460
+ def _global_context_markdown(prompt: str, layer: int, result) -> str:
461
+ token = result.tokens[result.token_index] if result.tokens else ""
462
+ short = prompt[:120] + ("…" if len(prompt) > 120 else "")
463
+ return (
464
+ "**Current Workbench context** — "
465
+ f"Prompt: `{short}` · layer **{int(layer)}** · token **{result.token_index}** ({token!r}). \n"
466
+ "Panels marked **Uses current Workbench context** inherit this prompt/layer/token. "
467
+ "Feature selectors remain editable inside each experiment."
468
+ )
469
+
470
  @gpu(duration=30)
471
  def analyze_prompt(prompt: str, layer: int, token_index: int, top_n: int):
472
  try:
 
495
  table,
496
  chart_df,
497
  feature_update,
498
+ gr.update(choices=choices, value=choices[0] if choices else None),
499
+ gr.update(choices=choices, value=choices[0] if choices else None),
500
  feature_set_update,
501
  contrast_update,
502
  gr.update(value=int(layer)),
503
  _analysis_metrics_markdown(result),
504
  location,
505
  location,
506
+ _global_context_markdown(prompt, int(layer), result),
507
  _tsv(table),
508
  )
509
  except Exception as exc:
 
914
  _raise_ui_error(exc)
915
 
916
 
917
+
918
+ @gpu(duration=35)
919
+ def run_concept_feature_discovery(concept: str, layer: int, prompts_per_concept: int, top_n: int):
920
+ try:
921
+ result = RUNTIME.concept_feature_discovery(
922
+ concept=concept,
923
+ layer=int(layer),
924
+ prompts_per_concept=int(prompts_per_concept),
925
+ top_n=int(top_n),
926
+ )
927
+ columns = [
928
+ "Rank",
929
+ "Feature id",
930
+ "Target mean max",
931
+ "Other mean max",
932
+ "Mean difference",
933
+ "Selectivity score",
934
+ "Target activation rate",
935
+ "Other activation rate",
936
+ "Target max activation",
937
+ ]
938
+ table = pd.DataFrame(result.rows, columns=columns)
939
+ chart = pd.DataFrame(result.chart_rows, columns=["Feature", "Target − other mean"])
940
+ chart["Series"] = "Concept contrast"
941
+ choices = [str(fid) for fid in result.candidate_ids]
942
+ candidate_update = gr.update(choices=choices, value=choices[0] if choices else None)
943
+ return _discovery_metrics_markdown(result), table, chart, candidate_update, _tsv(table)
944
+ except Exception as exc:
945
+ _raise_ui_error(exc)
946
+
947
+
948
+ @gpu(duration=25)
949
+ def run_feature_cue_scan(feature_id: str, layer: int, prompt_stem: str, cue_text: str):
950
+ try:
951
+ if feature_id is None or str(feature_id).strip() == "":
952
+ raise ValueError("Choose a feature id first.")
953
+ cues = [line for line in str(cue_text).splitlines() if line.strip()]
954
+ result = RUNTIME.feature_cue_scan(
955
+ feature_id=int(float(feature_id)),
956
+ layer=int(layer),
957
+ prompt_stem=prompt_stem,
958
+ cues=cues,
959
+ )
960
+ columns = ["Cue", "Full prompt", "Final token", "Activation", "Active in TopK"]
961
+ table = pd.DataFrame(result.rows, columns=columns)
962
+ chart = pd.DataFrame(result.chart_rows, columns=["Cue", "Activation"])
963
+ chart["Series"] = "Cue response"
964
+ return _cue_metrics_markdown(result), table, chart, _tsv(table)
965
+ except Exception as exc:
966
+ _raise_ui_error(exc)
967
+
968
+
969
+ def use_candidate_feature(candidate_id: str):
970
+ if candidate_id is None or str(candidate_id).strip() == "":
971
+ raise gr.Error("Run concept-guided discovery and choose a candidate first.")
972
+ value = str(int(float(candidate_id)))
973
+ return value, value, value, value
974
+
975
+
976
  def mode_help(mode: str):
977
  if mode == "ablate":
978
  return gr.update(value=0.0, interactive=False, label="Coefficient (unused for ablation)")
 
990
  with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
991
  gr.HTML(
992
  '<header class="hero">'
993
+ '<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.6</span></h1>'
994
  '<div class="subtitle">Causal Interpretability Workbench</div>'
995
  '<div class="metadata">Qwen3-1.7B-Base · Qwen-Scope residual SAEs · layers 4 / 14 / 26 · '
996
  '32,768 features · TopK=50 · ZeroGPU</div>'
 
999
  'also causally influence the model’s behaviour?</div>'
1000
  )
1001
 
1002
+ global_context = gr.Markdown(
1003
+ "**Current Workbench context:** none yet. Go to **Workbench → I. Inspect a prompt location**, "
1004
+ "set the prompt/layer/token, then click **Inspect sparse features**.",
1005
+ elem_classes=["context-card"],
1006
+ )
1007
+
1008
+ with gr.Tab("Start here"):
1009
+ gr.Markdown(
1010
+ "## Start here\n"
1011
+ "FeatureLens is organized as a short evidence workflow. You do **not** need to know a feature id before "
1012
+ "starting: inspect a prompt first, or use **Feature evidence → Concept-guided candidate discovery**."
1013
+ )
1014
+ with gr.Row(equal_height=True):
1015
+ gr.HTML(
1016
+ '<div class="start-card"><h3>1. Choose a location</h3>'
1017
+ '<p>In <b>Workbench</b>, enter a prompt, choose a residual layer and token, then click '
1018
+ '<b>Inspect sparse features</b>. This establishes the shared Workbench context.</p></div>'
1019
+ )
1020
+ gr.HTML(
1021
+ '<div class="start-card"><h3>2. Choose evidence</h3>'
1022
+ '<p>Pick a feature in the experiment you want to run. Feature selectors are populated from the latest '
1023
+ 'inspection but remain editable. Feature sets use 2–5 features.</p></div>'
1024
+ )
1025
+ gr.HTML(
1026
+ '<div class="start-card"><h3>3. Ask a causal question</h3>'
1027
+ '<p>Ablate/scale features, compare against norm-matched random controls, test dose response, '
1028
+ 'preference shifts, robustness, geometry, or concept association.</p></div>'
1029
+ )
1030
+ gr.Markdown(
1031
+ "### Plain-language glossary\n"
1032
+ "- **Token:** a chunk of text the model processes.\n"
1033
+ "- **Residual layer:** a checkpoint in the model's internal computation.\n"
1034
+ "- **SAE feature:** one sparse direction used by the autoencoder to represent a residual activation.\n"
1035
+ "- **Ablate:** remove that feature's current contribution. **Scale:** multiply it.\n"
1036
+ "- **Random control:** an equally large residual edit in an unrelated direction.\n"
1037
+ "- **Teacher-forced score:** probability assigned to exact continuation text, even if greedy generation does not change.\n\n"
1038
+ "**Tables:** use fullscreen/copy controls. **Plots:** use the top-right fullscreen button to zoom and the export button for PNG."
1039
+ )
1040
+
1041
  with gr.Tab("Workbench"):
1042
  gr.HTML('<div class="section-rule">I. Inspect a prompt location</div>')
1043
  with gr.Row(equal_height=False):
 
1101
  x_title="Feature id",
1102
  y_title="Activation",
1103
  x_label_angle=-35,
1104
+ buttons=["fullscreen", "export"],
1105
  height=330,
1106
  )
1107
 
 
1116
  choices=[],
1117
  allow_custom_value=True,
1118
  label="Single feature id",
1119
+ info="Populated from the latest inspection. You can choose another active feature or enter any valid id.",
1120
  )
1121
  mode = gr.Dropdown(
1122
  choices=["ablate", "scale", "inject"],
 
1166
  target_token_copy = _copy_button()
1167
 
1168
  gr.HTML('<div class="section-rule">III. Single-feature scale dose-response</div>')
1169
+ with gr.Accordion("Scale dose-response", open=False, elem_classes=["experiment-accordion"]):
1170
+ gr.Markdown("### Scale dose-response")
1171
+ gr.Markdown("**Uses current Workbench prompt / layer / token.** Choose the feature for this experiment below.")
1172
+ dose_feature_id = gr.Dropdown(
1173
+ choices=[],
1174
+ allow_custom_value=True,
1175
+ label="Dose-response feature id",
1176
+ info="Populated from the latest Workbench inspection; editable here.",
1177
+ )
1178
  gr.Markdown(
1179
  "Always a **scale** experiment: 0× = ablation, 1× = numerical no-edit reference, 2× = double "
1180
  "the native coefficient. The six conditions are evaluated together, and all deltas are measured "
 
1202
  title="Scale dose-response",
1203
  x_title="Feature multiplier",
1204
  y_title="Δ mean log p/token",
1205
+ buttons=["fullscreen", "export"],
1206
  height=330,
1207
  )
1208
 
1209
  gr.HTML('<div class="section-rule">IV. Contrastive continuation preference</div>')
1210
+ with gr.Accordion("Contrastive causal preference test", open=False, elem_classes=["experiment-accordion"]):
1211
+ gr.Markdown("### Contrastive causal preference test")
1212
+ gr.Markdown("**Uses current Workbench prompt / layer / token.** Select the feature explicitly below.")
1213
+ contrastive_feature_id = gr.Dropdown(
1214
+ choices=[],
1215
+ allow_custom_value=True,
1216
+ label="Contrastive feature id",
1217
+ info="Populated from the latest Workbench inspection; editable here.",
1218
+ )
1219
  gr.Markdown(
1220
  "Absolute target probability can move because an intervention broadly perturbs the distribution. "
1221
  "This test asks the stricter question: **does the edit shift preference between two exact continuations?** "
 
1261
  title="Preference between exact continuations",
1262
  x_title="Execution condition",
1263
  y_title="Sequence log-odds A−B",
1264
+ buttons=["fullscreen", "export"],
1265
  height=320,
1266
  )
1267
 
1268
  with gr.Tab("Feature sets"):
1269
  gr.Markdown(
1270
  "### Distributed sparse representations\n"
1271
+ "Run **Inspect sparse features** in the Workbench first. **Uses current Workbench context:** prompt, layer, "
1272
+ "and token are inherited from that inspection. The feature set below is always editable."
1273
  )
1274
  feature_set_location = gr.Markdown("No Workbench inspection has been run yet.")
1275
  feature_set_ids = gr.Dropdown(
 
1361
  title="Effect vs feature-set size",
1362
  x_title="Number of jointly ablated features",
1363
  y_title="Δ mean log p/token",
1364
+ buttons=["fullscreen", "export"],
1365
  height=330,
1366
  )
1367
 
 
1395
  x_title="Intervention condition",
1396
  y_title="Δ mean log p/token",
1397
  x_label_angle=-25,
1398
+ buttons=["fullscreen", "export"],
1399
  height=330,
1400
  )
1401
 
1402
 
1403
  gr.HTML('<div class="section-rule">Decoder geometry</div>')
1404
+ with gr.Accordion("Feature-set decoder geometry", open=False, elem_classes=["experiment-accordion"]):
1405
+ gr.Markdown("### Feature-set decoder geometry")
1406
  gr.Markdown(
1407
  "Joint causal effects can reflect both **decoder-direction geometry** and downstream non-linearity. "
1408
  "This zero-extra-generation diagnostic measures pairwise decoder cosines and compares the actual "
 
1433
  x_title="Feature pair",
1434
  y_title="Cosine similarity",
1435
  x_label_angle=-30,
1436
+ buttons=["fullscreen", "export"],
1437
  height=320,
1438
  )
1439
 
 
1444
  "across FeatureLens's seven controlled concept groups. The concept scan uses the feature's **maximum "
1445
  "activation across non-padding tokens in each prompt**, avoiding arbitrary final-token comparisons."
1446
  )
1447
+ gr.HTML('<div class="section-rule">A. Concept-guided candidate discovery</div>')
1448
+ gr.Markdown(
1449
+ "Don't know which feature id to inspect? Choose a controlled concept and let FeatureLens rank candidate "
1450
+ "features whose **prompt-wide maximum activation** is higher for that concept than for the other six groups. "
1451
+ "This is a live screening step, not a semantic label."
1452
+ )
1453
+ with gr.Row(equal_height=True):
1454
+ discovery_concept = gr.Dropdown(
1455
+ choices=[
1456
+ "code", "mathematics", "positive_sentiment", "negative_sentiment",
1457
+ "french_language", "factual_entities", "uncertainty"
1458
+ ],
1459
+ value="mathematics",
1460
+ label="Target concept",
1461
+ )
1462
+ discovery_layer = gr.Dropdown(choices=list(SETTINGS.layers), value=SETTINGS.layers[1], label="Residual layer")
1463
+ discovery_n = gr.Slider(2, 6, value=SETTINGS.contrast_prompts_per_concept, step=1, label="Prompts per concept")
1464
+ discovery_top_n = gr.Slider(5, 20, value=12, step=1, label="Candidate features")
1465
+ discovery_btn = gr.Button("Discover concept-associated candidates", variant="primary", elem_classes=["action-btn"])
1466
+ discovery_metrics = gr.Markdown()
1467
+ with gr.Row(equal_height=False):
1468
+ with gr.Column(scale=3):
1469
+ discovery_table = gr.Dataframe(
1470
+ interactive=False, label="Candidate feature contrast", buttons=["fullscreen"], wrap=False, max_height=380
1471
+ )
1472
+ discovery_tsv = gr.Textbox(visible="hidden")
1473
+ discovery_copy = _copy_button()
1474
+ with gr.Column(scale=2):
1475
+ discovery_plot = gr.BarPlot(
1476
+ x="Feature", y="Target − other mean", color="Series",
1477
+ color_map={"Concept contrast": INK_TEAL}, title="Candidate concept contrast",
1478
+ x_title="Feature id", y_title="Target − other mean max activation", x_label_angle=-35,
1479
+ buttons=["fullscreen", "export"], height=330
1480
+ )
1481
+ with gr.Row(equal_height=True):
1482
+ discovery_candidate = gr.Dropdown(choices=[], label="Discovered candidate feature id", allow_custom_value=True, scale=3)
1483
+ use_candidate_btn = gr.Button("Use candidate in feature experiments", variant="primary", elem_classes=["action-btn"], scale=2)
1484
+
1485
+ gr.HTML('<div class="section-rule">B. Inspect one feature</div>')
1486
+ contrast_location = gr.Markdown(
1487
+ "**Activation trace uses the current Workbench prompt.** The controlled concept scan below uses its own balanced prompt set."
1488
+ )
1489
  with gr.Row(equal_height=True):
1490
  contrast_feature_id = gr.Dropdown(
1491
  choices=[],
 
1508
  scale=2,
1509
  )
1510
 
1511
+ gr.Markdown("### Activation trace across the current Workbench prompt")
1512
  gr.Markdown(
1513
  "Uses the prompt currently entered in **Workbench** and shows exactly which prompt tokens include the "
1514
  "selected feature in the SAE TopK support."
 
1536
  x_title="Prompt token",
1537
  y_title="Activation",
1538
  x_label_angle=-35,
1539
+ buttons=["fullscreen", "export"],
1540
  height=320,
1541
  )
1542
 
1543
+ gr.HTML('<div class="section-rule">C. Completion-cue sensitivity</div>')
1544
+ gr.Markdown(
1545
+ "Feature 22632 in the current math example fires only on the final `is` token. This scan tests whether a "
1546
+ "selected feature responds to **completion cues themselves** rather than the underlying concept. Enter a stem "
1547
+ "and one cue per line; FeatureLens appends each cue and measures the feature at the final token."
1548
+ )
1549
+ with gr.Row(equal_height=True):
1550
+ cue_stem = gr.Textbox(label="Prompt stem", value="The derivative of x squared", lines=2, scale=3)
1551
+ cue_text = gr.Textbox(label="Completion cues (one per line)", value="is\n=\n:\nequals\ntherefore", lines=5, scale=2)
1552
+ cue_btn = gr.Button("Run completion-cue scan", variant="primary", elem_classes=["action-btn"])
1553
+ cue_metrics = gr.Markdown()
1554
+ with gr.Row(equal_height=False):
1555
+ with gr.Column(scale=3):
1556
+ cue_table = gr.Dataframe(interactive=False, label="Feature response by completion cue", buttons=["fullscreen"], wrap=False, max_height=340)
1557
+ cue_tsv = gr.Textbox(visible="hidden")
1558
+ cue_copy = _copy_button()
1559
+ with gr.Column(scale=2):
1560
+ cue_plot = gr.BarPlot(
1561
+ x="Cue", y="Activation", color="Series", color_map={"Cue response": INK_UMBER},
1562
+ title="Completion-cue feature response", x_title="Cue", y_title="Final-token activation",
1563
+ buttons=["fullscreen", "export"], height=320
1564
+ )
1565
+
1566
+ gr.HTML('<div class="section-rule">D. Controlled concept contrast for selected feature</div>')
1567
+ gr.Markdown("### Prompt-wide controlled concept contrast")
1568
  contrast_btn = gr.Button("Run controlled concept contrast", variant="primary", elem_classes=["action-btn"])
1569
  contrast_metrics = gr.Markdown()
1570
  with gr.Row(equal_height=False):
 
1588
  x_title="Concept",
1589
  y_title="Mean max activation",
1590
  x_label_angle=-25,
1591
+ buttons=["fullscreen", "export"],
1592
  height=330,
1593
  )
1594
 
 
1596
  gr.Markdown(
1597
  "### Local and prompt-wide robustness\n"
1598
  "The selected-token comparison is strict but can be misleading if the two chosen tokens play different "
1599
+ "semantic roles. FeatureLens reports a **prompt-wide max-pooled feature profile**: for every SAE "
1600
  "feature, take its maximum activation across all prompt tokens, then compare the two sparse profiles."
1601
  )
1602
  with gr.Row():
 
1649
  x_title="Feature id",
1650
  y_title="Activation",
1651
  x_label_angle=-35,
1652
+ buttons=["fullscreen", "export"],
1653
  height=330,
1654
  )
1655
 
 
1699
  title="Representation trajectory",
1700
  x_title="Layer",
1701
  y_title="Normalized value",
1702
+ buttons=["fullscreen", "export"],
1703
  height=330,
1704
  )
1705
 
 
1708
  gr.Markdown(
1709
  "The offline pipeline remains the source of held-out feature/concept AUROC + F1, reconstruction "
1710
  "quality, paraphrase stability, dense residual linear probes, and causal intervention conclusions. "
1711
+ "v0.6 strengthens the live workbench with batch-context null references and random-control ensembles; "
1712
  "the saved report should be generated only from real experiment artifacts."
1713
  )
1714
 
 
1729
 
1730
  FeatureLens patches the delta into the **original residual**; it never replaces the residual with the complete SAE reconstruction.
1731
 
1732
+ ### v0.6 control discipline
1733
 
1734
  Batched experiments include an explicit **zero-edit row**. Causal effects are measured against that row rather than a separately executed baseline, which removes batch-vs-single floating-point drift from the measured effect. Random specificity uses an ensemble of norm-matched residual directions rather than one arbitrary seed.
1735
 
 
1737
 
1738
  1. SAE reconstruction quality.
1739
  2. Held-out feature/concept prediction.
1740
+ 3. Concept-guided candidate discovery and selected-feature concept contrast.
1741
+ 4. Token-local activation traces and completion-cue sensitivity.
1742
+ 5. Local and prompt-wide paraphrase robustness.
1743
+ 6. Single-feature causal intervention and dose-response.
1744
+ 7. Contrastive continuation preference under intervention.
1745
+ 8. Joint feature-set intervention and set-size sensitivity.
1746
+ 9. Decoder-direction geometry and individual-vs-joint non-additivity.
1747
+ 10. Specificity relative to norm-matched random controls.
1748
 
1749
  Association, robustness, geometry, and intervention evidence remain separate claims.
1750
  """
 
1767
  feature_table,
1768
  feature_plot,
1769
  feature_id,
1770
+ dose_feature_id,
1771
+ contrastive_feature_id,
1772
  feature_set_ids,
1773
  contrast_feature_id,
1774
  contrast_layer,
1775
  analysis_metrics,
1776
  feature_set_location,
1777
  contrast_location,
1778
+ global_context,
1779
  feature_tsv,
1780
  ],
1781
  )
 
1795
  )
1796
  dose_btn.click(
1797
  run_dose_response,
1798
+ inputs=[prompt, layer, token_index, dose_feature_id, target_text],
1799
  outputs=[dose_table, dose_plot, dose_metrics, dose_tsv],
1800
  )
1801
  contrastive_mode.change(mode_help, inputs=[contrastive_mode], outputs=[contrastive_coefficient])
1802
  contrastive_btn.click(
1803
  run_contrastive_causal,
1804
+ inputs=[prompt, layer, token_index, contrastive_feature_id, contrastive_mode, contrastive_coefficient, contrastive_a, contrastive_b],
1805
  outputs=[contrastive_metrics, contrastive_table, contrastive_plot, contrastive_tsv],
1806
  )
1807
  set_mode.change(set_mode_help, inputs=[set_mode], outputs=[set_coefficient])
 
1835
  inputs=[contrast_feature_id, contrast_layer, contrast_n],
1836
  outputs=[contrast_metrics, contrast_table, contrast_plot, contrast_tsv],
1837
  )
1838
+ discovery_btn.click(
1839
+ run_concept_feature_discovery,
1840
+ inputs=[discovery_concept, discovery_layer, discovery_n, discovery_top_n],
1841
+ outputs=[discovery_metrics, discovery_table, discovery_plot, discovery_candidate, discovery_tsv],
1842
+ )
1843
+ use_candidate_btn.click(
1844
+ use_candidate_feature,
1845
+ inputs=[discovery_candidate],
1846
+ outputs=[feature_id, dose_feature_id, contrastive_feature_id, contrast_feature_id],
1847
+ queue=False,
1848
+ )
1849
+ cue_btn.click(
1850
+ run_feature_cue_scan,
1851
+ inputs=[contrast_feature_id, contrast_layer, cue_stem, cue_text],
1852
+ outputs=[cue_metrics, cue_table, cue_plot, cue_tsv],
1853
+ )
1854
  para_btn.click(
1855
  run_paraphrase_compare,
1856
  inputs=[para_a, para_b, para_layer, para_idx_a, para_idx_b, para_top_n],
 
1875
  (geometry_copy, geometry_tsv),
1876
  (trace_copy, trace_tsv),
1877
  (contrast_copy, contrast_tsv),
1878
+ (discovery_copy, discovery_tsv),
1879
+ (cue_copy, cue_tsv),
1880
  (para_copy, para_tsv),
1881
  (trajectory_copy, trajectory_tsv),
1882
  ]:
docs/HF_DEPLOY.md CHANGED
@@ -15,8 +15,11 @@ The live app does not need every SAE layer from the full repository.
15
 
16
  ## GPU-decorated actions
17
 
18
- Current v0.5 live actions include:
19
 
 
 
 
20
  - **Inspect sparse features**;
21
  - **Run single-feature causal test**;
22
  - **Run scale dose-response**;
 
15
 
16
  ## GPU-decorated actions
17
 
18
+ Current v0.6 live actions include:
19
 
20
+ - concept-guided candidate feature discovery and candidate reuse;
21
+ - completion-cue sensitivity scans;
22
+ - plot fullscreen/export controls and a persistent Workbench context banner;
23
  - **Inspect sparse features**;
24
  - **Run single-feature causal test**;
25
  - **Run scale dose-response**;
docs/METHODOLOGY.md CHANGED
@@ -264,7 +264,7 @@ The selected-token and prompt-wide metrics answer different questions and are in
264
 
265
  ## Feature-token activation trace
266
 
267
- For a selected SAE feature and layer, v0.5 can encode every prompt-token residual and report the feature's TopK activation token by token. This answers a basic localization question that a single selected-token view cannot: is the feature concentrated at one syntactic/semantic position, or does it recur across the prompt?
268
 
269
  The trace reports activation at each token, active-token count, and the peak token. A zero entry means that feature is not present in that token's TopK SAE support.
270
 
@@ -274,7 +274,7 @@ The trace reports activation at each token, active-token count, and the peak tok
274
 
275
  The live **Feature evidence** tab also provides an exploratory concept contrast for one selected feature.
276
 
277
- It samples a small balanced batch from the same seven controlled concept groups, using only one wording from each paraphrase pair. Earlier versions sampled only each prompt's final token, which can miss a feature that is active elsewhere in the prompt. v0.5 instead defines each prompt-level feature score as:
278
 
279
  ```text
280
  score(feature, prompt) = max over non-padding prompt tokens of z_feature
@@ -346,3 +346,32 @@ It then uses:
346
  A large point-estimate ratio alone is not sufficient for a strong causal-specificity narrative if paired uncertainty remains weak.
347
 
348
  FeatureLens keeps the raw rows even when the resulting conclusion is null, mixed, or contrary to the original hypothesis.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  ## Feature-token activation trace
266
 
267
+ For a selected SAE feature and layer, FeatureLens can encode every prompt-token residual and report the feature's TopK activation token by token. This answers a basic localization question that a single selected-token view cannot: is the feature concentrated at one syntactic/semantic position, or does it recur across the prompt?
268
 
269
  The trace reports activation at each token, active-token count, and the peak token. A zero entry means that feature is not present in that token's TopK SAE support.
270
 
 
274
 
275
  The live **Feature evidence** tab also provides an exploratory concept contrast for one selected feature.
276
 
277
+ It samples a small balanced batch from the same seven controlled concept groups, using only one wording from each paraphrase pair. Earlier versions sampled only each prompt's final token, which can miss a feature that is active elsewhere in the prompt. the current live scan instead defines each prompt-level feature score as:
278
 
279
  ```text
280
  score(feature, prompt) = max over non-padding prompt tokens of z_feature
 
346
  A large point-estimate ratio alone is not sufficient for a strong causal-specificity narrative if paired uncertainty remains weak.
347
 
348
  FeatureLens keeps the raw rows even when the resulting conclusion is null, mixed, or contrary to the original hypothesis.
349
+
350
+
351
+ ## v0.6 live candidate discovery
352
+
353
+ The selected-feature concept contrast asks **given a feature, where does it activate?** v0.6 adds the reverse live question: **given a controlled concept, which SAE features are plausible candidates to investigate?**
354
+
355
+ For a balanced batch with `n` prompts from each of the seven controlled groups, FeatureLens encodes every non-padding token and forms a prompt-wide feature profile by taking the maximum TopK activation of each SAE feature over the prompt. For target concept `c` and feature `f`, it reports:
356
+
357
+ ```text
358
+ target_mean(f) = mean prompt-wide max over prompts in c
359
+ other_mean(f) = mean prompt-wide max over prompts outside c
360
+ mean_difference(f) = target_mean(f) - other_mean(f)
361
+ selectivity(f) = mean_difference(f) / (target_mean(f) + other_mean(f) + eps)
362
+ ```
363
+
364
+ Candidates are ranked by positive `mean_difference`. The table also reports target/other activation rates and target maximum activation. This is intentionally **not a held-out labeler**: the same small batch is used for live screening. Semantic claims still require the offline grouped train/test feature evaluation.
365
+
366
+ ## v0.6 completion-cue sensitivity
367
+
368
+ The v0.5 math example showed feature `22632` active only at the final `is` token. That pattern motivates a lexical/structural control. For one prompt stem and a user-supplied list of completion cues, FeatureLens appends each cue, encodes the resulting prompt, and measures the selected feature at the final non-padding token.
369
+
370
+ The cue scan is intended to distinguish hypotheses such as:
371
+
372
+ - concept-linked activation;
373
+ - lexical activation tied to a particular token such as `is`;
374
+ - structural activation at a completion boundary;
375
+ - broad activation across several continuation cues.
376
+
377
+ It is a controlled diagnostic only. A cue response cannot establish the feature's complete semantics.
docs/VALIDATION.md CHANGED
@@ -1,8 +1,10 @@
1
- # FeatureLens v0.5.0 Validation
2
 
3
- This validation pass is intentionally focused on the **new v0.5 behavior and the core v0.4 regressions it could affect**. The full adversarial/edge-case battery is deferred to the final release, as requested.
4
 
5
- ## A. Local release gate
 
 
6
 
7
  From the repository root:
8
 
@@ -16,50 +18,33 @@ python3 scripts/release_check.py
16
  Expected:
17
 
18
  ```text
19
- 38 passed
20
- FeatureLens UI launch smoke: PASS (...)
21
  FeatureLens release check: PASS
22
  discovery prompts: 224
23
  causal tasks: 28
24
  layers: [4, 14, 26]
25
  feature-set sizes: [1, 3, 5]
26
  random controls: 8
27
- release: v0.5.0
28
  ```
29
 
30
- The UI smoke is important: it executes the real Gradio `launch()` path, which is where the v0.4 theme-font error occurred.
31
 
32
- ---
 
 
 
 
 
 
 
33
 
34
- ## B. Hugging Face startup and layout
35
 
36
- After pushing, inspect the container log.
37
 
38
- Expected startup:
39
-
40
- ```text
41
- * Running on local URL: http://0.0.0.0:7860
42
- ```
43
-
44
- There should be no SSR line, un-awaited coroutine warning, or theme/font traceback.
45
-
46
- ### Layout acceptance
47
-
48
- On a desktop-width browser:
49
-
50
- - The application should be **centered**, not pinned to the left edge.
51
- - The usable canvas should expand substantially farther across a wide monitor than v0.4.
52
- - Explicit action buttons and **Copy table with headers** buttons should use the same muted teal treatment.
53
- - Body copy, labels, controls, and headings should use a consistent serif-oriented typography system.
54
- - Tables should use the available width before horizontal scrolling is needed.
55
-
56
- The widest research tables may still need horizontal scrolling; that is preferable to wrapping numerical headings into unreadable multi-line columns.
57
-
58
- ---
59
-
60
- ## C. Core Workbench inspection
61
-
62
- In **Workbench** use:
63
 
64
  ```text
65
  Prompt: The derivative of x squared is
@@ -70,239 +55,170 @@ Displayed active features: 12
70
 
71
  Click **Inspect sparse features**.
72
 
73
- Expected regression values should remain approximately:
74
 
75
  ```text
76
- prompt token: 5
77
- Reconstruction cosine: 0.9015
 
78
  NMSE: 0.1874
79
  Top-5 activation mass: 26.7%
80
- Top feature: 22632
81
- Top activation: 32.90625
82
  ```
83
 
84
- The **Strongest active SAE features** table and **Activation profile** should populate.
85
-
86
- ### Copy feedback
87
 
88
- Click **Copy table with headers** below the feature table.
89
 
90
- Pass condition:
91
 
92
- 1. Clipboard content starts with the header row, e.g.
93
- ```text
94
- Rank<TAB>Feature id<TAB>Activation<TAB>Offline concept hint
95
- ```
96
- 2. The clicked button briefly changes to:
97
- ```text
98
- ✓ Copied with headers
99
- ```
100
- and then returns to **Copy table with headers**.
101
 
102
- ---
103
-
104
- ## D. Dynamic-height / embedded scrolling regression
105
-
106
- Still in **Workbench**, select feature `22632` and run:
107
 
108
  ```text
109
- Single-feature intervention: ablate
110
- Target continuation (optional): 2x
111
- Greedy generation length: 8
112
  ```
113
 
114
- Click **Run single-feature causal test**.
115
-
116
- After the result expands, **do not switch tabs**.
117
-
118
- Immediately try to scroll to:
119
 
120
- - **III. Single-feature scale dose-response**
121
- - **IV. Contrastive continuation preference**
122
- - and eventually the bottom of the Workbench tab.
123
 
124
- Pass condition: the page continues scrolling after the dynamic result appears. A tab switch should not be required to recover the missing lower portion of the page.
125
-
126
- v0.5 installs a browser-side resize/mutation observer specifically to request reflow after dynamic output-height changes. If the page still gets stuck, record whether the failure occurs on the Hugging Face Space page only or also when opening the direct `*.hf.space` app URL.
127
-
128
- ---
 
 
129
 
130
- ## E. Scale dose-response numerical-null regression
131
 
132
- Open **Scale dose-response** and click **Run scale dose-response** using target continuation `2x`.
133
 
134
- The `1.0` row must remain the exact batched numerical reference:
135
 
136
  ```text
137
- Multiplier: 1.0
138
- Δ feature coefficient: 0.0
139
- Perturbation L2: 0.0
140
- Δ mean log p/token: 0.0
141
- Δ sequence log p: 0.0
142
- Next-token JS: 0.0
143
  ```
144
 
145
- This is a hard regression requirement.
146
-
147
- ---
148
-
149
- ## F. Contrastive continuation preference — new v0.5 test
150
-
151
- In **Workbench → IV. Contrastive continuation preference**, use:
152
 
153
  ```text
 
154
  Continuation A (preferred): 2x
155
  Continuation B (comparison): x
156
  Contrastive intervention: ablate
157
  ```
158
 
159
- Use feature `22632`, layer `14`, prompt token `5` inherited from the Workbench.
160
-
161
  Click **Run contrastive preference test**.
162
 
163
- Expected outputs:
164
-
165
- - **Contrastive continuation scores** with two rows: `A (preferred)` and `B (comparison)`.
166
- - Exact sequence log-probability and mean log-probability/token for both continuations.
167
- - Baseline A−B sequence log-odds.
168
- - SAE-edited A−B sequence log-odds.
169
- - Causal A−B log-odds shift.
170
- - 8-control random magnitude mean/std, specificity ratio, and empirical tail p.
171
- - **Preference between exact continuations** chart.
172
 
173
- There is no predetermined scientific sign. The test passes if the quantities are finite and internally coherent.
174
 
175
- Interpretation rule: a strong JS shift with a weak contrastive preference shift supports **general distributional influence** more than selective preference control.
176
 
177
- ---
178
-
179
- ## G. Feature-token activation trace — new v0.5 test
180
-
181
- Open **Feature evidence**. The Workbench inspection should have populated:
182
 
183
  ```text
184
- Feature id: 22632
185
  Residual layer: 14
 
 
186
  ```
187
 
188
- Click **Trace feature across prompt tokens**.
189
-
190
- For the canonical prompt, the output should contain one row per prompt token. The final selected token (`is`, token index `5`) should reproduce the known feature activation approximately:
191
-
192
- ```text
193
- Feature 22632 at token 5 ≈ 32.90625
194
- ```
195
-
196
- Expected outputs:
197
 
198
- - active token count;
199
- - peak token position and activation;
200
- - **Feature activation by prompt token** table;
201
- - **Feature activation across prompt tokens** chart.
202
 
203
- This test directly reveals whether a feature is localized to one token or distributed across the prompt.
 
 
 
 
 
204
 
205
- ---
206
 
207
- ## H. Prompt-wide controlled concept contrast — corrected v0.5 test
208
 
209
- In **Feature evidence** use:
210
 
211
  ```text
212
- Feature id: 22632
213
- Residual layer: 14
214
- Prompts per concept: 4
215
  ```
216
 
217
- Click **Run controlled concept contrast**.
218
 
219
- v0.5 no longer evaluates only the final token. For each sampled prompt it takes the feature's **maximum activation across all non-padding prompt tokens**.
 
 
 
220
 
221
- The table columns should be:
222
 
223
- ```text
224
- Concept
225
- Prompts
226
- Mean prompt-wide max
227
- Median prompt-wide max
228
- Prompt activation rate
229
- Mean when active
230
- Max activation
231
- ```
232
-
233
- Two outcomes are valid:
234
-
235
- ### If at least one sampled prompt activates the feature
236
- The UI may report a highest-mean concept and a runner-up ratio.
237
 
238
- ### If all sampled activations remain zero
239
- The UI must say that the feature was **inactive in every sampled prompt**. It must **not** report `code` (or any other concept) as a winner and must not print an undefined winner ratio.
240
 
241
- ---
242
-
243
- ## I. Feature-set decoder geometry — new v0.5 test
244
-
245
- Open **Feature sets** after the canonical Workbench inspection. Select the top three features:
246
 
247
  ```text
248
- 22632
249
- 25695
250
- 16369
 
 
 
 
 
 
251
  ```
252
 
253
- Open **Feature-set decoder geometry** and click **Inspect selected-feature geometry**.
254
 
255
- Expected:
256
 
257
- - exactly three pair rows (`3 choose 2`);
258
- - decoder cosine for each pair;
259
- - mean absolute decoder cosine;
260
- - maximum absolute decoder cosine;
261
- - activation-weighted joint-ablation L2;
262
- - independent-direction root-sum-square L2 reference;
263
- - geometry ratio.
264
 
265
- Interpretation:
266
 
267
- ```text
268
- ratio < 1 -> net geometric cancellation
269
- ratio ≈ 1 -> approximately independent/orthogonal net norm
270
- ratio > 1 -> net geometric alignment
271
- ```
272
 
273
- This does not replace the individual-vs-joint causal test; it helps separate decoder geometry from downstream non-linearity.
274
 
275
- ---
 
 
 
 
 
276
 
277
- ## J. Existing feature-set and paraphrase regressions
278
 
279
- A full rerun is not necessary for every v0.4 panel. Do these quick checks:
280
 
281
- 1. **Run 1/3/5-feature ablation sweep** still produces rows for `k = 1, 3, 5` and 8-control statistics.
282
- 2. **Run individual-vs-joint decomposition** still produces three individual rows plus one joint row for the selected top three features.
283
- 3. In **Paraphrase robustness**, identical prompt vs identical prompt should still return:
284
- ```text
285
- selected-token Jaccard = 1.000
286
- selected-token cosine = 1.000
287
- prompt-wide Jaccard = 1.000
288
- prompt-wide cosine = 1.000
289
- ```
290
- 4. **Layer trajectory → Compare layers** still returns layers `4`, `14`, and `26`.
291
 
292
- ---
 
 
 
 
293
 
294
- ## Deferred final-release tests
295
 
296
- The comprehensive adversarial suite remains intentionally deferred until the project is feature-complete. The final release should include, at minimum:
297
 
298
- - empty prompts/targets;
299
- - invalid token indices;
300
- - feature IDs `0`, `32767`, and invalid `32768`;
301
- - inactive feature ablation/scale/injection semantics;
302
- - whitespace-sensitive continuations (`2x` vs ` 2x`);
303
- - unrelated and negated paraphrases;
304
- - mixed-language prompts;
 
305
  - long-prompt truncation;
306
- - rapid repeated clicks / ZeroGPU queue behavior;
307
- - narrow/mobile layout;
308
- - direct Space URL vs Hugging Face embedded-page scrolling.
 
1
+ # FeatureLens v0.6 validation
2
 
3
+ This guide tests the **new v0.6 behavior** plus a few high-value regressions. The comprehensive edge/adversarial suite remains deferred until the final hardening release, as agreed.
4
 
5
+ Use the exact UI labels below.
6
+
7
+ ## A. Local release gates
8
 
9
  From the repository root:
10
 
 
18
  Expected:
19
 
20
  ```text
21
+ 40 passed
22
+ FeatureLens UI launch smoke: PASS
23
  FeatureLens release check: PASS
24
  discovery prompts: 224
25
  causal tasks: 28
26
  layers: [4, 14, 26]
27
  feature-set sizes: [1, 3, 5]
28
  random controls: 8
29
+ release: v0.6.0
30
  ```
31
 
32
+ ## B. Navigation / typography / plot controls
33
 
34
+ 1. Open **Start here**.
35
+ 2. Confirm the three workflow cards and the plain-language glossary are readable without tiny heading text.
36
+ 3. Open **Workbench**.
37
+ 4. Confirm the hierarchy is visually descending: section title > subsection heading > explanatory body text.
38
+ 5. Confirm result-table column headers are not noticeably smaller than normal body text.
39
+ 6. After any plot is produced, use its top-right **fullscreen** button. The plot should open large enough for labels to be read comfortably.
40
+ 7. Confirm the adjacent **export** control produces a PNG.
41
+ 8. Confirm copy buttons still show `✓ Copied with headers` briefly.
42
 
43
+ Pass condition: no heading level appears smaller than the body text it introduces, tables remain readable, and every native plot has fullscreen/export controls.
44
 
45
+ ## C. Establish the shared Workbench context
46
 
47
+ In **Workbench → I. Inspect a prompt location** set:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  ```text
50
  Prompt: The derivative of x squared is
 
55
 
56
  Click **Inspect sparse features**.
57
 
58
+ Expected regression values are approximately:
59
 
60
  ```text
61
+ selected token: 5 (' is')
62
+ feature 22632 activation: 32.90625
63
+ reconstruction cosine: 0.9015
64
  NMSE: 0.1874
65
  Top-5 activation mass: 26.7%
 
 
66
  ```
67
 
68
+ The persistent **Current Workbench context** banner above the tabs must now state the inspected prompt, layer 14, and token 5. Switch to **Feature sets** and **Feature evidence**; the banner should remain visible.
 
 
69
 
70
+ ## D. Explicit experiment feature selectors
71
 
72
+ ### D1. Scale dose-response
73
 
74
+ Open **Workbench III. Single-feature scale dose-response Scale dose-response**.
 
 
 
 
 
 
 
 
75
 
76
+ Confirm there is a visible field named:
 
 
 
 
77
 
78
  ```text
79
+ Dose-response feature id
 
 
80
  ```
81
 
82
+ It should be populated from the latest inspection but editable independently. Set it to `22632` and run with target continuation `2x` from the single-feature causal panel.
 
 
 
 
83
 
84
+ The `1.0` multiplier row must remain the exact numerical null:
 
 
85
 
86
+ ```text
87
+ Δ feature coefficient = 0
88
+ Perturbation L2 = 0
89
+ Δ mean log p/token = 0
90
+ Δ sequence log p = 0
91
+ Next-token JS = 0
92
+ ```
93
 
94
+ ### D2. Contrastive continuation preference
95
 
96
+ Open **Workbench IV. Contrastive continuation preference Contrastive causal preference test**.
97
 
98
+ Confirm there is now a visible field named:
99
 
100
  ```text
101
+ Contrastive feature id
 
 
 
 
 
102
  ```
103
 
104
+ Set:
 
 
 
 
 
 
105
 
106
  ```text
107
+ Contrastive feature id: 22632
108
  Continuation A (preferred): 2x
109
  Continuation B (comparison): x
110
  Contrastive intervention: ablate
111
  ```
112
 
 
 
113
  Click **Run contrastive preference test**.
114
 
115
+ The v0.5 reference result was a causal sequence-log-odds shift of about `-0.0882`; small numerical variation is acceptable. The important regression is that the selected feature is explicit and the panel no longer silently inherits a hidden feature id.
 
 
 
 
 
 
 
 
116
 
117
+ ## E. Concept-guided candidate feature discovery
118
 
119
+ Open **Feature evidence A. Concept-guided candidate discovery**.
120
 
121
+ Set:
 
 
 
 
122
 
123
  ```text
124
+ Target concept: mathematics
125
  Residual layer: 14
126
+ Prompts per concept: 4
127
+ Candidate features: 12
128
  ```
129
 
130
+ Click **Discover concept-associated candidates**.
 
 
 
 
 
 
 
 
131
 
132
+ Verify:
 
 
 
133
 
134
+ - the result is a table named **Candidate feature contrast**;
135
+ - rows contain `Feature id`, target/other prompt-wide means, mean difference, selectivity score, and activation rates;
136
+ - candidates are ordered by positive `Target − other mean` evidence;
137
+ - the UI calls them **candidates**, not semantic labels;
138
+ - **Discovered candidate feature id** is populated when candidates exist;
139
+ - the plot has fullscreen/export controls.
140
 
141
+ An empty candidate list is not itself a software failure if the UI explicitly reports that no positively selective candidate was found. It must not invent a label.
142
 
143
+ ## F. Reuse a discovered candidate
144
 
145
+ After Test E, choose any value in **Discovered candidate feature id** and click:
146
 
147
  ```text
148
+ Use candidate in feature experiments
 
 
149
  ```
150
 
151
+ Verify that the same id appears in:
152
 
153
+ - **Workbench Single feature id**;
154
+ - **Workbench → Dose-response feature id**;
155
+ - **Workbench → Contrastive feature id**;
156
+ - **Feature evidence → Feature id**.
157
 
158
+ This is a UI/state-routing test only; it should not invoke the GPU.
159
 
160
+ ## G. Completion-cue sensitivity
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ Open **Feature evidence C. Completion-cue sensitivity**.
 
163
 
164
+ Use:
 
 
 
 
165
 
166
  ```text
167
+ Feature id: 22632
168
+ Residual layer: 14
169
+ Prompt stem: The derivative of x squared
170
+ Completion cues:
171
+ is
172
+ =
173
+ :
174
+ equals
175
+ therefore
176
  ```
177
 
178
+ Click **Run completion-cue scan**.
179
 
180
+ The `is` row reconstructs the original prompt `The derivative of x squared is`, so its final-token activation should be approximately `32.90625` and `Active in TopK = True`.
181
 
182
+ Record the remaining cue activations. Their scientific purpose is to test whether feature 22632 is strongly tied to a completion cue such as `is`, rather than to mathematics generally. Do **not** require the other cues to be zero; that is an empirical result.
 
 
 
 
 
 
183
 
184
+ ## H. Prompt-wide selected-feature contrast regression
185
 
186
+ Still in **Feature evidence**, use feature `22632`, layer `14`, 4 prompts/concept and click **Run controlled concept contrast**.
 
 
 
 
187
 
188
+ Reference v0.5 behavior:
189
 
190
+ ```text
191
+ active in 7/28 prompts
192
+ uncertainty mean ≈ 7.55
193
+ mathematics mean ≈ 5.42
194
+ french_language mean = 0
195
+ ```
196
 
197
+ Small numerical variation is acceptable. This verifies the existing prompt-wide selected-feature scan remains unchanged while the new candidate-discovery scan answers the reverse question: *given a concept, which features should I investigate?*
198
 
199
+ ## I. Quick existing regressions
200
 
201
+ You do not need to resend full tables unless something changes unexpectedly.
 
 
 
 
 
 
 
 
 
202
 
203
+ 1. **Paraphrase robustness**: default pair should remain roughly selected-token cosine `0.301`, prompt-wide cosine `0.981`.
204
+ 2. **Identical-prompt control**: all four robustness metrics should remain `1.000`.
205
+ 3. **Layer trajectory**: layer-14 reconstruction should remain the weakest of layers 4/14/26 for the default prompt.
206
+ 4. **Feature sets → Set-size sensitivity → Run 1/3/5-feature ablation sweep**: this is the “top-k sweep.” It is **not** the basic Workbench inspection. Only confirm the experiment still runs and produces k = 1, 3, 5 rows.
207
+ 5. Scroll all the way to **End of workbench** after dynamic outputs expand; no tab switch should be necessary.
208
 
209
+ ## J. Deferred final hardening
210
 
211
+ Do not spend ZeroGPU quota on the complete adversarial suite yet. Keep these for the final release:
212
 
213
+ - empty prompts;
214
+ - out-of-range token indices;
215
+ - feature ids `0`, `32767`, invalid `32768`;
216
+ - inactive feature ablation vs injection;
217
+ - whitespace-sensitive targets such as `2x` vs ` 2x`;
218
+ - missing required target continuations;
219
+ - unrelated paraphrases;
220
+ - negation and mixed-language prompts;
221
  - long-prompt truncation;
222
+ - rapid repeated clicks / queue behavior;
223
+ - narrow/mobile-width layout;
224
+ - final table-copy and plot fullscreen/export sweep.
featurelens/runtime.py CHANGED
@@ -227,6 +227,28 @@ class ContrastiveCausalResult:
227
  random_control_count: int
228
 
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  @dataclass
231
  class ParaphraseResult:
232
  tokens_a: list[str]
@@ -1749,6 +1771,199 @@ class FeatureLensRuntime:
1749
  total_prompt_count=len(values),
1750
  )
1751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1752
 
1753
  RUNTIME = FeatureLensRuntime()
1754
 
 
227
  random_control_count: int
228
 
229
 
230
+ @dataclass
231
+ class ConceptFeatureDiscoveryResult:
232
+ concept: str
233
+ layer: int
234
+ prompts_per_concept: int
235
+ top_n: int
236
+ rows: list[list[object]]
237
+ chart_rows: list[list[object]]
238
+ candidate_ids: list[int]
239
+
240
+
241
+ @dataclass
242
+ class FeatureCueScanResult:
243
+ feature_id: int
244
+ layer: int
245
+ prompt_stem: str
246
+ rows: list[list[object]]
247
+ chart_rows: list[list[object]]
248
+ active_cue_count: int
249
+ cue_count: int
250
+
251
+
252
  @dataclass
253
  class ParaphraseResult:
254
  tokens_a: list[str]
 
1771
  total_prompt_count=len(values),
1772
  )
1773
 
1774
+ @torch.inference_mode()
1775
+ def concept_feature_discovery(
1776
+ self,
1777
+ concept: str,
1778
+ layer: int,
1779
+ prompts_per_concept: int | None = None,
1780
+ top_n: int = 12,
1781
+ ) -> ConceptFeatureDiscoveryResult:
1782
+ """Find SAE features whose prompt-wide activation is higher for one controlled concept."""
1783
+ self.ensure_ready(preload_saes=False)
1784
+ assert self.model is not None and self.tokenizer is not None and self.sae_store is not None
1785
+ concept = str(concept).strip()
1786
+ if not concept:
1787
+ raise ValueError('Choose a target concept.')
1788
+ n = int(prompts_per_concept or self.settings.contrast_prompts_per_concept)
1789
+ if n < 1 or n > 8:
1790
+ raise ValueError('Discovery prompts per concept must be between 1 and 8.')
1791
+ top_n = int(top_n)
1792
+ if top_n < 1 or top_n > 25:
1793
+ raise ValueError('Number of candidate features must be between 1 and 25.')
1794
+ rows = self._contrast_prompt_rows(n)
1795
+ available = sorted({str(row['concept']) for row in rows})
1796
+ if concept not in available:
1797
+ raise ValueError(f'Concept must be one of {available}.')
1798
+
1799
+ texts = [str(row['text']) for row in rows]
1800
+ batch = self.tokenizer(
1801
+ texts,
1802
+ return_tensors='pt',
1803
+ padding=True,
1804
+ truncation=True,
1805
+ max_length=self.settings.max_prompt_tokens,
1806
+ )
1807
+ batch = {key: value.to(self.device) for key, value in batch.items()}
1808
+ capture: dict = {}
1809
+ with self._capture_hook(int(layer), capture):
1810
+ self.model(**batch, use_cache=False)
1811
+ sae = self.sae_store.get(int(layer))
1812
+ encoding = sae.encode(capture['hidden'])
1813
+ attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool()
1814
+ valid = attention.unsqueeze(-1).expand_as(encoding.values)
1815
+ values = torch.where(valid, encoding.values, torch.zeros_like(encoding.values)).float()
1816
+
1817
+ dense = torch.zeros(
1818
+ (values.shape[0], self.settings.sae_width),
1819
+ device=values.device,
1820
+ dtype=torch.float32,
1821
+ )
1822
+ dense.scatter_reduce_(
1823
+ 1,
1824
+ encoding.indices.reshape(values.shape[0], -1),
1825
+ values.reshape(values.shape[0], -1),
1826
+ reduce='amax',
1827
+ include_self=True,
1828
+ )
1829
+ target_mask = torch.tensor(
1830
+ [str(row['concept']) == concept for row in rows],
1831
+ device=dense.device,
1832
+ dtype=torch.bool,
1833
+ )
1834
+ other_mask = ~target_mask
1835
+ target = dense[target_mask]
1836
+ other = dense[other_mask]
1837
+ target_mean = target.mean(dim=0)
1838
+ other_mean = other.mean(dim=0)
1839
+ target_rate = (target > 0).float().mean(dim=0)
1840
+ other_rate = (other > 0).float().mean(dim=0)
1841
+ mean_diff = target_mean - other_mean
1842
+ selectivity = mean_diff / (target_mean + other_mean + 1e-8)
1843
+
1844
+ eligible = (target_mean > 0) & (mean_diff > 0)
1845
+ candidate_idx = torch.nonzero(eligible, as_tuple=False).flatten()
1846
+ if candidate_idx.numel() == 0:
1847
+ return ConceptFeatureDiscoveryResult(
1848
+ concept=concept,
1849
+ layer=int(layer),
1850
+ prompts_per_concept=n,
1851
+ top_n=top_n,
1852
+ rows=[],
1853
+ chart_rows=[],
1854
+ candidate_ids=[],
1855
+ )
1856
+ order = torch.argsort(mean_diff[candidate_idx], descending=True)
1857
+ candidate_idx = candidate_idx[order[:top_n]]
1858
+
1859
+ table_rows: list[list[object]] = []
1860
+ chart_rows: list[list[object]] = []
1861
+ for rank, feature_tensor in enumerate(candidate_idx, start=1):
1862
+ fid = int(feature_tensor.item())
1863
+ row = [
1864
+ rank,
1865
+ fid,
1866
+ float(target_mean[fid].item()),
1867
+ float(other_mean[fid].item()),
1868
+ float(mean_diff[fid].item()),
1869
+ float(selectivity[fid].item()),
1870
+ float(target_rate[fid].item()),
1871
+ float(other_rate[fid].item()),
1872
+ float(target[:, fid].max().item()),
1873
+ ]
1874
+ table_rows.append(row)
1875
+ chart_rows.append([str(fid), float(mean_diff[fid].item())])
1876
+ return ConceptFeatureDiscoveryResult(
1877
+ concept=concept,
1878
+ layer=int(layer),
1879
+ prompts_per_concept=n,
1880
+ top_n=top_n,
1881
+ rows=table_rows,
1882
+ chart_rows=chart_rows,
1883
+ candidate_ids=[int(x.item()) for x in candidate_idx],
1884
+ )
1885
+
1886
+ @staticmethod
1887
+ def _cue_prompt(stem: str, cue: str) -> str:
1888
+ stem = stem.rstrip()
1889
+ cue = cue.strip()
1890
+ if not cue:
1891
+ return stem
1892
+ if cue[0] in ':;,.!?=)]}':
1893
+ return stem + cue
1894
+ return stem + ' ' + cue
1895
+
1896
+ @torch.inference_mode()
1897
+ def feature_cue_scan(
1898
+ self,
1899
+ feature_id: int,
1900
+ layer: int,
1901
+ prompt_stem: str,
1902
+ cues: Sequence[str],
1903
+ ) -> FeatureCueScanResult:
1904
+ """Measure one feature at the final token after appending controlled completion cues."""
1905
+ if not prompt_stem.strip():
1906
+ raise ValueError('Enter a prompt stem.')
1907
+ cue_list: list[str] = []
1908
+ seen: set[str] = set()
1909
+ for raw in cues:
1910
+ cue = str(raw).strip()
1911
+ if not cue or cue in seen:
1912
+ continue
1913
+ seen.add(cue)
1914
+ cue_list.append(cue)
1915
+ if not cue_list:
1916
+ raise ValueError('Enter at least one cue.')
1917
+ if len(cue_list) > 12:
1918
+ raise ValueError('Cue scan supports at most 12 cues per run.')
1919
+ if int(feature_id) < 0 or int(feature_id) >= self.settings.sae_width:
1920
+ raise ValueError(f'Feature id must be in [0, {self.settings.sae_width - 1}].')
1921
+
1922
+ self.ensure_ready(preload_saes=False)
1923
+ assert self.model is not None and self.tokenizer is not None and self.sae_store is not None
1924
+ prompts = [self._cue_prompt(prompt_stem, cue) for cue in cue_list]
1925
+ batch = self.tokenizer(
1926
+ prompts,
1927
+ return_tensors='pt',
1928
+ padding=True,
1929
+ truncation=True,
1930
+ max_length=self.settings.max_prompt_tokens,
1931
+ )
1932
+ batch = {key: value.to(self.device) for key, value in batch.items()}
1933
+ capture: dict = {}
1934
+ with self._capture_hook(int(layer), capture):
1935
+ self.model(**batch, use_cache=False)
1936
+ sae = self.sae_store.get(int(layer))
1937
+ encoding = sae.encode(capture['hidden'])
1938
+ attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool()
1939
+
1940
+ rows: list[list[object]] = []
1941
+ chart_rows: list[list[object]] = []
1942
+ active_count = 0
1943
+ for row_idx, (cue, full_prompt) in enumerate(zip(cue_list, prompts, strict=True)):
1944
+ valid_positions = torch.nonzero(attention[row_idx], as_tuple=False).flatten()
1945
+ final_pos = int(valid_positions[-1].item())
1946
+ indices = encoding.indices[row_idx, final_pos]
1947
+ values = encoding.values[row_idx, final_pos]
1948
+ mask = indices == int(feature_id)
1949
+ activation = float(values[mask][0].item()) if bool(mask.any()) else 0.0
1950
+ active = activation > 0
1951
+ active_count += int(active)
1952
+ token_id = int(batch['input_ids'][row_idx, final_pos].item())
1953
+ final_token = self.tokenizer.decode([token_id])
1954
+ rows.append([cue, full_prompt, repr(final_token), activation, active])
1955
+ chart_rows.append([cue, activation])
1956
+
1957
+ return FeatureCueScanResult(
1958
+ feature_id=int(feature_id),
1959
+ layer=int(layer),
1960
+ prompt_stem=prompt_stem,
1961
+ rows=rows,
1962
+ chart_rows=chart_rows,
1963
+ active_cue_count=active_count,
1964
+ cue_count=len(cue_list),
1965
+ )
1966
+
1967
 
1968
  RUNTIME = FeatureLensRuntime()
1969
 
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "featurelens"
3
- version = "0.5.0"
4
  description = "Causal sparse-feature interpretability workbench for Qwen3 and Qwen-Scope SAEs"
5
  requires-python = ">=3.10"
6
 
 
1
  [project]
2
  name = "featurelens"
3
+ version = "0.6.0"
4
  description = "Causal sparse-feature interpretability workbench for Qwen3 and Qwen-Scope SAEs"
5
  requires-python = ">=3.10"
6
 
research_config.json CHANGED
@@ -89,5 +89,16 @@
89
  "feature_token_activation_trace",
90
  "contrastive_continuation_preference_test",
91
  "feature_decoder_geometry"
 
 
 
 
 
 
 
 
 
 
 
92
  ]
93
  }
 
89
  "feature_token_activation_trace",
90
  "contrastive_continuation_preference_test",
91
  "feature_decoder_geometry"
92
+ ],
93
+ "concept_candidate_discovery_metric": "prompt-wide target-minus-other mean max activation; exploratory only",
94
+ "completion_cue_scan": "final-token feature activation after controlled suffix/cue substitution",
95
+ "live_features_v0_6": [
96
+ "start_here_plain_language_onboarding",
97
+ "persistent_workbench_context_banner",
98
+ "explicit_per_experiment_feature_selectors",
99
+ "plot_fullscreen_and_export_controls",
100
+ "consistent_heading_and_table_typography",
101
+ "concept_guided_candidate_feature_discovery",
102
+ "completion_cue_sensitivity_scan"
103
  ]
104
  }
scripts/release_check.py CHANGED
@@ -120,6 +120,22 @@ def check_config(config: dict) -> None:
120
  f'{sorted(actual_live_v05)}'
121
  )
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  def check_datasets(config: dict) -> tuple[list[dict], list[dict]]:
125
  prompts = load_jsonl(ROOT / 'data' / 'prompts.jsonl')
@@ -176,23 +192,26 @@ def check_readme() -> None:
176
  'full-continuation',
177
  'feature-set',
178
  'paraphrase',
179
- 'random ensemble',
180
  'batched zero-edit',
181
  'concept contrast',
182
  'non-additivity',
183
  'contrastive',
184
  'decoder geometry',
185
  'token activation',
 
 
 
186
  ]
187
  missing = [value for value in required_strings if value.lower() not in readme.lower()]
188
  if missing:
189
- raise SystemExit(f'README.md is missing required v0.5 content: {missing}')
190
 
191
 
192
  def check_pyproject() -> None:
193
  text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
194
- if 'version = "0.5.0"' not in text:
195
- raise SystemExit('pyproject.toml must declare version 0.5.0.')
196
 
197
 
198
  def main() -> None:
@@ -210,7 +229,7 @@ def main() -> None:
210
  print(f' layers: {config["layers"]}')
211
  print(f' feature-set sizes: {config["feature_set_sizes"]}')
212
  print(f' random controls: {config["live_random_controls"]}')
213
- print(' release: v0.5.0')
214
 
215
 
216
  if __name__ == '__main__':
 
120
  f'{sorted(actual_live_v05)}'
121
  )
122
 
123
+ required_live_v06 = {
124
+ 'start_here_plain_language_onboarding',
125
+ 'persistent_workbench_context_banner',
126
+ 'explicit_per_experiment_feature_selectors',
127
+ 'plot_fullscreen_and_export_controls',
128
+ 'consistent_heading_and_table_typography',
129
+ 'concept_guided_candidate_feature_discovery',
130
+ 'completion_cue_sensitivity_scan',
131
+ }
132
+ actual_live_v06 = set(config.get('live_features_v0_6', []))
133
+ if actual_live_v06 != required_live_v06:
134
+ raise SystemExit(
135
+ 'research_config.json live_features_v0_6 mismatch: '
136
+ f'{sorted(actual_live_v06)}'
137
+ )
138
+
139
 
140
  def check_datasets(config: dict) -> tuple[list[dict], list[dict]]:
141
  prompts = load_jsonl(ROOT / 'data' / 'prompts.jsonl')
 
192
  'full-continuation',
193
  'feature-set',
194
  'paraphrase',
195
+ 'random-control ensemble',
196
  'batched zero-edit',
197
  'concept contrast',
198
  'non-additivity',
199
  'contrastive',
200
  'decoder geometry',
201
  'token activation',
202
+ 'concept-guided candidate',
203
+ 'completion-cue',
204
+ 'start here',
205
  ]
206
  missing = [value for value in required_strings if value.lower() not in readme.lower()]
207
  if missing:
208
+ raise SystemExit(f'README.md is missing required v0.6 content: {missing}')
209
 
210
 
211
  def check_pyproject() -> None:
212
  text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
213
+ if 'version = "0.6.0"' not in text:
214
+ raise SystemExit('pyproject.toml must declare version 0.6.0.')
215
 
216
 
217
  def main() -> None:
 
229
  print(f' layers: {config["layers"]}')
230
  print(f' feature-set sizes: {config["feature_set_sizes"]}')
231
  print(f' random controls: {config["live_random_controls"]}')
232
+ print(' release: v0.6.0')
233
 
234
 
235
  if __name__ == '__main__':
tests/test_live_runtime_helpers.py CHANGED
@@ -214,3 +214,28 @@ def test_concept_contrast_promptwide_scan_runs_on_toy_runtime() -> None:
214
  assert len(result.rows) == 7
215
  assert all(len(row) == 7 for row in result.rows)
216
  assert 0 <= result.active_prompt_count <= result.total_prompt_count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  assert len(result.rows) == 7
215
  assert all(len(row) == 7 for row in result.rows)
216
  assert 0 <= result.active_prompt_count <= result.total_prompt_count
217
+
218
+
219
+ def test_concept_feature_discovery_runs_on_toy_runtime() -> None:
220
+ runtime = make_runtime()
221
+ result = runtime.concept_feature_discovery(
222
+ concept='mathematics', layer=0, prompts_per_concept=1, top_n=3
223
+ )
224
+ assert result.concept == 'mathematics'
225
+ assert len(result.rows) <= 3
226
+ assert result.candidate_ids == [int(row[1]) for row in result.rows]
227
+ assert all(len(row) == 9 for row in result.rows)
228
+
229
+
230
+ def test_feature_cue_scan_runs_on_toy_runtime() -> None:
231
+ runtime = make_runtime()
232
+ result = runtime.feature_cue_scan(
233
+ feature_id=0,
234
+ layer=0,
235
+ prompt_stem='abc',
236
+ cues=['is', '=', ':'],
237
+ )
238
+ assert result.cue_count == 3
239
+ assert len(result.rows) == 3
240
+ assert all(len(row) == 5 for row in result.rows)
241
+ assert 0 <= result.active_cue_count <= result.cue_count