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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//! The agent-name **mint** (ARCH §2.3): **two words from the embedded
//! wordlist joined in PascalCase** (`PeachHollow`), drawn whenever a
//! creation path omits `name`.
//!
//! Moved here from yog `src/names` by the yog bl-aca4 ruling: the moment
//! *every* creation path must mint on omission — the `dispatch` tool,
//! `litany dispatch`, `litany prompt`, none of which pass through yog —
//! the mint's one home is beside the uniqueness check it races
//! ([`super::require_available`]). Yog draws the same function through
//! the crate it already links (the [`crate::mint`] facade), so preview
//! and spawn cannot drift into two lists.
//!
//! **Two words, because one does not read as a name** (bl-79a2, operator
//! ruling 2026-08-16). A lone common noun in a conversation row or a
//! `litany list` line reads as a word that happens to be there; the
//! PascalCase pair carries the naming intent in its own shape. The join
//! has no separator, so the name stays one path component and one
//! unbroken token — and a name carrying no hyphen can never be misread
//! as two segments of the hyphenated descent `<a>-<b>-…` (§2.3).
//!
//! [`mint`] is a **pure function over an injected RNG and an occupied
//! set**: one RNG draw picks a start index into the *pair* space, then
//! the scan walks forward with wraparound, discarding each occupied name
//! for the next, to the first unoccupied one.
//!
//! **Purity is a contract on the caller, and it has been broken once**
//! (bl-8fe8). Same draw plus same occupied set means same name, so a
//! consumer supplying its own [`Rng`] owes it **per-creation** entropy —
//! not per second, not per anything coarser than a creation. Nothing
//! downstream can recover from a coarser one: two creators racing inside
//! that grain both scan the living names *before* either has committed a
//! `name` blob, so their occupied sets are equal too, and
//! [`super::require_available`] — the check this mint is documented as
//! racing — sees the name free for both. Measured: yog seeded
//! `SplitMix64` from a hash of its ops-log timestamp, which is unix
//! **seconds**, so three conversations started in one second on one
//! workspace were all minted `ScarfPeach`, and every seat verb that
//! takes a name then refused all three as ambiguous. The engine's own
//! creation paths draw from [`SplitMix64::from_entropy`] and are not
//! affected; the contract is stated here because this is where a
//! consumer reads it, and pinned by
//! [`tests::two_generators_seeded_from_one_wall_clock_second_mint_one_name`]. Collision retry is that
//! scan; its bound is the pair space — exhaustion is the scan running the
//! whole pool out ([`MintError::Exhausted`], loud, never a loop). The
//! occupied set is the caller's; at the creation pre-flights it is the
//! same living-names scan ([`super::named`]) the supplied-name check
//! reads — one derivation, never a second registry.
//!
//! The pair space is the **index space widened**, not a second draw: an
//! index into `0..n * (n - 1)` names an ordered pair of distinct words,
//! so the scan, the purity, the one-draw-per-mint property and the exact
//! exhaustion bound all survive the change untouched. Consequently the
//! walk is **not uniform over pairs** — a collision steps to the next
//! *second* word, not to a fresh random pair. That is exactly what the
//! one-word mint already did, and nothing asks the mint for randomness
//! guarantees: it is a collision-avoidance device, and uniqueness is the
//! occupied-set check's, not the generator's.
//!
//! **Case is not a distinction a filesystem keeps.** The occupied set is
//! an exact-match `HashSet<String>`, so `PeachHollow` and `peachhollow`
//! are two names here and one directory on macOS or Windows. The mint
//! spells PascalCase and nothing else, so it can never produce that pair
//! itself; an operator-supplied name still can, which is
//! [`super::require_available`]'s existing behaviour and unchanged by
//! bl-79a2. Recorded, not widened.
use ;
use HashSet;
use ;
use ;
/// The embedded pool: 541 concrete, neutral, everyday English words,
/// authored for this repository and covered by the crate's own licence
/// (bl-b59c — it replaces an EFF-derived CC BY 4.0 list, which put a
/// second licence inside an MIT package and minted names like `wrath`).
/// Provenance, the sizing argument and the invariants are in the file's
/// own header; the approval is pinned by count and digest in
/// [`tests::corpus`]. It is data, so it ships in the binary via
/// `include_str!`. The list is still sized for human review, not for
/// entropy — the pair space it spells (541 × 540 = 292,140 names) is not
/// the constraint on it, and widening the space is why bl-79a2 needed no
/// second wordlist.
const WORDS_TXT: &str = include_str!;
/// The one way a mint fails: every name the pool can spell is taken.
/// The injected randomness the mint is pure over. A trait rather than a
/// concrete generator so a test scripts the draw and the production
/// seeding stays out of the pure path. The draw takes `&self` — state
/// advances by interior mutability — so a generator rides the crate's
/// `&dyn` injection seams ([`crate::prompt::Deps`]) like every other
/// injected dependency.
/// SplitMix64 — the production [`Rng`]. Chosen because it is a few lines
/// of wrapping arithmetic: the mint needs one draw per name, and a
/// `rand` dependency for that is not worth the supply-chain surface.
/// The state is atomic (the `&self` draw above); the additive stream
/// constant makes `fetch_add` the whole state advance.
/// SplitMix64's additive stream constant.
const GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
/// The embedded wordlist as words: non-blank, non-comment lines, trimmed.
/// Two words joined with their initials upper-cased and nothing between
/// them — `("peach", "hollow")` ⇒ `PeachHollow`. Ascii-only upper-casing,
/// because the pool is pinned to `^[a-z]{3,9}$` ([`tests::corpus`]); an
/// empty entry contributes nothing rather than panicking.
/// The mint over an explicit wordlist — the whole algorithm, kept
/// list-injectable so tests exercise collision retry and exhaustion on a
/// tiny pool instead of the embedded one.
///
/// The pool is the **ordered pairs of distinct words**, `n * (n - 1)` of
/// them: index `i` picks `i / (n - 1)` as the first word and the
/// `i % (n - 1)`-th of the *others* as the second, so a word never pairs
/// with itself (`PeachPeach` is not a name the mint can spell) and every
/// pair is reachable exactly once. The retry is bounded by that pool:
/// each occupied name is discarded for the next with wraparound, and one
/// full lap proves exhaustion exactly — no free name is ever missed, no
/// loop runs unbounded. A list of fewer than two words spells no name at
/// all, which is the same empty pool the empty list is rather than a
/// case of its own. An out-of-range index cannot occur, and a missing
/// word reads as empty rather than panicking.
/// Mint a name from the embedded wordlist: the first PascalCase pair of
/// distinct words not in `occupied`, scanning from an RNG-chosen start.
/// Pure — same RNG state and same occupied set, same name.
/// The settle-the-name pre-flight both creation paths run before forking
/// (ARCH §2.3): a supplied name is validated against the living agents
/// ([`super::require_available`]); an absent one is minted against the
/// **same** living-names scan ([`super::named`]) — one occupied-set
/// derivation, so no fork ends nameless and no second registry exists to
/// drift. A refusal (taken, malformed, id-shaped, or an exhausted pool)
/// leaves no branch, no worktree and no inbox behind.
/// True iff `name` wears the shape the mint spells (bl-79a2): two words
/// joined in PascalCase — ASCII letters only (so no separator of any
/// kind), exactly two upper-case initials, the first of them leading.
/// The one home of that predicate, so the tests of the *other* modules
/// that assert on an omitted name — the fork's staged `name` file, a
/// child's minted name, the e2e blob — cannot each drift into their own
/// weaker reading of "minted".
pub
/// A seeded, process-shared [`SplitMix64`] for tests whose subject is
/// not the mint: `'static`, so it drops into a `Deps` or a call tail
/// with no local binding. A test that asserts on the minted name
/// constructs its own seeded generator instead — this one's draw order
/// depends on what ran before it.
pub