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 migration_not_declared: DoctorCodeDef,
37 pub locked_migration_missing: DoctorCodeDef,
39 pub locked_migration_edited: DoctorCodeDef,
41 pub captcha_not_effective: DoctorCodeDef,
44 pub payments_webhook_secret_missing: DoctorCodeDef,
47 pub auth_not_configured: DoctorCodeDef,
50 pub sidecar_shadows_module: DoctorCodeDef,
53 pub sidecar_mount_invalid: DoctorCodeDef,
55 pub module_self_check: DoctorCodeDef,
59 pub non_portable_sql: DoctorCodeDef,
61 pub card_data_in_migration: DoctorCodeDef,
64 pub push_transport_misconfigured: DoctorCodeDef,
68 pub push_required_but_unrouted: DoctorCodeDef,
71 pub module_unknown: DoctorCodeDef,
74 pub not_deployed: DoctorCodeDef,
76 pub production_deploy_unauthorized: DoctorCodeDef,
79 pub stale_plan: DoctorCodeDef,
82 pub composition_drift: DoctorCodeDef,
85 pub config_drift: DoctorCodeDef,
88 pub deploy_plan_required: DoctorCodeDef,
91 pub deploy_record_unreadable: DoctorCodeDef,
93 pub destructive_change_unauthorized: DoctorCodeDef,
97 pub env_drift: DoctorCodeDef,
100 pub manifest_drift: DoctorCodeDef,
104 pub manifest_exists: DoctorCodeDef,
107 pub manifest_invalid: DoctorCodeDef,
110 pub manifest_unreadable: DoctorCodeDef,
112 pub manifest_write_failed: DoctorCodeDef,
114 pub client_write_failed: DoctorCodeDef,
117 pub surface_action_unreadable: DoctorCodeDef,
121 pub surface_invalid: DoctorCodeDef,
124 pub surface_no_tables_module: DoctorCodeDef,
127 pub surface_table_name_invalid: DoctorCodeDef,
131 pub surface_unreadable: DoctorCodeDef,
134 pub surface_unsupported_contract: DoctorCodeDef,
137 pub admin_token_too_short: DoctorCodeDef,
141 pub rate_limiter_not_effective: DoctorCodeDef,
145 pub hmac_webhook_secret_missing: DoctorCodeDef,
150}
151
152pub const CODES: Codes = Codes {
153 harness_api_mismatch: DoctorCodeDef {
154 code: "harness-api-mismatch",
155 title: "Harness API mismatch",
156 description: "A module was built against a different HARNESS_API than the core crate this venture linked.",
157 },
158 lockfile_unreadable: DoctorCodeDef {
159 code: "lockfile-unreadable",
160 title: "Lockfile unreadable",
161 description: "The .harness-lock.json could not be read or parsed.",
162 },
163 migration_not_collected: DoctorCodeDef {
164 code: "migration-not-collected",
165 title: "Migration not collected",
166 description: "A module migration has no locked file yet; run `fz migrations collect`.",
167 },
168 migration_not_declared: DoctorCodeDef {
169 code: "migration-not-declared",
170 title: "Migration not declared",
171 description: "A dependency crate ships a sqlite migration file that no mounted module declares; it is never collected, so its tables never exist on D1.",
172 },
173 locked_migration_missing: DoctorCodeDef {
174 code: "locked-migration-missing",
175 title: "Locked migration missing",
176 description: "A locked migration's file is missing from disk.",
177 },
178 locked_migration_edited: DoctorCodeDef {
179 code: "locked-migration-edited",
180 title: "Locked migration edited",
181 description: "A locked migration's file no longer matches its pinned sha256; restore it or add a new migration.",
182 },
183 captcha_not_effective: DoctorCodeDef {
184 code: "captcha-not-effective",
185 title: "Captcha not effective",
186 description: "A production venture has captcha-guarded public writes but the Captcha port is not effectively configured.",
187 },
188 payments_webhook_secret_missing: DoctorCodeDef {
189 code: "payments-webhook-secret-missing",
190 title: "Payments webhook secret missing",
191 description: "A production venture has signature-guarded routes but STRIPE_WEBHOOK_SECRET is unset.",
192 },
193 auth_not_configured: DoctorCodeDef {
194 code: "auth-not-configured",
195 title: "Cannot identify a caller",
196 description: "A production venture mounts a module that requires the Auth port, but AUTH_ISSUER or AUTH_CLIENT_ID is unset — every route needing a caller answers 503.",
197 },
198 sidecar_shadows_module: DoctorCodeDef {
199 code: "sidecar-shadows-module",
200 title: "Sidecar shadows a compiled-in module",
201 description: "A sidecar mount names a module compiled into this venture; the runtime ignores the mount.",
202 },
203 sidecar_mount_invalid: DoctorCodeDef {
204 code: "sidecar-mount-invalid",
205 title: "Sidecar mount invalid",
206 description: "The sidecar mount table could not be parsed.",
207 },
208 module_self_check: DoctorCodeDef {
209 code: "module-self-check",
210 title: "Module self-check failed",
211 description: "A module reported a problem in its own embedded data, such as a \
212 localisation catalog missing a message it declared.",
213 },
214 non_portable_sql: DoctorCodeDef {
215 code: "non-portable-sql",
216 title: "Non-portable SQL",
217 description: "A migration contains SQL outside the portable subset (dialect-specific tokens).",
218 },
219 card_data_in_migration: DoctorCodeDef {
220 code: "card-data-in-migration",
221 title: "Card data in migration",
222 description: "A migration contains card data: a card number, verification code or full expiry.",
223 },
224 push_transport_misconfigured: DoctorCodeDef {
225 code: "push-transport-misconfigured",
226 title: "Push transport misconfigured",
227 description: "A push transport has some of its variables set and some unset, or its credentials were refused; it is left unrouted.",
228 },
229 push_required_but_unrouted: DoctorCodeDef {
230 code: "push-required-but-unrouted",
231 title: "Push required but unrouted",
232 description: "A production venture has modules that require the Push port but its environment routes no push transport.",
233 },
234 module_unknown: DoctorCodeDef {
235 code: "module-unknown",
236 title: "Module unknown",
237 description: "`fz add` / `fz init` name a slug the catalog does not carry.",
238 },
239 not_deployed: DoctorCodeDef {
240 code: "not-deployed",
241 title: "Not deployed",
242 description: "`fz verify` finds no deployment recorded for this manifest.",
243 },
244 production_deploy_unauthorized: DoctorCodeDef {
245 code: "production-deploy-unauthorized",
246 title: "Production deploy unauthorized",
247 description: "A production deploy without the explicit --i-am-deploying-to-production consent flag.",
248 },
249 stale_plan: DoctorCodeDef {
250 code: "stale-plan",
251 title: "Stale plan",
252 description: "`fz deploy --plan <digest>`: the digest does not match a plan recomputed from the current inputs.",
253 },
254 composition_drift: DoctorCodeDef {
255 code: "composition-drift",
256 title: "Composition drift",
257 description: "The recorded deployment's module set no longer matches the manifest's resolved composition.",
258 },
259 config_drift: DoctorCodeDef {
260 code: "config-drift",
261 title: "Config drift",
262 description: "The recorded deployment's config no longer matches the manifest's.",
263 },
264 deploy_plan_required: DoctorCodeDef {
265 code: "deploy-plan-required",
266 title: "Deploy plan required",
267 description: "`fz deploy` refuses to run without --plan <digest>; approval is the point.",
268 },
269 deploy_record_unreadable: DoctorCodeDef {
270 code: "deploy-record-unreadable",
271 title: "Deploy record unreadable",
272 description: "The .harness-deploy.json record could not be read or parsed.",
273 },
274 destructive_change_unauthorized: DoctorCodeDef {
275 code: "destructive-change-unauthorized",
276 title: "Destructive change unauthorized",
277 description: "The plan removes modules without the --i-am-removing-modules consent; they stop being served and no data is deleted.",
278 },
279 env_drift: DoctorCodeDef {
280 code: "env-drift",
281 title: "Environment drift",
282 description: "The resolved deployment environment changed since the recorded deployment.",
283 },
284 manifest_drift: DoctorCodeDef {
285 code: "manifest-drift",
286 title: "Manifest drift",
287 description: "The recorded digest no longer matches a recomputed plan and no narrower drift explains it.",
288 },
289 manifest_exists: DoctorCodeDef {
290 code: "manifest-exists",
291 title: "Manifest exists",
292 description: "`fz init` was pointed at an existing manifest without --force.",
293 },
294 manifest_invalid: DoctorCodeDef {
295 code: "manifest-invalid",
296 title: "Manifest invalid",
297 description: "A manifest parsed but does not resolve: empty name/host, duplicate module, or a refused selection.",
298 },
299 manifest_unreadable: DoctorCodeDef {
300 code: "manifest-unreadable",
301 title: "Manifest unreadable",
302 description: "The venture manifest could not be read or parsed.",
303 },
304 manifest_write_failed: DoctorCodeDef {
305 code: "manifest-write-failed",
306 title: "Manifest write failed",
307 description: "A manifest could not be written back to disk.",
308 },
309 client_write_failed: DoctorCodeDef {
310 code: "client-write-failed",
311 title: "Client write failed",
312 description: "A file of the generated TypeScript client could not be written to --out.",
313 },
314 surface_action_unreadable: DoctorCodeDef {
315 code: "surface-action-unreadable",
316 title: "Tables action unreadable",
317 description: "The tables module publishes an action outside the five-verb vocabulary the generator reads.",
318 },
319 surface_invalid: DoctorCodeDef {
320 code: "surface-invalid",
321 title: "Not a surface document",
322 description: "The document parsed as JSON but is not a /__surface document.",
323 },
324 surface_no_tables_module: DoctorCodeDef {
325 code: "surface-no-tables-module",
326 title: "No tables module",
327 description: "The document declares no `tables` module, so there is no contract to generate a client from.",
328 },
329 surface_table_name_invalid: DoctorCodeDef {
330 code: "surface-table-name-invalid",
331 title: "Table name unsupported",
332 description: "The tables module publishes a table whose name is not a safe identifier (lowercase [a-z][a-z0-9]*, no double or trailing underscore), which the generator would interpolate into the generated TypeScript.",
333 },
334 surface_unreadable: DoctorCodeDef {
335 code: "surface-unreadable",
336 title: "Surface document unreadable",
337 description: "The /__surface document could not be read, or is not valid JSON.",
338 },
339 surface_unsupported_contract: DoctorCodeDef {
340 code: "surface-unsupported-contract",
341 title: "Surface contract unsupported",
342 description: "The document speaks a surface_api contract version this generator does not read.",
343 },
344 admin_token_too_short: DoctorCodeDef {
345 code: "admin-token-too-short",
346 title: "Admin token too short",
347 description: "ADMIN_TOKEN is set but shorter than the 32-byte floor the boot enforces in every environment; lengthen it or remove it.",
348 },
349 rate_limiter_not_effective: DoctorCodeDef {
350 code: "rate-limiter-not-effective",
351 title: "Rate limiter not effective",
352 description: "A production venture takes public writes or admin routes but the RateLimiter port is not resolved; set HARNESS_ALLOW_UNLIMITED_PUBLIC_ROUTES to a reason to serve unlimited.",
353 },
354 hmac_webhook_secret_missing: DoctorCodeDef {
355 code: "hmac-webhook-secret-missing",
356 title: "Webhook signing secret missing",
357 description: "A production venture has a module verifying its signature routes with the core webhook_signature HMAC scheme, but the module-scoped secret key it named is unset; its handler refuses every delivery.",
358 },
359};
360
361#[must_use]
363pub fn registry() -> Vec<&'static DoctorCodeDef> {
364 vec![
365 &CODES.admin_token_too_short,
366 &CODES.auth_not_configured,
367 &CODES.captcha_not_effective,
368 &CODES.card_data_in_migration,
369 &CODES.client_write_failed,
370 &CODES.composition_drift,
371 &CODES.config_drift,
372 &CODES.deploy_plan_required,
373 &CODES.deploy_record_unreadable,
374 &CODES.destructive_change_unauthorized,
375 &CODES.env_drift,
376 &CODES.harness_api_mismatch,
377 &CODES.hmac_webhook_secret_missing,
378 &CODES.locked_migration_edited,
379 &CODES.locked_migration_missing,
380 &CODES.lockfile_unreadable,
381 &CODES.manifest_drift,
382 &CODES.manifest_exists,
383 &CODES.manifest_invalid,
384 &CODES.manifest_unreadable,
385 &CODES.manifest_write_failed,
386 &CODES.migration_not_collected,
387 &CODES.migration_not_declared,
388 &CODES.module_self_check,
389 &CODES.module_unknown,
390 &CODES.non_portable_sql,
391 &CODES.not_deployed,
392 &CODES.payments_webhook_secret_missing,
393 &CODES.production_deploy_unauthorized,
394 &CODES.push_required_but_unrouted,
395 &CODES.push_transport_misconfigured,
396 &CODES.rate_limiter_not_effective,
397 &CODES.sidecar_mount_invalid,
398 &CODES.sidecar_shadows_module,
399 &CODES.stale_plan,
400 &CODES.surface_action_unreadable,
401 &CODES.surface_invalid,
402 &CODES.surface_no_tables_module,
403 &CODES.surface_table_name_invalid,
404 &CODES.surface_unreadable,
405 &CODES.surface_unsupported_contract,
406 ]
407}
408
409#[cfg(test)]
410mod tests {
411 use super::{CODES, registry};
412
413 #[test]
416 fn codes_are_kebab_case() {
417 for def in registry() {
418 assert!(
419 !def.code.is_empty()
420 && def
421 .code
422 .bytes()
423 .all(|byte| byte.is_ascii_lowercase() || byte == b'-')
424 && !def.code.starts_with('-')
425 && !def.code.ends_with('-')
426 && !def.code.contains("--"),
427 "{:?} must be lowercase kebab-case",
428 def.code
429 );
430 assert!(!def.title.is_empty(), "{:?} needs a title", def.code);
431 assert!(
432 !def.description.is_empty(),
433 "{:?} needs a description",
434 def.code
435 );
436 }
437 }
438
439 #[test]
443 fn registry_covers_every_field_exactly_once_and_is_sorted() {
444 let registered = registry();
445 let mut by_slug: Vec<&str> = registered.iter().map(|def| def.code).collect();
446 let mut expected = by_slug.clone();
447 expected.sort_unstable();
448 assert_eq!(by_slug, expected, "registry must stay sorted by code");
449 by_slug.dedup();
450 assert_eq!(by_slug.len(), registered.len(), "duplicate code registered");
451
452 let fields = [
453 CODES.admin_token_too_short.code,
454 CODES.auth_not_configured.code,
455 CODES.harness_api_mismatch.code,
456 CODES.hmac_webhook_secret_missing.code,
457 CODES.lockfile_unreadable.code,
458 CODES.migration_not_collected.code,
459 CODES.migration_not_declared.code,
460 CODES.locked_migration_missing.code,
461 CODES.locked_migration_edited.code,
462 CODES.captcha_not_effective.code,
463 CODES.payments_webhook_secret_missing.code,
464 CODES.sidecar_shadows_module.code,
465 CODES.sidecar_mount_invalid.code,
466 CODES.non_portable_sql.code,
467 CODES.card_data_in_migration.code,
468 CODES.push_transport_misconfigured.code,
469 CODES.push_required_but_unrouted.code,
470 CODES.rate_limiter_not_effective.code,
471 CODES.module_self_check.code,
472 CODES.module_unknown.code,
473 CODES.not_deployed.code,
474 CODES.production_deploy_unauthorized.code,
475 CODES.stale_plan.code,
476 CODES.composition_drift.code,
477 CODES.config_drift.code,
478 CODES.deploy_plan_required.code,
479 CODES.deploy_record_unreadable.code,
480 CODES.destructive_change_unauthorized.code,
481 CODES.env_drift.code,
482 CODES.manifest_drift.code,
483 CODES.manifest_exists.code,
484 CODES.manifest_invalid.code,
485 CODES.manifest_unreadable.code,
486 CODES.manifest_write_failed.code,
487 CODES.client_write_failed.code,
488 CODES.surface_action_unreadable.code,
489 CODES.surface_invalid.code,
490 CODES.surface_no_tables_module.code,
491 CODES.surface_table_name_invalid.code,
492 CODES.surface_unreadable.code,
493 CODES.surface_unsupported_contract.code,
494 ];
495 assert_eq!(fields.len(), registered.len());
496 for field in fields {
497 assert!(
498 registered.iter().any(|def| def.code == field),
499 "{field} is in the catalogue but not the registry"
500 );
501 }
502 }
503}