mechbench-schema 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: mechbench-schema
3
+ Version: 0.13.0
4
+ Summary: Typed emission contract for the mechbench family. Pydantic models as single source of truth; generates TypeScript bindings for mechbench-ui.
5
+ Author-email: Benji Smith <benji@shaxpir.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mechbench/mechbench-schema
8
+ Project-URL: Website, https://mechbench.ai
9
+ Keywords: interpretability,schema,pydantic,mechbench
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: pydantic>=2.6
18
+ Requires-Dist: cbor2>=5.6
19
+ Provides-Extra: codegen
20
+ Requires-Dist: datamodel-code-generator[typescript]>=0.25; extra == "codegen"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Requires-Dist: ruff>=0.5; extra == "dev"
24
+
25
+ # mechbench-schema
26
+
27
+ The typed emission contract for the [mechbench](https://mechbench.ai) family.
28
+
29
+ **Pydantic models are the single source of truth.** TypeScript bindings are generated from them; both halves are published from this one repo to two package registries.
30
+
31
+ | Consumer | Registry | Package |
32
+ | --- | --- | --- |
33
+ | Python (core, agent, remote, experiments) | PyPI | `pip install mechbench-schema` |
34
+ | TypeScript (ui, skills) | npm | `npm install mechbench-schema` |
35
+
36
+ The two packages share a name and a version. CI regenerates the TS bindings and fails on drift, so the two halves cannot disagree.
37
+
38
+ ## Organization: by domain axis, not by consumer
39
+
40
+ Modules are named after the **indexing axis** of the records they carry, not after who renders them. This keeps the schema from becoming "chart data" just because charts are the current biggest consumer; the same records drive CSV exports, agent tool surfaces, the memoization cache, and anything else downstream.
41
+
42
+ | module | category | archetypal records |
43
+ |---|---|---|
44
+ | `per_layer_data` | indexed by `layer` | ablation damage, DLA diffs, convergence summaries |
45
+ | `per_head_data` | indexed by `(layer, head)` | per-head DLA, OV rank-0 singular values |
46
+ | `attention_trace` | indexed by `(layer, head, pos_from, pos_to)` | attention patterns |
47
+ | `per_layer_per_position_data` | indexed by `(layer, position)` | logit-lens trajectories, causal-trace grids |
48
+ | `vector_data` | atomic: one direction in residual space | captured / steering / probe / centroid vectors |
49
+ | `cluster_data` | collection of vectors with aggregate stats | named clusters, cross-cluster statistics |
50
+ | `identity` | path grammar | `MechbenchPath` validated-string type |
51
+
52
+ Every payload model is part of a discriminated union keyed by `kind` (or `origin` for vectors), so the UI can dispatch rendering on the tag and the DAG solver can reason about compatible outputs.
53
+
54
+ ## Repo layout
55
+
56
+ ```
57
+ mechbench-schema/
58
+ ├── src/mechbench_schema/ # Pydantic models — the source of truth
59
+ │ ├── __init__.py # __all__ + __schema_all__
60
+ │ ├── attention_trace.py
61
+ │ ├── cluster_data.py
62
+ │ ├── identity.py
63
+ │ ├── per_head_data.py
64
+ │ ├── per_layer_data.py
65
+ │ ├── per_layer_per_position_data.py
66
+ │ └── vector_data.py
67
+ ├── ts/ # published to npm as "mechbench-schema"
68
+ │ ├── package.json
69
+ │ ├── tsconfig.json
70
+ │ └── src/
71
+ │ ├── index.ts
72
+ │ ├── generated.ts # codegen output — DO NOT EDIT
73
+ │ └── schema.json # consolidated JSON Schema — codegen output
74
+ ├── scripts/
75
+ │ └── codegen.py # Pydantic → JSON Schema → TypeScript
76
+ ├── pyproject.toml # published to PyPI as "mechbench-schema"
77
+ └── README.md
78
+ ```
79
+
80
+ ## Editing the contract
81
+
82
+ 1. Edit an existing module under `src/mechbench_schema/`, or add a new module (following the domain-axis convention).
83
+ 2. Export the new type from `src/mechbench_schema/__init__.py`. Add it to `__all__` for Python consumers and to `__schema_all__` for codegen (wire types only).
84
+ 3. Run `python scripts/codegen.py`. This writes `ts/src/schema.json` and `ts/src/generated.ts`.
85
+ 4. Commit everything in one PR — Python source, generated JSON Schema, generated TypeScript.
86
+
87
+ CI runs codegen and `git diff --exit-code ts/src/schema.json ts/src/generated.ts`. A drifted PR cannot land.
88
+
89
+ The codegen uses `json-schema-to-typescript` (via `npx`) for the TS emission. Python → JSON Schema comes from Pydantic's built-in `model_json_schema()` (for `BaseModel` subclasses) and `TypeAdapter(...).json_schema()` (for type aliases like the discriminated unions).
90
+
91
+ ## Install (Python)
92
+
93
+ ```bash
94
+ pip install mechbench-schema
95
+ ```
96
+
97
+ ```python
98
+ from mechbench_schema import LayerAblationPayload, DlaSweepPayload
99
+
100
+ payload = LayerAblationPayload(
101
+ experiment="step_02_layer_ablation",
102
+ description="Per-layer ablation damage on FACTUAL_15.",
103
+ model="mlx-community/gemma-4-E4B-it-bf16",
104
+ n_layers=42,
105
+ global_layers=[5, 11, 17, 23, 29, 35, 41],
106
+ prompts=[...],
107
+ aggregates=LayerAggregates(mean=[...], median=[...]),
108
+ )
109
+
110
+ # Emit to JSON — mode="json" canonicalizes datetimes, bytes, etc.
111
+ import json
112
+ path.write_text(json.dumps(payload.model_dump(mode="json"), indent=2))
113
+ ```
114
+
115
+ ## Install (TypeScript)
116
+
117
+ ```bash
118
+ npm install mechbench-schema
119
+ ```
120
+
121
+ ```ts
122
+ import type { LayerAblationPayload, Vector, MechbenchPath } from "mechbench-schema";
123
+
124
+ const payload: LayerAblationPayload = JSON.parse(text);
125
+ // All fields, discriminated unions, and nested types come from the generated bindings.
126
+ ```
127
+
128
+ ## Identity grammar
129
+
130
+ `MechbenchPath` is the type used to address every object in the mechbench family — charts, articles, experiments, corpuses, probes, cached DAG intermediates. Five categories:
131
+
132
+ - `<owner>/<project>/<folders>/<leaf>` — user-named
133
+ - `~canonical/<area>/<path>/<leaf>` — human-readable aliases for globally-shared content
134
+ - `~system/<area>/<path>/<leaf>` — platform / first-party
135
+ - `~hash/<algo>:<digest>` — global content-hashed (deduplicated across users)
136
+ - `<owner>/<project>/~hash/<algo>:<digest>` — workspace-scoped content-hashed
137
+
138
+ Full spec in the meta repo's IDENTITY_AND_NAMESPACING.md. The Python side (`mechbench_schema.identity`) owns the authoritative validator; a mirror implementation lives in `mechbench-ui/src/lib/mechbenchPath.ts` for UI-side early failure.
139
+
140
+ ## Why one repo, two publications
141
+
142
+ Schemas that live in two repos drift. The only invariant that matters — "Python and TS agree on the shape" — is enforced by keeping the source in one place and generating the target. Consumers never need the other language's toolchain to install; `pip` and `npm` each resolve to a clean single-language package.
143
+
144
+ See [the family overview](https://mechbench.ai) for the rationale behind this and other multi-repo decisions.
145
+
146
+ ## Status
147
+
148
+ Version 0.8.0. Seven modules in active use; the legacy `records.py` holding pen was retired when the domain-axis reorg completed. `mechbench-experiments`' two exporters (`step_02_layer_ablation`, `step_33_dla_factual_sweep`) emit via Pydantic models; `mechbench-ui`'s chart interfaces are one-line aliases over the generated TS types.
149
+
150
+ Open work lives in the meta repo's `tasks/mechbench-schema/` directory. The two notable open epics downstream of this repo:
151
+
152
+ - **`000161`** — compact binary formats for records at rest and in transit (safetensors for tensor-bulk, parquet for record-collections).
153
+ - **`000163`** — the identity-and-namespacing epic that produced `MechbenchPath`; Phase 4 (content-addressing grammar) remains open and coordinates with `000162` (the DAG-solver epic in `mechbench-compute`).
154
+
155
+ ## License
156
+
157
+ MIT.
@@ -0,0 +1,133 @@
1
+ # mechbench-schema
2
+
3
+ The typed emission contract for the [mechbench](https://mechbench.ai) family.
4
+
5
+ **Pydantic models are the single source of truth.** TypeScript bindings are generated from them; both halves are published from this one repo to two package registries.
6
+
7
+ | Consumer | Registry | Package |
8
+ | --- | --- | --- |
9
+ | Python (core, agent, remote, experiments) | PyPI | `pip install mechbench-schema` |
10
+ | TypeScript (ui, skills) | npm | `npm install mechbench-schema` |
11
+
12
+ The two packages share a name and a version. CI regenerates the TS bindings and fails on drift, so the two halves cannot disagree.
13
+
14
+ ## Organization: by domain axis, not by consumer
15
+
16
+ Modules are named after the **indexing axis** of the records they carry, not after who renders them. This keeps the schema from becoming "chart data" just because charts are the current biggest consumer; the same records drive CSV exports, agent tool surfaces, the memoization cache, and anything else downstream.
17
+
18
+ | module | category | archetypal records |
19
+ |---|---|---|
20
+ | `per_layer_data` | indexed by `layer` | ablation damage, DLA diffs, convergence summaries |
21
+ | `per_head_data` | indexed by `(layer, head)` | per-head DLA, OV rank-0 singular values |
22
+ | `attention_trace` | indexed by `(layer, head, pos_from, pos_to)` | attention patterns |
23
+ | `per_layer_per_position_data` | indexed by `(layer, position)` | logit-lens trajectories, causal-trace grids |
24
+ | `vector_data` | atomic: one direction in residual space | captured / steering / probe / centroid vectors |
25
+ | `cluster_data` | collection of vectors with aggregate stats | named clusters, cross-cluster statistics |
26
+ | `identity` | path grammar | `MechbenchPath` validated-string type |
27
+
28
+ Every payload model is part of a discriminated union keyed by `kind` (or `origin` for vectors), so the UI can dispatch rendering on the tag and the DAG solver can reason about compatible outputs.
29
+
30
+ ## Repo layout
31
+
32
+ ```
33
+ mechbench-schema/
34
+ ├── src/mechbench_schema/ # Pydantic models — the source of truth
35
+ │ ├── __init__.py # __all__ + __schema_all__
36
+ │ ├── attention_trace.py
37
+ │ ├── cluster_data.py
38
+ │ ├── identity.py
39
+ │ ├── per_head_data.py
40
+ │ ├── per_layer_data.py
41
+ │ ├── per_layer_per_position_data.py
42
+ │ └── vector_data.py
43
+ ├── ts/ # published to npm as "mechbench-schema"
44
+ │ ├── package.json
45
+ │ ├── tsconfig.json
46
+ │ └── src/
47
+ │ ├── index.ts
48
+ │ ├── generated.ts # codegen output — DO NOT EDIT
49
+ │ └── schema.json # consolidated JSON Schema — codegen output
50
+ ├── scripts/
51
+ │ └── codegen.py # Pydantic → JSON Schema → TypeScript
52
+ ├── pyproject.toml # published to PyPI as "mechbench-schema"
53
+ └── README.md
54
+ ```
55
+
56
+ ## Editing the contract
57
+
58
+ 1. Edit an existing module under `src/mechbench_schema/`, or add a new module (following the domain-axis convention).
59
+ 2. Export the new type from `src/mechbench_schema/__init__.py`. Add it to `__all__` for Python consumers and to `__schema_all__` for codegen (wire types only).
60
+ 3. Run `python scripts/codegen.py`. This writes `ts/src/schema.json` and `ts/src/generated.ts`.
61
+ 4. Commit everything in one PR — Python source, generated JSON Schema, generated TypeScript.
62
+
63
+ CI runs codegen and `git diff --exit-code ts/src/schema.json ts/src/generated.ts`. A drifted PR cannot land.
64
+
65
+ The codegen uses `json-schema-to-typescript` (via `npx`) for the TS emission. Python → JSON Schema comes from Pydantic's built-in `model_json_schema()` (for `BaseModel` subclasses) and `TypeAdapter(...).json_schema()` (for type aliases like the discriminated unions).
66
+
67
+ ## Install (Python)
68
+
69
+ ```bash
70
+ pip install mechbench-schema
71
+ ```
72
+
73
+ ```python
74
+ from mechbench_schema import LayerAblationPayload, DlaSweepPayload
75
+
76
+ payload = LayerAblationPayload(
77
+ experiment="step_02_layer_ablation",
78
+ description="Per-layer ablation damage on FACTUAL_15.",
79
+ model="mlx-community/gemma-4-E4B-it-bf16",
80
+ n_layers=42,
81
+ global_layers=[5, 11, 17, 23, 29, 35, 41],
82
+ prompts=[...],
83
+ aggregates=LayerAggregates(mean=[...], median=[...]),
84
+ )
85
+
86
+ # Emit to JSON — mode="json" canonicalizes datetimes, bytes, etc.
87
+ import json
88
+ path.write_text(json.dumps(payload.model_dump(mode="json"), indent=2))
89
+ ```
90
+
91
+ ## Install (TypeScript)
92
+
93
+ ```bash
94
+ npm install mechbench-schema
95
+ ```
96
+
97
+ ```ts
98
+ import type { LayerAblationPayload, Vector, MechbenchPath } from "mechbench-schema";
99
+
100
+ const payload: LayerAblationPayload = JSON.parse(text);
101
+ // All fields, discriminated unions, and nested types come from the generated bindings.
102
+ ```
103
+
104
+ ## Identity grammar
105
+
106
+ `MechbenchPath` is the type used to address every object in the mechbench family — charts, articles, experiments, corpuses, probes, cached DAG intermediates. Five categories:
107
+
108
+ - `<owner>/<project>/<folders>/<leaf>` — user-named
109
+ - `~canonical/<area>/<path>/<leaf>` — human-readable aliases for globally-shared content
110
+ - `~system/<area>/<path>/<leaf>` — platform / first-party
111
+ - `~hash/<algo>:<digest>` — global content-hashed (deduplicated across users)
112
+ - `<owner>/<project>/~hash/<algo>:<digest>` — workspace-scoped content-hashed
113
+
114
+ Full spec in the meta repo's IDENTITY_AND_NAMESPACING.md. The Python side (`mechbench_schema.identity`) owns the authoritative validator; a mirror implementation lives in `mechbench-ui/src/lib/mechbenchPath.ts` for UI-side early failure.
115
+
116
+ ## Why one repo, two publications
117
+
118
+ Schemas that live in two repos drift. The only invariant that matters — "Python and TS agree on the shape" — is enforced by keeping the source in one place and generating the target. Consumers never need the other language's toolchain to install; `pip` and `npm` each resolve to a clean single-language package.
119
+
120
+ See [the family overview](https://mechbench.ai) for the rationale behind this and other multi-repo decisions.
121
+
122
+ ## Status
123
+
124
+ Version 0.8.0. Seven modules in active use; the legacy `records.py` holding pen was retired when the domain-axis reorg completed. `mechbench-experiments`' two exporters (`step_02_layer_ablation`, `step_33_dla_factual_sweep`) emit via Pydantic models; `mechbench-ui`'s chart interfaces are one-line aliases over the generated TS types.
125
+
126
+ Open work lives in the meta repo's `tasks/mechbench-schema/` directory. The two notable open epics downstream of this repo:
127
+
128
+ - **`000161`** — compact binary formats for records at rest and in transit (safetensors for tensor-bulk, parquet for record-collections).
129
+ - **`000163`** — the identity-and-namespacing epic that produced `MechbenchPath`; Phase 4 (content-addressing grammar) remains open and coordinates with `000162` (the DAG-solver epic in `mechbench-compute`).
130
+
131
+ ## License
132
+
133
+ MIT.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mechbench-schema"
7
+ version = "0.13.0"
8
+ description = "Typed emission contract for the mechbench family. Pydantic models as single source of truth; generates TypeScript bindings for mechbench-ui."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Benji Smith", email = "benji@shaxpir.com" }]
13
+ keywords = ["interpretability", "schema", "pydantic", "mechbench"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = [
22
+ "pydantic>=2.6",
23
+ "cbor2>=5.6",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ codegen = ["datamodel-code-generator[typescript]>=0.25"]
28
+ dev = ["pytest>=8", "ruff>=0.5"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/mechbench/mechbench-schema"
32
+ Website = "https://mechbench.ai"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+ include = ["mechbench_schema*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,213 @@
1
+ """mechbench-schema — typed emission contract for the mechbench family.
2
+
3
+ This package is the single source of truth for every interpretability-record
4
+ shape that crosses a repo boundary. Python consumers (mechbench-compute,
5
+ mechbench-runner, mechbench-experiments) import types from
6
+ here; TypeScript consumers (mechbench-ui) import types from the parallel
7
+ mechbench-schema npm package, which is generated from these Pydantic models
8
+ via scripts/codegen.py.
9
+
10
+ Rule of thumb: if the shape is ever serialized to disk, sent over the wire,
11
+ or rendered by a non-Python consumer, it belongs here. If the shape is only
12
+ used as an in-memory container within a single repo, it does not.
13
+ """
14
+
15
+ from .attention_trace import (
16
+ AttentionPattern,
17
+ AttentionTraceData,
18
+ )
19
+ from .codec_cbor import (
20
+ dump_canonical,
21
+ load_canonical,
22
+ load_raw,
23
+ )
24
+ from .identity import (
25
+ InvalidPathError,
26
+ MechbenchPath,
27
+ ParsedPath,
28
+ PathCategory,
29
+ make_canonical_path,
30
+ make_global_hash_path,
31
+ make_platform_path,
32
+ make_scoped_hash_path,
33
+ make_user_path,
34
+ parse_path,
35
+ )
36
+ from .cluster_data import (
37
+ Cluster,
38
+ ClusterSet,
39
+ )
40
+ from .per_head_data import (
41
+ PerHeadBase,
42
+ PerHeadData,
43
+ PerHeadScalarGrid,
44
+ )
45
+ from .document_data import (
46
+ AnnotationAnchor,
47
+ AnnotationLayer,
48
+ AnnotationValue,
49
+ BASE_KIND_ANNOTATED_TOKENS,
50
+ BASE_KIND_CONVERSATION,
51
+ BASE_KIND_TEXT,
52
+ DocumentCollection,
53
+ DocumentItem,
54
+ DocumentPayload,
55
+ GenerationSpan,
56
+ KindManifest,
57
+ RendererBinding,
58
+ Segment,
59
+ Segmentation,
60
+ Trace,
61
+ Turn,
62
+ )
63
+ from .metric_data import (
64
+ MetricColumn,
65
+ MetricTable,
66
+ )
67
+ from .provenance import (
68
+ Emitted,
69
+ Fidelity,
70
+ Provenance,
71
+ ToolInfo,
72
+ fidelity_satisfies,
73
+ fingerprint_params,
74
+ )
75
+ from .per_layer_data import (
76
+ AblationPrompt,
77
+ ConvergencePayload,
78
+ ConvergenceRow,
79
+ DlaPrompt,
80
+ DlaSweepPayload,
81
+ LayerAblationPayload,
82
+ LayerAggregates,
83
+ PerLayerBase,
84
+ PerLayerData,
85
+ )
86
+ from .per_layer_per_position_data import (
87
+ LogitLensTrajectory,
88
+ PerLayerPerPositionBase,
89
+ PerLayerPerPositionData,
90
+ )
91
+ from .vector_data import (
92
+ CapturedVector,
93
+ CentroidVector,
94
+ HookKind,
95
+ ProbeVector,
96
+ SteeringVector,
97
+ Vector,
98
+ )
99
+
100
+ __version__ = "0.12.0"
101
+
102
+ __all__ = [
103
+ # Document collections + annotations + kind manifests (task 000239)
104
+ "DocumentCollection",
105
+ "DocumentItem",
106
+ "DocumentPayload",
107
+ "Trace",
108
+ "GenerationSpan",
109
+ "Segmentation",
110
+ "Segment",
111
+ "Turn",
112
+ "AnnotationLayer",
113
+ "AnnotationValue",
114
+ "AnnotationAnchor",
115
+ "KindManifest",
116
+ "RendererBinding",
117
+ "BASE_KIND_TEXT",
118
+ "BASE_KIND_CONVERSATION",
119
+ "BASE_KIND_ANNOTATED_TOKENS",
120
+ # Metric tables (task 000244)
121
+ "MetricTable",
122
+ "MetricColumn",
123
+ # Provenance + fidelity (the emission envelope, task 000237)
124
+ "Provenance",
125
+ "ToolInfo",
126
+ "Emitted",
127
+ "Fidelity",
128
+ "fidelity_satisfies",
129
+ "fingerprint_params",
130
+ # Per-layer data (records indexed by transformer layer)
131
+ "PerLayerBase",
132
+ "LayerAggregates",
133
+ "AblationPrompt",
134
+ "LayerAblationPayload",
135
+ "DlaPrompt",
136
+ "DlaSweepPayload",
137
+ "ConvergenceRow",
138
+ "ConvergencePayload",
139
+ "PerLayerData",
140
+ # Per-head data (records indexed by (layer, head))
141
+ "PerHeadBase",
142
+ "PerHeadScalarGrid",
143
+ "PerHeadData",
144
+ # Attention-trace data (records indexed by (layer, head, position, position))
145
+ "AttentionPattern",
146
+ "AttentionTraceData",
147
+ # Vector data (atomic directions in residual-stream space)
148
+ "CapturedVector",
149
+ "SteeringVector",
150
+ "ProbeVector",
151
+ "CentroidVector",
152
+ "HookKind",
153
+ "Vector",
154
+ # Cluster data (collections of vectors with aggregate stats)
155
+ "Cluster",
156
+ "ClusterSet",
157
+ # Per-(layer, position) data (residual-stream-axis records)
158
+ "PerLayerPerPositionBase",
159
+ "LogitLensTrajectory",
160
+ "PerLayerPerPositionData",
161
+ # Identity / namespacing (see docs/IDENTITY_AND_NAMESPACING.md)
162
+ "MechbenchPath",
163
+ "ParsedPath",
164
+ "PathCategory",
165
+ "InvalidPathError",
166
+ "parse_path",
167
+ "make_user_path",
168
+ "make_canonical_path",
169
+ "make_platform_path",
170
+ "make_global_hash_path",
171
+ "make_scoped_hash_path",
172
+ ]
173
+
174
+ # Names that should be emitted to TS via codegen. Subset of __all__ — excludes
175
+ # Python-side utilities (functions, exceptions, non-Pydantic dataclasses)
176
+ # that don't cross the wire.
177
+ __schema_all__ = [
178
+ # Per-layer data
179
+ "PerLayerBase",
180
+ "LayerAggregates",
181
+ "AblationPrompt",
182
+ "LayerAblationPayload",
183
+ "DlaPrompt",
184
+ "DlaSweepPayload",
185
+ "ConvergenceRow",
186
+ "ConvergencePayload",
187
+ "PerLayerData",
188
+ # Per-head data
189
+ "PerHeadBase",
190
+ "PerHeadScalarGrid",
191
+ "PerHeadData",
192
+ # Attention-trace data
193
+ "AttentionPattern",
194
+ "AttentionTraceData",
195
+ # Vector data
196
+ "CapturedVector",
197
+ "SteeringVector",
198
+ "ProbeVector",
199
+ "CentroidVector",
200
+ "HookKind",
201
+ "Vector",
202
+ # Cluster data
203
+ "Cluster",
204
+ "ClusterSet",
205
+ # Per-(layer, position) data
206
+ "PerLayerPerPositionBase",
207
+ "LogitLensTrajectory",
208
+ "PerLayerPerPositionData",
209
+ # Identity (only the validated-string type crosses the wire; parsers and
210
+ # helpers are Python-side-only)
211
+ "MechbenchPath",
212
+ "PathCategory",
213
+ ]
@@ -0,0 +1,81 @@
1
+ """Attention-trace data shapes.
2
+
3
+ Records indexed by `(layer, head, position_from, position_to)` — the
4
+ attention patterns that heads compute during a forward pass. An
5
+ attention trace for a single prompt over a full model is a collection
6
+ of `(layer, head)` weight matrices, each `[seq_len × seq_len]` for
7
+ self-attention.
8
+
9
+ This module organizes the attention-pattern axis as a first-class
10
+ category, parallel to per_layer_data and per_head_data. A downstream
11
+ consumer can render a single (layer, head) matrix as a heatmap, a
12
+ small-multiples grid over (layer, head), a per-head attention-entropy
13
+ summary, etc.
14
+
15
+ The kinds today:
16
+
17
+ attention_pattern — one (layer, head) attention matrix, possibly
18
+ bundled with token labels for axis rendering.
19
+ This is the atomic unit; a full trace is a
20
+ collection of these.
21
+
22
+ Future kinds (file when a real consumer needs them):
23
+ - attention_entropy — per-(layer, head, position) scalar summary.
24
+ - attention_topk — per-(layer, head, position) top-k target
25
+ positions by weight.
26
+ - attention_trace — a sweep containing many attention_patterns plus
27
+ shared metadata (prompt_id, token_labels). May
28
+ want a `PerHeadOf[AttentionPattern]`-style
29
+ container when task 000157's generic work lands.
30
+
31
+ Migrated from the old records.py as part of the domain-axis reorg
32
+ (task 000156).
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from typing import Annotated, Literal, Union
38
+
39
+ from pydantic import BaseModel, Field
40
+
41
+
42
+ class AttentionPattern(BaseModel):
43
+ """Post-softmax attention weights for one (layer, head) over a sequence.
44
+
45
+ The weights matrix is stored as a flat row-major `[n_queries * n_keys]`
46
+ list. Consumers reshape on ingest; this keeps the wire format
47
+ JSON-native and trivially diffable. `token_labels`, when present,
48
+ labels both axes (self-attention) — for cross-attention the caller
49
+ should split into query_labels / key_labels (not yet modeled;
50
+ add when needed).
51
+ """
52
+
53
+ kind: Literal["attention_pattern"] = "attention_pattern"
54
+ layer: int = Field(..., ge=0, description="0-indexed transformer layer.")
55
+ head: int = Field(..., ge=0, description="0-indexed attention head within the layer.")
56
+ n_queries: int = Field(..., ge=1, description="Query-side sequence length.")
57
+ n_keys: int = Field(..., ge=1, description="Key-side sequence length.")
58
+ weights: list[float] = Field(
59
+ ...,
60
+ description=(
61
+ "Row-major flattened post-softmax weights, shape "
62
+ "[n_queries * n_keys]. weights[q * n_keys + k] is the attention "
63
+ "weight from query q to key k."
64
+ ),
65
+ )
66
+ token_labels: list[str] | None = Field(
67
+ None,
68
+ description=(
69
+ "Optional per-position decoded tokens, for axis labelling. "
70
+ "Assumes self-attention (same labels on both axes); if the "
71
+ "shape is asymmetric, model the split explicitly."
72
+ ),
73
+ )
74
+
75
+
76
+ # --- Discriminated union over all attention-trace kinds ---------------------
77
+
78
+ AttentionTraceData = Annotated[
79
+ Union[AttentionPattern],
80
+ Field(discriminator="kind"),
81
+ ]