Name translation
Source trackers put no meaningful restriction on metric names — they are dict keys, so any string goes.
Metrana validates names on a denylist: no whitespace or control
characters, none of the query-language reserved set ( ) [ ] , = < > ! ~ " |, no leading :, and byte caps.
Migration bridges the two by substituting every rejected character. Nothing is deleted, so a migrated name still reads like the original.
The character map
Section titled “The character map”| Source | Becomes | |
|---|---|---|
| space, tab, newline, control characters | _ |
runs collapse to one |
( [ |
{ |
|
) ] |
} |
|
, |
; |
|
= |
: |
|
< |
.lt. |
|
> |
.gt. |
|
! |
.not. |
|
~ |
.approx. |
|
" |
' |
|
| |
- |
|
leading : |
_ |
first character only; : is legal elsewhere |
In practice:
| Source name | Migrated name |
|---|---|
train/loss |
train/loss — unchanged, / is legal and carries the hierarchy |
accuracy (%) |
accuracy_{%} |
grad_norm[0] |
grad_norm{0} |
precision,recall |
precision;recall |
lr=3e-4 |
lr:3e-4 |
acc>0.9 |
acc.gt.0.9 |
GPU Utilization |
GPU_Utilization |
Unicode is untouched — état/loss migrates as-is.
Names longer than the 256-byte metric-name cap are truncated on a character boundary, never mid-character.
Collisions
Section titled “Collisions”Substitution is many-to-one, so two different source keys can land on the same name. loss(train) and
loss[train] both sanitize to loss{train} — and silently merging two series into one is worse than an ugly
name.
When a name is claimed by more than one key, every claimant gets a three-letter slug:
'loss(train)' -> 'loss{train}-wax''loss[train]' -> 'loss{train}-yuu'Including the one that would otherwise have kept the bare name. Two properties make this safe for a resumable migration, which rebuilds the mapping from scratch on every pass:
- Slugs are derived from the source key, not handed out by a counter. A counter would produce different slugs if a later pass enumerated keys in a different order, splitting one series into two.
- All claimants are slugged, so “who got there first” never leaks into the naming.
The result is identical no matter what order the keys arrive in, and identical across runs of the command. Every collision is named in the report, with all of its claimants.
Run names
Section titled “Run names”Run names are also portal URL path segments, so they replace / \ ? # % on top of the map above —
/ and \ become -, the rest become _. Then the source prefix is applied (wandb-), and a run id
suffix if the name is contested. See Weights & Biases.
Config and attribute paths
Section titled “Config and attribute paths”Config keys become attribute paths, which are
/-delimited and built by flattening nested dicts. A / inside a single config key would fabricate a
level that was never there, so it becomes .:
{"learning/rate": 0.1} -> config/learning.rateOther rejected characters follow the same map as metric names, against the larger 512-byte cap.
Unlike a metric name, a colliding attribute path is not slugged apart — the path is how the attribute is read back, so renaming it would hide the value under a name nobody looks for. Two keys that sanitize onto one path keep the first and drop the rest, and every dropped key is named in the report:
attribute collision on 'config/learning.rate': kept 'learning/rate', dropped 'learning.rate'Previewing all of it
Section titled “Previewing all of it”--dry-run prints the complete mapping — every rename and every collision — without writing anything:
metrana migrate wandb --entity my-team --project sweeps --workspace my-workspace --dry-run