Submit an issue View all issues Source
MIR-1447

Extend typed entity-id boundaries beyond NodeId to the other string-sliced kinds

Open public
phinze phinze Opened Jul 20, 2026 Updated Jul 21, 2026

Follow-up to MIR-1072 (typed entity.NodeId).

MIR-1072 introduced entity.NodeId and proved out the pattern: a defined type with a single normalizing constructor, pushed through controller fields and constructors so "node/" lives in exactly one place. This issue is the natural continuation, applying that same discipline to the other twelve kinds a wide code survey turned up.

The survey found 112 string-slicing construction sites across 13 kinds (40 non-test, 72 test), plus 10 hand-rolled HasPrefix/TrimPrefix guards. With node handled, the remaining non-node surface is roughly 24 non-test sites concentrated in a few kinds: sandbox/ (5), app/ (4), disk_volume/ (3), pool/ (4), and a scatter of disk/, disk-lease/, disk_mount/, artifact/, svc/, session/, runner_invite/. Unlike node's tidy controller-field concentration, most of these live as saga-serialized string fields (build_saga.go's AppID/AppVersionID/ArtifactID, create_saga.go's nine SandboxID fields), so the boundary type has to survive a serialization round-trip. That's the main design wrinkle to work through here.

Doing this pass also cleans up real inconsistencies the compiler currently can't see: the same kind spelled two ways (app_version/ vs app-version/, disk-lease/ vs the disk_lease schema kind), and guarded-vs-unguarded construction for pool/ and sandbox/. Each typed constructor forces one canonical spelling.

Probably worth doing kind-by-kind rather than one mega-PR, prioritizing the kinds with the most sites and the sharpest inconsistencies. The full site-by-site listing was captured during the MIR-1072 inventory.