1#[derive(Debug, Clone, Copy)]
14pub struct DoctorCodeDef {
15 pub code: &'static str,
17 pub title: &'static str,
19 pub description: &'static str,
21}
22
23pub struct Codes {
25 pub harness_api_mismatch: DoctorCodeDef,
28 pub lockfile_unreadable: DoctorCodeDef,
30 pub migration_not_collected: DoctorCodeDef,
33 pub locked_migration_missing: DoctorCodeDef,
35 pub locked_migration_edited: DoctorCodeDef,
37 pub captcha_not_effective: DoctorCodeDef,
40 pub payments_webhook_secret_missing: DoctorCodeDef,
43 pub sidecar_shadows_module: DoctorCodeDef,
46 pub sidecar_mount_invalid: DoctorCodeDef,
48 pub module_self_check: DoctorCodeDef,
52 pub non_portable_sql: DoctorCodeDef,
54 pub card_data_in_migration: DoctorCodeDef,
57 pub push_transport_misconfigured: DoctorCodeDef,
61 pub push_required_but_unrouted: DoctorCodeDef,
64 pub module_unknown: DoctorCodeDef,
67 pub not_deployed: DoctorCodeDef,
69 pub production_deploy_unauthorized: DoctorCodeDef,
72 pub stale_plan: DoctorCodeDef,
75 pub composition_drift: DoctorCodeDef,
78 pub config_drift: DoctorCodeDef,
81 pub deploy_plan_required: DoctorCodeDef,
84 pub deploy_record_unreadable: DoctorCodeDef,
86 pub destructive_change_unauthorized: DoctorCodeDef,
90 pub env_drift: DoctorCodeDef,
93 pub manifest_drift: DoctorCodeDef,
97 pub manifest_exists: DoctorCodeDef,
100 pub manifest_invalid: DoctorCodeDef,
103 pub manifest_unreadable: DoctorCodeDef,
105 pub manifest_write_failed: DoctorCodeDef,
107}
108
109pub const CODES: Codes = Codes {
110 harness_api_mismatch: DoctorCodeDef {
111 code: "harness-api-mismatch",
112 title: "Harness API mismatch",
113 description: "A module was built against a different HARNESS_API than the core crate this venture linked.",
114 },
115 lockfile_unreadable: DoctorCodeDef {
116 code: "lockfile-unreadable",
117 title: "Lockfile unreadable",
118 description: "The .harness-lock.json could not be read or parsed.",
119 },
120 migration_not_collected: DoctorCodeDef {
121 code: "migration-not-collected",
122 title: "Migration not collected",
123 description: "A module migration has no locked file yet; run `fz migrations collect`.",
124 },
125 locked_migration_missing: DoctorCodeDef {
126 code: "locked-migration-missing",
127 title: "Locked migration missing",
128 description: "A locked migration's file is missing from disk.",
129 },
130 locked_migration_edited: DoctorCodeDef {
131 code: "locked-migration-edited",
132 title: "Locked migration edited",
133 description: "A locked migration's file no longer matches its pinned sha256; restore it or add a new migration.",
134 },
135 captcha_not_effective: DoctorCodeDef {
136 code: "captcha-not-effective",
137 title: "Captcha not effective",
138 description: "A production venture has captcha-guarded public writes but the Captcha port is not effectively configured.",
139 },
140 payments_webhook_secret_missing: DoctorCodeDef {
141 code: "payments-webhook-secret-missing",
142 title: "Payments webhook secret missing",
143 description: "A production venture has signature-guarded routes but STRIPE_WEBHOOK_SECRET is unset.",
144 },
145 sidecar_shadows_module: DoctorCodeDef {
146 code: "sidecar-shadows-module",
147 title: "Sidecar shadows a compiled-in module",
148 description: "A sidecar mount names a module compiled into this venture; the runtime ignores the mount.",
149 },
150 sidecar_mount_invalid: DoctorCodeDef {
151 code: "sidecar-mount-invalid",
152 title: "Sidecar mount invalid",
153 description: "The sidecar mount table could not be parsed.",
154 },
155 module_self_check: DoctorCodeDef {
156 code: "module-self-check",
157 title: "Module self-check failed",
158 description: "A module reported a problem in its own embedded data, such as a \
159 localisation catalog missing a message it declared.",
160 },
161 non_portable_sql: DoctorCodeDef {
162 code: "non-portable-sql",
163 title: "Non-portable SQL",
164 description: "A migration contains SQL outside the portable subset (dialect-specific tokens).",
165 },
166 card_data_in_migration: DoctorCodeDef {
167 code: "card-data-in-migration",
168 title: "Card data in migration",
169 description: "A migration contains card data: a card number, verification code or full expiry.",
170 },
171 push_transport_misconfigured: DoctorCodeDef {
172 code: "push-transport-misconfigured",
173 title: "Push transport misconfigured",
174 description: "A push transport has some of its variables set and some unset, or its credentials were refused; it is left unrouted.",
175 },
176 push_required_but_unrouted: DoctorCodeDef {
177 code: "push-required-but-unrouted",
178 title: "Push required but unrouted",
179 description: "A production venture has modules that require the Push port but its environment routes no push transport.",
180 },
181 module_unknown: DoctorCodeDef {
182 code: "module-unknown",
183 title: "Module unknown",
184 description: "`fz add` / `fz init` name a slug the catalog does not carry.",
185 },
186 not_deployed: DoctorCodeDef {
187 code: "not-deployed",
188 title: "Not deployed",
189 description: "`fz verify` finds no deployment recorded for this manifest.",
190 },
191 production_deploy_unauthorized: DoctorCodeDef {
192 code: "production-deploy-unauthorized",
193 title: "Production deploy unauthorized",
194 description: "A production deploy without the explicit --i-am-deploying-to-production consent flag.",
195 },
196 stale_plan: DoctorCodeDef {
197 code: "stale-plan",
198 title: "Stale plan",
199 description: "`fz deploy --plan <digest>`: the digest does not match a plan recomputed from the current inputs.",
200 },
201 composition_drift: DoctorCodeDef {
202 code: "composition-drift",
203 title: "Composition drift",
204 description: "The recorded deployment's module set no longer matches the manifest's resolved composition.",
205 },
206 config_drift: DoctorCodeDef {
207 code: "config-drift",
208 title: "Config drift",
209 description: "The recorded deployment's config no longer matches the manifest's.",
210 },
211 deploy_plan_required: DoctorCodeDef {
212 code: "deploy-plan-required",
213 title: "Deploy plan required",
214 description: "`fz deploy` refuses to run without --plan <digest>; approval is the point.",
215 },
216 deploy_record_unreadable: DoctorCodeDef {
217 code: "deploy-record-unreadable",
218 title: "Deploy record unreadable",
219 description: "The .harness-deploy.json record could not be read or parsed.",
220 },
221 destructive_change_unauthorized: DoctorCodeDef {
222 code: "destructive-change-unauthorized",
223 title: "Destructive change unauthorized",
224 description: "The plan removes modules (their data leaves the venture) without the explicit consent flag.",
225 },
226 env_drift: DoctorCodeDef {
227 code: "env-drift",
228 title: "Environment drift",
229 description: "The resolved deployment environment changed since the recorded deployment.",
230 },
231 manifest_drift: DoctorCodeDef {
232 code: "manifest-drift",
233 title: "Manifest drift",
234 description: "The recorded digest no longer matches a recomputed plan and no narrower drift explains it.",
235 },
236 manifest_exists: DoctorCodeDef {
237 code: "manifest-exists",
238 title: "Manifest exists",
239 description: "`fz init` was pointed at an existing manifest without --force.",
240 },
241 manifest_invalid: DoctorCodeDef {
242 code: "manifest-invalid",
243 title: "Manifest invalid",
244 description: "A manifest parsed but does not resolve: empty name/host, duplicate module, or a refused selection.",
245 },
246 manifest_unreadable: DoctorCodeDef {
247 code: "manifest-unreadable",
248 title: "Manifest unreadable",
249 description: "The venture manifest could not be read or parsed.",
250 },
251 manifest_write_failed: DoctorCodeDef {
252 code: "manifest-write-failed",
253 title: "Manifest write failed",
254 description: "A manifest could not be written back to disk.",
255 },
256};
257
258#[must_use]
260pub fn registry() -> Vec<&'static DoctorCodeDef> {
261 vec![
262 &CODES.captcha_not_effective,
263 &CODES.card_data_in_migration,
264 &CODES.composition_drift,
265 &CODES.config_drift,
266 &CODES.deploy_plan_required,
267 &CODES.deploy_record_unreadable,
268 &CODES.destructive_change_unauthorized,
269 &CODES.env_drift,
270 &CODES.harness_api_mismatch,
271 &CODES.locked_migration_edited,
272 &CODES.locked_migration_missing,
273 &CODES.lockfile_unreadable,
274 &CODES.manifest_drift,
275 &CODES.manifest_exists,
276 &CODES.manifest_invalid,
277 &CODES.manifest_unreadable,
278 &CODES.manifest_write_failed,
279 &CODES.migration_not_collected,
280 &CODES.module_self_check,
281 &CODES.module_unknown,
282 &CODES.non_portable_sql,
283 &CODES.not_deployed,
284 &CODES.payments_webhook_secret_missing,
285 &CODES.production_deploy_unauthorized,
286 &CODES.push_required_but_unrouted,
287 &CODES.push_transport_misconfigured,
288 &CODES.sidecar_mount_invalid,
289 &CODES.sidecar_shadows_module,
290 &CODES.stale_plan,
291 ]
292}
293
294#[cfg(test)]
295mod tests {
296 use super::{CODES, registry};
297
298 #[test]
301 fn codes_are_kebab_case() {
302 for def in registry() {
303 assert!(
304 !def.code.is_empty()
305 && def
306 .code
307 .bytes()
308 .all(|byte| byte.is_ascii_lowercase() || byte == b'-')
309 && !def.code.starts_with('-')
310 && !def.code.ends_with('-')
311 && !def.code.contains("--"),
312 "{:?} must be lowercase kebab-case",
313 def.code
314 );
315 assert!(!def.title.is_empty(), "{:?} needs a title", def.code);
316 assert!(
317 !def.description.is_empty(),
318 "{:?} needs a description",
319 def.code
320 );
321 }
322 }
323
324 #[test]
328 fn registry_covers_every_field_exactly_once_and_is_sorted() {
329 let registered = registry();
330 let mut by_slug: Vec<&str> = registered.iter().map(|def| def.code).collect();
331 let mut expected = by_slug.clone();
332 expected.sort_unstable();
333 assert_eq!(by_slug, expected, "registry must stay sorted by code");
334 by_slug.dedup();
335 assert_eq!(by_slug.len(), registered.len(), "duplicate code registered");
336
337 let fields = [
338 CODES.harness_api_mismatch.code,
339 CODES.lockfile_unreadable.code,
340 CODES.migration_not_collected.code,
341 CODES.locked_migration_missing.code,
342 CODES.locked_migration_edited.code,
343 CODES.captcha_not_effective.code,
344 CODES.payments_webhook_secret_missing.code,
345 CODES.sidecar_shadows_module.code,
346 CODES.sidecar_mount_invalid.code,
347 CODES.non_portable_sql.code,
348 CODES.card_data_in_migration.code,
349 CODES.push_transport_misconfigured.code,
350 CODES.push_required_but_unrouted.code,
351 CODES.module_self_check.code,
352 CODES.module_unknown.code,
353 CODES.not_deployed.code,
354 CODES.production_deploy_unauthorized.code,
355 CODES.stale_plan.code,
356 CODES.composition_drift.code,
357 CODES.config_drift.code,
358 CODES.deploy_plan_required.code,
359 CODES.deploy_record_unreadable.code,
360 CODES.destructive_change_unauthorized.code,
361 CODES.env_drift.code,
362 CODES.manifest_drift.code,
363 CODES.manifest_exists.code,
364 CODES.manifest_invalid.code,
365 CODES.manifest_unreadable.code,
366 CODES.manifest_write_failed.code,
367 ];
368 assert_eq!(fields.len(), registered.len());
369 for field in fields {
370 assert!(
371 registered.iter().any(|def| def.code == field),
372 "{field} is in the catalogue but not the registry"
373 );
374 }
375 }
376}