Skip to content

Retiring a NodeType or LinkType enumerator has no mechanism and no tripwire #410

Description

@typeless

NodeType and LinkType have no retirement mechanism, and their dense values make a silent misread more likely than the flags case that got one.

What #399 fenced, and what it left open

The flags leg of #399 established that a vocabulary read across the readable window — not just at the current version — needs a way to retire an enumerator without a format bump. For NodeFlags that now exists in include/pup/index/format.hpp:

  • RECORDED_NODE_FLAGS_MASK — what this build names,
  • RETIRED_NODE_FLAGS_MASK (:287) — bits that were named once and must still be readable,
  • READABLE_NODE_FLAGS_MASK (:298) — the union actually enforced in FileEntry::from_raw,
  • a (RECORDED & RETIRED) == 0 disjointness static_assert (:290) so a future flag cannot silently take a retired bit's meaning,
  • a value pin whose failure message (:279) tells the maintainer to move the bit rather than delete it.

NodeType and LinkType are read by the same recovery path, across the same window, and have none of it. names_node_type (include/pup/core/types.hpp:170-187) is a bare exhaustive switch over eleven enumerators; names_link_type (:190-193) delegates to link_role. Retiring an enumerator from either means deleting its case, and nothing anywhere records that the value was ever spoken for.

Two failure modes, and the second is the one that matters

Over-rejection (loud). Delete a case and every legitimate record in the readable window carrying that byte fails names_node_type, returns IndexDamaged, and is announced as damage. This is the exact defect the flags leg found in read_prior_paths — a legitimate v9–v13 record becoming Kind::Lost — and it is why RETIRED exists. Bad, but it is loud, and loud is survivable.

Misinterpretation (silent), and it is likelier here than for flags. NodeType's enumerators are dense and sequential — File=0 through Phi=10. Retire one and the freed value is not a hole anyone must deliberately reach into; it is the next value the natural numbering hands to whoever adds an enumerator, particularly if the retired case was removed from the middle and later entries were not renumbered. At that point an old record's byte silently means the new type: it passes validation, constructs a FileEntry, and routes as something it never was.

That is precisely the argument that justified the flags disjointness assert. It applies with more force to a dense enum than to a bitmask, where a retired bit stays a hole until someone deliberately reuses it.

Why this is not just the flags fix again

There is nothing to put in a retired set today: no enumerator has ever been retired from NodeType, LinkType, NodeFlags, or CommandFlag across the readable window except the single NodeFlags::Inactive bit that #399 already handles. So this is not a latent live bug — it is an unfenced invariant, and the fence is cheap now and expensive after the first retirement.

The flags leg's comment at names_node_type states the obligation in prose and explicitly denies the mechanism ("would need the retired set NodeFlags has and this does not"). That was the correct thing to ship with a comment-only follow-up, and it is not a substitute for a compiler-checked device. Prose does not fail the build.

Suggested shape, not prescribed

The symmetric answer is a retired set per vocabulary plus the same disjointness assert, so that retiring an enumerator is a mechanical move rather than a deletion, and reusing a retired value is a build error. Whether both vocabularies need it, and whether the assert or the value pin is the right tripwire for a dense enum, is a design question — the flags shape should not be copied over by reflex.

Found by the builder while applying the #399 flags-leg deltas, and correctly not built there: it is a design change, out of scope for a comment-only follow-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions