cratefield-cli 0.6.1

The fz CLI for Cratefield ventures: migrations collect/apply, doctor, modules, push
Documentation
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
//! The catalogue of every `fz` error code: stable kebab-case slugs an
//! agent can branch on forever, the seed of the MCP server's "stable
//! error-code catalogue" (issue #160). Modelled on
//! `cratefield_core::problems`: a struct of `&'static` definitions, a
//! registry function, stable string codes. Seeded by `fz doctor` (#220);
//! the agent-safe workflow commands (`fz plan` / `deploy` / `add` /
//! `init` / `verify`, harness #140) appended theirs.
//!
//! The contract: a code is never renamed, reworded or removed. A failure
//! *message* may change wording freely; a *code* may not. New codes append.

/// Definition of one `fz` error code.
#[derive(Debug, Clone, Copy)]
pub struct DoctorCodeDef {
    /// The stable kebab-case code, exactly as `fz doctor --json` emits it.
    pub code: &'static str,
    /// Short, stable human title.
    pub title: &'static str,
    /// One-line description for consumers; not part of the JSON output.
    pub description: &'static str,
}

/// Every code the doctor emits. Keep the registry below sorted by code.
pub struct Codes {
    /// A module was built against a different `HARNESS_API` than the
    /// core crate it linked against.
    pub harness_api_mismatch: DoctorCodeDef,
    /// The `.harness-lock.json` could not be read or parsed.
    pub lockfile_unreadable: DoctorCodeDef,
    /// A module migration has no locked file yet — run
    /// `fz migrations collect` first.
    pub migration_not_collected: DoctorCodeDef,
    /// A locked migration's file is missing from disk.
    pub locked_migration_missing: DoctorCodeDef,
    /// A locked migration's file no longer matches its pinned sha256.
    pub locked_migration_edited: DoctorCodeDef,
    /// The production captcha rule: captcha-guarded public writes
    /// without an effectively configured Captcha port.
    pub captcha_not_effective: DoctorCodeDef,
    /// The production payments rule: signature-guarded routes without
    /// `STRIPE_WEBHOOK_SECRET`.
    pub payments_webhook_secret_missing: DoctorCodeDef,
    /// The production auth rule: a module that must identify a caller,
    /// in a deployment that cannot.
    pub auth_not_configured: DoctorCodeDef,
    /// A sidecar mount names a module compiled into this venture; the
    /// runtime ignores the mount.
    pub sidecar_shadows_module: DoctorCodeDef,
    /// The sidecar mount table could not be parsed.
    pub sidecar_mount_invalid: DoctorCodeDef,
    /// A module found a problem in its own embedded data — a catalog
    /// missing a translation it declared, and anything else a module
    /// reports from `Module::self_check`.
    pub module_self_check: DoctorCodeDef,
    /// A migration contains SQL outside the portable subset.
    pub non_portable_sql: DoctorCodeDef,
    /// A migration contains card data (number, verification code or
    /// full expiry).
    pub card_data_in_migration: DoctorCodeDef,
    /// A push transport is half-wired, or its credentials were refused:
    /// either way it routes nothing and every send to it reports
    /// `NotConfigured`.
    pub push_transport_misconfigured: DoctorCodeDef,
    /// The production push rule: modules that `requires()` the Push port
    /// on a venture whose environment routes no transport at all.
    pub push_required_but_unrouted: DoctorCodeDef,
    /// The workflow commands (#140): `fz add` / `fz init` name a slug
    /// the catalog does not carry.
    pub module_unknown: DoctorCodeDef,
    /// `fz verify`: no deployment is recorded for this manifest.
    pub not_deployed: DoctorCodeDef,
    /// A production deploy without the explicit
    /// `--i-am-deploying-to-production` consent flag.
    pub production_deploy_unauthorized: DoctorCodeDef,
    /// `fz deploy --plan <digest>`: the digest does not match a plan
    /// recomputed from the current inputs.
    pub stale_plan: DoctorCodeDef,
    /// `fz verify`: the recorded deployment's module set no longer
    /// matches the manifest's resolved composition.
    pub composition_drift: DoctorCodeDef,
    /// `fz verify`: the recorded deployment's config no longer matches
    /// the manifest's.
    pub config_drift: DoctorCodeDef,
    /// `fz deploy` was run without `--plan <digest>`: approval is the
    /// point, so there is no default.
    pub deploy_plan_required: DoctorCodeDef,
    /// The `.harness-deploy.json` record could not be read or parsed.
    pub deploy_record_unreadable: DoctorCodeDef,
    /// The plan removes modules from the served composition without the
    /// explicit `--i-am-removing-modules` consent flag; they stop being
    /// served and no data is deleted.
    pub destructive_change_unauthorized: DoctorCodeDef,
    /// `fz verify`: the resolved deployment environment changed since
    /// the recorded deployment.
    pub env_drift: DoctorCodeDef,
    /// `fz verify`: the recorded digest no longer matches a plan
    /// recomputed now, and no narrower drift (composition, config, env)
    /// explains it — the change is in the venture's identity or seed.
    pub manifest_drift: DoctorCodeDef,
    /// `fz init` was pointed at a manifest that already exists, without
    /// `--force`.
    pub manifest_exists: DoctorCodeDef,
    /// A manifest parsed but does not resolve: empty name/host, a
    /// duplicate module, or a selection the catalog refuses.
    pub manifest_invalid: DoctorCodeDef,
    /// The venture manifest could not be read or parsed.
    pub manifest_unreadable: DoctorCodeDef,
    /// A manifest could not be written back to disk.
    pub manifest_write_failed: DoctorCodeDef,
    /// `fz client-ts` (issue #155): a generated file could not be written
    /// to `--out`.
    pub client_write_failed: DoctorCodeDef,
    /// `fz client-ts`: the tables module publishes an action outside the
    /// five-verb vocabulary (`list-`/`read-`/`create-`/`replace-`/
    /// `delete-`, then the table name) the generator reads.
    pub surface_action_unreadable: DoctorCodeDef,
    /// `fz client-ts`: the document parsed as JSON but is not a
    /// `/__surface` document.
    pub surface_invalid: DoctorCodeDef,
    /// `fz client-ts`: the document declares no `tables` module, so there
    /// is no contract to generate a client from.
    pub surface_no_tables_module: DoctorCodeDef,
    /// `fz client-ts`: the tables module publishes a table whose name is
    /// not a safe identifier, which the generator would interpolate into
    /// the generated TypeScript.
    pub surface_table_name_invalid: DoctorCodeDef,
    /// `fz client-ts`: the `/__surface` document could not be read, or is
    /// not valid JSON.
    pub surface_unreadable: DoctorCodeDef,
    /// `fz client-ts`: the document speaks a `surface_api` contract
    /// version this generator does not read.
    pub surface_unsupported_contract: DoctorCodeDef,
    /// `ADMIN_TOKEN` is set but shorter than the 32-byte floor
    /// `HarnessConfig::from_config` enforces in every environment
    /// (issue #437).
    pub admin_token_too_short: DoctorCodeDef,
    /// The production rate-limiter rule (issue #437): a venture with
    /// public writes or admin routes on a runtime whose `RateLimiter` port
    /// is not resolved, and no recorded acceptance.
    pub rate_limiter_not_effective: DoctorCodeDef,
    /// The production webhook-secret rule (issue #533): a module that
    /// verifies its signature routes with the core `webhook_signature`
    /// HMAC scheme, in a deployment that has not set the secret key it
    /// named.
    pub hmac_webhook_secret_missing: DoctorCodeDef,
}

pub const CODES: Codes = Codes {
    harness_api_mismatch: DoctorCodeDef {
        code: "harness-api-mismatch",
        title: "Harness API mismatch",
        description: "A module was built against a different HARNESS_API than the core crate this venture linked.",
    },
    lockfile_unreadable: DoctorCodeDef {
        code: "lockfile-unreadable",
        title: "Lockfile unreadable",
        description: "The .harness-lock.json could not be read or parsed.",
    },
    migration_not_collected: DoctorCodeDef {
        code: "migration-not-collected",
        title: "Migration not collected",
        description: "A module migration has no locked file yet; run `fz migrations collect`.",
    },
    locked_migration_missing: DoctorCodeDef {
        code: "locked-migration-missing",
        title: "Locked migration missing",
        description: "A locked migration's file is missing from disk.",
    },
    locked_migration_edited: DoctorCodeDef {
        code: "locked-migration-edited",
        title: "Locked migration edited",
        description: "A locked migration's file no longer matches its pinned sha256; restore it or add a new migration.",
    },
    captcha_not_effective: DoctorCodeDef {
        code: "captcha-not-effective",
        title: "Captcha not effective",
        description: "A production venture has captcha-guarded public writes but the Captcha port is not effectively configured.",
    },
    payments_webhook_secret_missing: DoctorCodeDef {
        code: "payments-webhook-secret-missing",
        title: "Payments webhook secret missing",
        description: "A production venture has signature-guarded routes but STRIPE_WEBHOOK_SECRET is unset.",
    },
    auth_not_configured: DoctorCodeDef {
        code: "auth-not-configured",
        title: "Cannot identify a caller",
        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.",
    },
    sidecar_shadows_module: DoctorCodeDef {
        code: "sidecar-shadows-module",
        title: "Sidecar shadows a compiled-in module",
        description: "A sidecar mount names a module compiled into this venture; the runtime ignores the mount.",
    },
    sidecar_mount_invalid: DoctorCodeDef {
        code: "sidecar-mount-invalid",
        title: "Sidecar mount invalid",
        description: "The sidecar mount table could not be parsed.",
    },
    module_self_check: DoctorCodeDef {
        code: "module-self-check",
        title: "Module self-check failed",
        description: "A module reported a problem in its own embedded data, such as a \
                      localisation catalog missing a message it declared.",
    },
    non_portable_sql: DoctorCodeDef {
        code: "non-portable-sql",
        title: "Non-portable SQL",
        description: "A migration contains SQL outside the portable subset (dialect-specific tokens).",
    },
    card_data_in_migration: DoctorCodeDef {
        code: "card-data-in-migration",
        title: "Card data in migration",
        description: "A migration contains card data: a card number, verification code or full expiry.",
    },
    push_transport_misconfigured: DoctorCodeDef {
        code: "push-transport-misconfigured",
        title: "Push transport misconfigured",
        description: "A push transport has some of its variables set and some unset, or its credentials were refused; it is left unrouted.",
    },
    push_required_but_unrouted: DoctorCodeDef {
        code: "push-required-but-unrouted",
        title: "Push required but unrouted",
        description: "A production venture has modules that require the Push port but its environment routes no push transport.",
    },
    module_unknown: DoctorCodeDef {
        code: "module-unknown",
        title: "Module unknown",
        description: "`fz add` / `fz init` name a slug the catalog does not carry.",
    },
    not_deployed: DoctorCodeDef {
        code: "not-deployed",
        title: "Not deployed",
        description: "`fz verify` finds no deployment recorded for this manifest.",
    },
    production_deploy_unauthorized: DoctorCodeDef {
        code: "production-deploy-unauthorized",
        title: "Production deploy unauthorized",
        description: "A production deploy without the explicit --i-am-deploying-to-production consent flag.",
    },
    stale_plan: DoctorCodeDef {
        code: "stale-plan",
        title: "Stale plan",
        description: "`fz deploy --plan <digest>`: the digest does not match a plan recomputed from the current inputs.",
    },
    composition_drift: DoctorCodeDef {
        code: "composition-drift",
        title: "Composition drift",
        description: "The recorded deployment's module set no longer matches the manifest's resolved composition.",
    },
    config_drift: DoctorCodeDef {
        code: "config-drift",
        title: "Config drift",
        description: "The recorded deployment's config no longer matches the manifest's.",
    },
    deploy_plan_required: DoctorCodeDef {
        code: "deploy-plan-required",
        title: "Deploy plan required",
        description: "`fz deploy` refuses to run without --plan <digest>; approval is the point.",
    },
    deploy_record_unreadable: DoctorCodeDef {
        code: "deploy-record-unreadable",
        title: "Deploy record unreadable",
        description: "The .harness-deploy.json record could not be read or parsed.",
    },
    destructive_change_unauthorized: DoctorCodeDef {
        code: "destructive-change-unauthorized",
        title: "Destructive change unauthorized",
        description: "The plan removes modules without the --i-am-removing-modules consent; they stop being served and no data is deleted.",
    },
    env_drift: DoctorCodeDef {
        code: "env-drift",
        title: "Environment drift",
        description: "The resolved deployment environment changed since the recorded deployment.",
    },
    manifest_drift: DoctorCodeDef {
        code: "manifest-drift",
        title: "Manifest drift",
        description: "The recorded digest no longer matches a recomputed plan and no narrower drift explains it.",
    },
    manifest_exists: DoctorCodeDef {
        code: "manifest-exists",
        title: "Manifest exists",
        description: "`fz init` was pointed at an existing manifest without --force.",
    },
    manifest_invalid: DoctorCodeDef {
        code: "manifest-invalid",
        title: "Manifest invalid",
        description: "A manifest parsed but does not resolve: empty name/host, duplicate module, or a refused selection.",
    },
    manifest_unreadable: DoctorCodeDef {
        code: "manifest-unreadable",
        title: "Manifest unreadable",
        description: "The venture manifest could not be read or parsed.",
    },
    manifest_write_failed: DoctorCodeDef {
        code: "manifest-write-failed",
        title: "Manifest write failed",
        description: "A manifest could not be written back to disk.",
    },
    client_write_failed: DoctorCodeDef {
        code: "client-write-failed",
        title: "Client write failed",
        description: "A file of the generated TypeScript client could not be written to --out.",
    },
    surface_action_unreadable: DoctorCodeDef {
        code: "surface-action-unreadable",
        title: "Tables action unreadable",
        description: "The tables module publishes an action outside the five-verb vocabulary the generator reads.",
    },
    surface_invalid: DoctorCodeDef {
        code: "surface-invalid",
        title: "Not a surface document",
        description: "The document parsed as JSON but is not a /__surface document.",
    },
    surface_no_tables_module: DoctorCodeDef {
        code: "surface-no-tables-module",
        title: "No tables module",
        description: "The document declares no `tables` module, so there is no contract to generate a client from.",
    },
    surface_table_name_invalid: DoctorCodeDef {
        code: "surface-table-name-invalid",
        title: "Table name unsupported",
        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.",
    },
    surface_unreadable: DoctorCodeDef {
        code: "surface-unreadable",
        title: "Surface document unreadable",
        description: "The /__surface document could not be read, or is not valid JSON.",
    },
    surface_unsupported_contract: DoctorCodeDef {
        code: "surface-unsupported-contract",
        title: "Surface contract unsupported",
        description: "The document speaks a surface_api contract version this generator does not read.",
    },
    admin_token_too_short: DoctorCodeDef {
        code: "admin-token-too-short",
        title: "Admin token too short",
        description: "ADMIN_TOKEN is set but shorter than the 32-byte floor the boot enforces in every environment; lengthen it or remove it.",
    },
    rate_limiter_not_effective: DoctorCodeDef {
        code: "rate-limiter-not-effective",
        title: "Rate limiter not effective",
        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.",
    },
    hmac_webhook_secret_missing: DoctorCodeDef {
        code: "hmac-webhook-secret-missing",
        title: "Webhook signing secret missing",
        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.",
    },
};

/// Every `fz` code definition, for tests and docs. Sorted by code.
#[must_use]
pub fn registry() -> Vec<&'static DoctorCodeDef> {
    vec![
        &CODES.admin_token_too_short,
        &CODES.auth_not_configured,
        &CODES.captcha_not_effective,
        &CODES.card_data_in_migration,
        &CODES.client_write_failed,
        &CODES.composition_drift,
        &CODES.config_drift,
        &CODES.deploy_plan_required,
        &CODES.deploy_record_unreadable,
        &CODES.destructive_change_unauthorized,
        &CODES.env_drift,
        &CODES.harness_api_mismatch,
        &CODES.hmac_webhook_secret_missing,
        &CODES.locked_migration_edited,
        &CODES.locked_migration_missing,
        &CODES.lockfile_unreadable,
        &CODES.manifest_drift,
        &CODES.manifest_exists,
        &CODES.manifest_invalid,
        &CODES.manifest_unreadable,
        &CODES.manifest_write_failed,
        &CODES.migration_not_collected,
        &CODES.module_self_check,
        &CODES.module_unknown,
        &CODES.non_portable_sql,
        &CODES.not_deployed,
        &CODES.payments_webhook_secret_missing,
        &CODES.production_deploy_unauthorized,
        &CODES.push_required_but_unrouted,
        &CODES.push_transport_misconfigured,
        &CODES.rate_limiter_not_effective,
        &CODES.sidecar_mount_invalid,
        &CODES.sidecar_shadows_module,
        &CODES.stale_plan,
        &CODES.surface_action_unreadable,
        &CODES.surface_invalid,
        &CODES.surface_no_tables_module,
        &CODES.surface_table_name_invalid,
        &CODES.surface_unreadable,
        &CODES.surface_unsupported_contract,
    ]
}

#[cfg(test)]
mod tests {
    use super::{CODES, registry};

    /// A code is lowercase kebab-case: an agent regexes it, so it must
    /// stay machine-shaped forever.
    #[test]
    fn codes_are_kebab_case() {
        for def in registry() {
            assert!(
                !def.code.is_empty()
                    && def
                        .code
                        .bytes()
                        .all(|byte| byte.is_ascii_lowercase() || byte == b'-')
                    && !def.code.starts_with('-')
                    && !def.code.ends_with('-')
                    && !def.code.contains("--"),
                "{:?} must be lowercase kebab-case",
                def.code
            );
            assert!(!def.title.is_empty(), "{:?} needs a title", def.code);
            assert!(
                !def.description.is_empty(),
                "{:?} needs a description",
                def.code
            );
        }
    }

    /// Every catalogue field is registered exactly once — the registry
    /// is the whole catalogue, and a code that drifts from its slug
    /// fails here.
    #[test]
    fn registry_covers_every_field_exactly_once_and_is_sorted() {
        let registered = registry();
        let mut by_slug: Vec<&str> = registered.iter().map(|def| def.code).collect();
        let mut expected = by_slug.clone();
        expected.sort_unstable();
        assert_eq!(by_slug, expected, "registry must stay sorted by code");
        by_slug.dedup();
        assert_eq!(by_slug.len(), registered.len(), "duplicate code registered");

        let fields = [
            CODES.admin_token_too_short.code,
            CODES.auth_not_configured.code,
            CODES.harness_api_mismatch.code,
            CODES.hmac_webhook_secret_missing.code,
            CODES.lockfile_unreadable.code,
            CODES.migration_not_collected.code,
            CODES.locked_migration_missing.code,
            CODES.locked_migration_edited.code,
            CODES.captcha_not_effective.code,
            CODES.payments_webhook_secret_missing.code,
            CODES.sidecar_shadows_module.code,
            CODES.sidecar_mount_invalid.code,
            CODES.non_portable_sql.code,
            CODES.card_data_in_migration.code,
            CODES.push_transport_misconfigured.code,
            CODES.push_required_but_unrouted.code,
            CODES.rate_limiter_not_effective.code,
            CODES.module_self_check.code,
            CODES.module_unknown.code,
            CODES.not_deployed.code,
            CODES.production_deploy_unauthorized.code,
            CODES.stale_plan.code,
            CODES.composition_drift.code,
            CODES.config_drift.code,
            CODES.deploy_plan_required.code,
            CODES.deploy_record_unreadable.code,
            CODES.destructive_change_unauthorized.code,
            CODES.env_drift.code,
            CODES.manifest_drift.code,
            CODES.manifest_exists.code,
            CODES.manifest_invalid.code,
            CODES.manifest_unreadable.code,
            CODES.manifest_write_failed.code,
            CODES.client_write_failed.code,
            CODES.surface_action_unreadable.code,
            CODES.surface_invalid.code,
            CODES.surface_no_tables_module.code,
            CODES.surface_table_name_invalid.code,
            CODES.surface_unreadable.code,
            CODES.surface_unsupported_contract.code,
        ];
        assert_eq!(fields.len(), registered.len());
        for field in fields {
            assert!(
                registered.iter().any(|def| def.code == field),
                "{field} is in the catalogue but not the registry"
            );
        }
    }
}