Fine-grained versioning of entities in programming languages
The Unison programming language is based around two simple fundamental ideas:

code is immutable
code is content-addressed

What does that mean? Code being immutable means just what it sounds like: since code is immutable, it will never change. You can only ever add new code to the system, but you can never change or remove existing code.
Code being content-addressed means that all addresses (references) are based on the content of the code, not any names. And computing the content address is invariant under α-conversion by using De Bruijn indices in the code database.
This has some interesting implications: since you only reference functions, types, modules, variables, etc. by their content address and the content address is based on a name-less representation, you can rename anything and everything to your heart's desire, and never break any code. Also, since all references are based on content, and content never changes (remember, code is immutable), code that works at one time, will always continue to work until all eternity.
This poses an interesting question, though: How do you fix bugs?
One part of the puzzle is how names are handled in Unison. I wrote above that Unison's code model is based on addresses, but that doesn't mean that there are no names at all. It would be very annoying to write code like
#1uekd4 = #lle0xy #5q17cp #h8i067

instead of
fullName = concat firstName lastName

and having to remember all the addresses.
Instead, Unison also stores a mapping of names to addresses in its code database. This is similar to how Unix filesystems work: in a Unix filesystem, a file has no name, it only has a file serial number. However, there are directories (which are also files) which map names to serial numbers (which could point to regular files or to another directory). Also, Git works in a similar fashion. There are blobs, which only have content, and there are trees which assign names to addresses (which could be blobs or trees).
If you want to fix a bug, then you create a new version of the buggy function which doesn't have the bug. But all existing code still references the old function! (Remember, all addressing is based on content, not names.) So, you have to update every function that references the old function. And you have to update every function that references a function that references the old function.
Obviously, this gets tedious really fast. This is where names come in. After updating the function (really creating a new function), you also update the name database (really creating a new database) with a version where the name now points to the new, fixed, function.
Unison will notice this update, and will automatically propagate this name change through all existing functions. So, it will automatically update all functions that reference the buggy function to now reference the fixed function, then it will update the functions which reference the functions it just updated, and so on.
Where &quot;automatically propagate&quot; can mean one of two things:

If the new function has the same type as the old one, Unison will automatically apply the update.
If the new function has an incompatible type, Unison will create a to-do list with all the places that need to be changed.

Once the change is made, regardless of whether it was done aumatically or it required manual intervention, this change is recorded in a patch and becomes part of the code database as well. So, anybody else who pulls in the changes will benefit from the fact that you already resolved the issue and they can pull in the patch as well.
Bringing this to your example: since the content of time::UnixTime hasn't changed, it still has the same address, and since code is immutable, it is guaranteed that this address will refer to this type for all eternity. And since both logger and totp only refer to it by its address, which will never change, they, too, will continue to work unchanged for all eternity.