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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
//! Structured config-resolution diagnostics: fatal problems and non-fatal
//! notices.
//!
//! [`ConfigDiagnostics`] is threaded through every section resolver during a
//! `resolve_config` pass — and through every reloadable section during a
//! SIGHUP reload (`runtime::reload`) — so every problem an operator made
//! surfaces in one bulleted `anyhow::Error`, not one per re-run.
//!
//! Each problem is recorded under a dotted field label (`blockchain.rpc_url`,
//! `cache.origins[2]`, ...) with the resolver's message text. Single-line
//! messages are reproduced verbatim; multi-line messages have their
//! continuation lines indented to stay under their bullet (see
//! [`ConfigDiagnostics::into_result`]).
//!
//! [`ConfigNotice`] carries the other half: a value that resolves fine but
//! that the operator should know about (a disabled cap, an unbounded map, a
//! privileged port). A notice never fails resolution. It rides the same bag
//! because the bag already reaches every resolver on both the startup and the
//! SIGHUP path, so the caller — which is the only layer that knows whether a
//! `tracing` subscriber exists yet — decides how to render it.
/// Field labels that participate in a `has_field` cascade-suppression guard.
///
/// These are the only labels whose exact string is load-bearing for
/// *correctness*: a producing-site `push`/`try_with` and a guarding-site
/// `has_field` (e.g. `resolve_identity_into` and
/// `ensure_region_when_publishing_global_into`) must agree byte-for-byte, or
/// the cascade-suppression guard silently degrades and a misleading second
/// error bubbles up. Naming them as constants makes that coupling
/// refactor-safe.
///
/// Add a new constant here only when introducing a new `has_field` guard.
/// The other ~60 non-guarded labels stay inline literals — they appear only
/// in operator output, never in a guard, so promoting them all would defeat
/// the point of singling these two out.
pub const IDENTITY_REGION: &str = "identity.region";
pub const IDENTITY_DATA_DIR: &str = "identity.data_dir";
/// How loud a [`ConfigNotice`] is.
///
/// The split is about what a consumer does with it, not about how the resolver
/// feels: `Warn` is what an operator alerts on and what moves
/// `decdn node doctor --strict` off zero, `Info` is visible but never changes
/// an exit status. A consumer keys off this rather than parsing the message.
///
/// As a guide, `Warn` fits a value that weakens a safety property (an
/// unbounded bookkeeping map, a collapsed failover list) or a setting that is
/// likely a mistake (a privileged port). `Info` fits a
/// deliberate opt-out working exactly as configured, where paging someone
/// would only teach them to ignore the channel.
///
/// Variants are declared in ascending severity. Deliberately no `Ord`: a
/// consumer matches exhaustively, so a future variant inserted by topic cannot
/// silently flip a comparison.
/// One non-fatal resolve-time notice: a severity, the label of whatever the
/// operator wrote, and the operator-facing message text.
///
/// `field` is the dotted config label (`security.max_tracked_sources`),
/// matching the problem-label convention so a notice and a problem about the
/// same field read the same way.
///
/// `field` is kept out of the message text, so a consumer can
/// render or filter on it independently of what it says.
/// One resolved-config problem: a dotted field label (`blockchain.rpc_url`)
/// plus the resolver's message text.
/// Accumulates every problem and every non-fatal notice found during a single
/// `resolve_config` pass.
///
/// Threaded by `&mut` through the `*_into` section workers. Callers record
/// problems via `check` (the `anyhow::ensure!` replacement) and
/// [`try_with`](Self::try_with) (the `?`/`.context()` replacement),
/// substituting a placeholder for any value they could not resolve so
/// later independent checks still run. They record notices via `warn` / `note`.
///
/// Insertion order is preserved end-to-end: [`into_result`](Self::into_result)
/// renders bullets in the same order they were `push`ed, so
/// resolver authors can rely on operator-facing problem order matching the
/// order of validation logic, and tests that pin specific output order keep
/// working through future refactors. [`take_notices`](Self::take_notices)
/// preserves notice order for the same reason.
///
/// Notices and problems are independent: a bag carrying only notices still
/// collapses to `Ok(())`, and a bag that fails resolution may still hold
/// notices its caller chooses not to render.
/// Wraps a section `*_into` worker so a single-section caller gets back an
/// `anyhow::Result<T>`. Cross-section aggregation lives in `resolve_config`,
/// which runs the workers against one shared bag instead.
///
/// **Notices recorded by the wrapped worker are discarded**, so do not route
/// an operator-facing resolve through this. The two paths that render notices
/// — `resolve_config` and `RuntimeReloadState::reload` — drive the workers
/// against their own bag and drain it with
/// [`ConfigDiagnostics::take_notices`]. Every wrapper built on this is
/// therefore `#[cfg(test)]`, except `resolve_discovery`, whose worker records
/// no notices; a worker that grows one needs its caller moved off this
/// wrapper, not a second sink here.
pub