1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! 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.
use ;
use cratePlanTarget;
/// The qualification levels a same-ecosystem group is disambiguated through, in
/// increasing verbosity. `package` alone suffices for the common multi-crate case
/// (two crates.io crates); `registry` separates same-package channels
/// (`crates.io` vs `gh-releases` vs `homebrew` for one crate); `adapter` is the
/// last resort before two targets are genuinely identical.
const MAX_LEVEL: u8 = 3;
/// Assign a stable, unique journal id to each target in `targets`, returned
/// positionally aligned with the input.
///
/// A target that is the only one in its ecosystem gets the bare ecosystem wire
/// string (`"rust"`); an ecosystem carrying several targets gets each of them
/// `"<ecosystem>:<discriminator>"`, where the discriminator is the shortest of
/// `package` / `package:registry` / `package:registry:adapter` that is distinct
/// across that ecosystem's targets.
///
/// If two targets are *byte-identical* (same ecosystem, package, registry, and
/// adapter) their ids still collide even at the fullest qualification — a
/// degenerate duplicate the caller ([`crate::release::coordinator::validate_plan`])
/// rejects rather than papering over. Ids across *different* ecosystems never
/// collide (the ecosystem prefix differs), and a bare-ecosystem id never equals a
/// qualified `"<ecosystem>:…"` id.
/// The least qualification level (`1..=`[`MAX_LEVEL`]) at which every target in
/// `idxs` has a distinct [`discriminator`]. Falls back to [`MAX_LEVEL`] when even
/// the fullest form collides (two identical targets) — the caller detects the
/// resulting duplicate id and refuses the plan.
/// The `level`-deep discriminator for one target: `package`, then
/// `package:registry`, then `package:registry:adapter`. A target with no resolved
/// package name contributes an empty package segment (an unresolved target is not
/// executable anyway — the coordinator refuses it before any external action).