roles-migrate declares roles on designs, points each design's own
StatTerm.Role at the role that governs it, and authors matching ProductRole
quality on every product selling a now-role-bearing design -- all through
AetherDb's own CultCache-backed types, one commit, nothing written if any
record fails to derive (same all-or-nothing contract as shield-migrate and
brownout-migrate). Field->role maps are per item kind (Weapon/Ballistic,
Weapon/Energy, Weapon/Launcher, Radiator, Reactor, Sensors, Thruster,
AetherDrive, Hull/Ship); Tool is keyed by design name since it is not a
shared kind. A stat only gets a role when it carries an unrouted
StatSource.Quality term AND still varies (Min != Max) -- Cut 1's flat
placeholder stats carry no observable role-vs-workmanship difference, so
routing one would be authoring fiction.
Product role means are deterministic generic placeholders (stable FNV hash
of product+role, not System.String.GetHashCode, which is randomized per
process and would make a dry run and the later apply disagree) flagged
"needs operator review" in the printed table, except Arctica/Alakrita's
`fins`, which item-provenance-target.md already gives a real brand identity
(heat efficiency).
census gains a role-coverage check: every sold, role-bearing design's
product must author a matching ProductRole spread, reported as "(product,
role) pairs where ... authors no matching spread" -- the gap the ruling
names directly (a newly minted lot from a product with no spread rolls the
default mean .5, silently pulling every maker toward average).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>