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