Expand description
Stable, per-target journal ids for a release plan’s targets.
The event-sourced journal (ADR-0003) keys every per-target fact —
dry_run / built / published — by a short string id, and the coordinator,
the resume reconciler, and the CLI’s RunCreated.targets list must all derive
that id identically or a resume looks up the wrong key (and, worst case,
re-publishes an already-landed target). This module is the one place the id is
derived, so those three callers cannot drift.
§Why not just the ecosystem string
Historically a cut carried at most one target per ecosystem (the normalizer
expanded ecosystems 1:1), so the ecosystem wire string (rust, node, …)
was itself a unique key. A contract may now declare several targets in one
ecosystem — e.g. ossctl’s own two crates.io crates (ossctl-core then
ossctl), plus a gh-releases and a homebrew target all under rust — so
the ecosystem alone collides. journal_target_ids disambiguates only as far
as it must: a lone-in-its-ecosystem target keeps the bare ecosystem id (so
single-target cuts, and every existing journal, are byte-for-byte unchanged);
an ecosystem with several targets qualifies each with the least of
package → package:registry → package:registry:adapter that makes the
group’s ids distinct.
§Determinism (and its coupling to plan_id)
The ids are a pure function of the target list (which is itself the
normalizer’s canonical order), computed through a BTreeMap group scan — no
wall-clock, no HashMap iteration — so the same plan always yields the same
ids, in the same positions. The ids are journal keys only; they are not
part of the content-addressed plan_id (that hashes the target fields), so
this derivation never affects plan identity or drift detection.
That exclusion is load-bearing for resume safety, and it holds only because
this derivation reads exactly the target fields (ecosystem, package,
registry, adapter, and their order) that plan_id also seals
(crate::release::plan’s SealInput). The coordinator writes the journal
keyed by these ids and resume re-derives them from the (drift-checked) plan; a
matching plan_id therefore guarantees byte-identical ids, so resume looks up
the same receipt the cut wrote and never re-publishes a landed target. If a
future edit made this function read a field plan_id does not seal (or vice
versa), two plans could share a plan_id yet key their journals differently —
a silent re-publish hazard. Keep the two field sets in lockstep, and bump
crate::release::plan’s SEAL_VERSION if the covered fields change.
§Id stability across contract edits (a documented non-guarantee)
A target’s id is stable for a given plan, not across contract revisions.
Adding a second target to an ecosystem that previously had one flips the
first target’s id on the next cut from the bare "rust" to a qualified
"rust:<disc>". Old runs’ journals keep their "rust" keys forever (they are
never rewritten); only new runs use the qualified form. Downstream consumers
(release show --json, dashboards, log queries) must therefore not assume a
per-target journal id is stable across contract edits.
Functions§
- journal_
target_ ids - Assign a stable, unique journal id to each target in
targets, returned positionally aligned with the input.