NodeType Supersession

The rule in one sentence: a retired NodeType declares its successor and how its content maps, and an instance re-types itself the next time its hub activates — so a rename is a declaration, not a migration script.

Today a type rename is a hand-run job: find every instance, rewrite nodeType, rewrite content.$type, fill the new fields, recycle each address, and hope nobody adds an instance of the old type while you are doing it. That work is mechanical and the same every time, which is the signature of something the platform should do.

The declaration

Supersession lives on the old type, because the old type is what a stale instance points at and therefore the only thing the platform is guaranteed to find:

// Crm/Client.json — the retired type. It STAYS; see "Do not delete the old type".
"content": {
  "$type": "NodeTypeDefinition",
  "supersededBy": "Crm/Counterparty",
  "contentMapping": {
    "$type": "CounterpartyContent",          // the new discriminator, written EXPLICITLY
    "defaults": { "type": "Crm/CounterpartyType/Client" }   // fields the new type adds
  }
}

Fields the two content types share carry over by name. Anything the new type adds and the mapping does not name is left at its own default. Anything richer than that — a field that splits, a value that must be computed — is a handler naming a Code node, resolved exactly as a dispatch rule's handler is (Open Vocabularies Are String Constants).

When it runs

On activation, from the hub's own first emission — the established wake-up recovery rule: drive any non-terminal state to a valid one, exactly once (Activity Control Plane). "My nodeType is superseded" is exactly such a state.

It is declarative convergence, not interruption-sniffing: the hub asks "is my type retired?", never "has something been running too long?". The owning hub is the single writer for its own node, so two replicas cannot race, and re-running finds nothing to do.

🚨 The migrating activation is still bound to the OLD type

A per-node hub binds its NodeType once, while activating, and nothing re-reads it while it lives (Stale State Until Recycle). So the activation that performs the migration is serving the old type the whole time it rewrites the node: the views, the layout areas and the content type in force are the retired ones until the address is torn down.

The migration therefore ends with the node recycling itself, and the fresh activation binds the successor. A migration that rewrites the node and stops has produced a node that reads as the new type and behaves as the old one until something unrelated happens to recycle it — which is worse than not migrating, because it looks done.

🚨 It converges LAZILY — and therefore never finishes on its own

An instance nobody opens never activates, and therefore never migrates. A partition that sees no traffic for a year holds nodes of the retired type for a year. This is the feature's defining limit and the one thing that must not be forgotten when it is built:

🚨 Do not delete the old type

The retired NodeType stays registered until the sweep reads zero. Its Source/ and Test/ areas are in-mesh C# that no dotnet build ever type-checks (NodeType Compilation), so deleting it breaks code the compiler cannot see, in a way that surfaces only when a portal compiles it at runtime. Retire it as a declaration; delete it, if ever, as a separate change once the count is zero and stays zero.

🚨 A read must not become an unaudited write

Activation is triggered by whoever opens the node, so a migration on activation is a write caused by a read. Three consequences, each of which has a wrong answer that looks right:

What this does NOT replace

Supersession is for a mechanical, lossless mapping — the same record under a new name and shape. It removes the ceremony, not the governance:

Cross-references