caixa-feira 0.1.7

feira — CLI for the caixa tatara-lisp package system (init, add, lock, build, nix).
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
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
//! Shared per-verb IO entry-point for the `<root>/caixa.lisp` manifest.
//!
//! Closes the PRIME DIRECTIVE duplication on the verb-entry IO axis the
//! `feira` CLI carries. Before this lift the same four-line shape —
//! `root.join("caixa.lisp")` → `std::fs::read_to_string` →
//! `Caixa::from_lisp`, all with verbatim `"reading {…}"` and
//! `"parsing {…}"` `.with_context(...)` strings — appeared at every
//! `feira` verb's `Run::run` entry-point: `feira add` (cmd/add.rs),
//! `feira build` (cmd/build.rs), `feira chart` (cmd/chart.rs),
//! `feira deploy` (cmd/deploy.rs), `feira lock` (cmd/lock.rs),
//! `feira nix` (cmd/nix.rs), `feira publish` (cmd/publish.rs),
//! `feira resolve` (cmd/resolve.rs), and the `feira app` verbs'
//! shared `load_aplicacao` helper (cmd/app.rs). Nine inline copies of
//! the same load-bearing diagnostic shape, each one another place a
//! future change to the `"reading {…}"` / `"parsing {…}"` discipline
//! has to remember to touch.
//!
//! After the lift every per-verb entry-point routes through
//! [`load_caixa`]: the manifest-filename literal lives at exactly one
//! `pub(crate) const CAIXA_MANIFEST_FILENAME` definition, the
//! `read_to_string` and `Caixa::from_lisp` calls are wrapped with the
//! canonical context strings at exactly one call-site, and the
//! underlying `tatara_lisp::LispError` remains reachable on the
//! anyhow cause chain for typed-view downstream callers (peer with
//! the per-renderer typed-view discipline `KindMismatch`,
//! `ServicoCountMismatch`, etc. follow). A future per-caixa-root
//! `feira` verb — the future `feira oci publish` per-Servico OCI
//! packager, the future `feira validate --strict` per-caixa
//! admission verb, the future M4 `feira reconcile` per-cluster
//! diff verb the absorption-roadmap acknowledges — inherits the
//! canonical entry-point shape through this helper rather than
//! re-derives a skewed copy.

use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};
use caixa_core::Caixa;

/// The canonical caixa manifest filename. Every `feira` verb's
/// per-caixa-root entry-point resolves `<root>/caixa.lisp` through
/// [`caixa_manifest_path`] and reaches for the manifest through
/// [`load_caixa`]; no verb should hard-code the literal string.
pub(crate) const CAIXA_MANIFEST_FILENAME: &str = "caixa.lisp";

/// The canonical "where does `--path` default to?" answer — the
/// current working directory as a relative `PathBuf`. Every `feira`
/// verb's `Option<PathBuf>` `--path` flag resolves through
/// [`caixa_root`] with this literal as the absent-flag fallback; no
/// verb should hard-code the `"."` string.
pub(crate) const CAIXA_ROOT_DEFAULT_DIRNAME: &str = ".";

/// Resolve the per-caixa-root manifest path.
pub(crate) fn caixa_manifest_path(root: &Path) -> PathBuf {
    root.join(CAIXA_MANIFEST_FILENAME)
}

/// Resolve a `feira` verb's `--path` flag into the canonical caixa
/// root, defaulting to the current working directory when the flag
/// is absent.
///
/// Closes the PRIME DIRECTIVE duplication (THEORY.md §I.5 — "the
/// duplication budget is zero") on the per-verb `--path` fallback
/// axis. Before this lift the same one-line shape —
/// `self.path.clone().unwrap_or_else(|| PathBuf::from("."))` —
/// appeared verbatim at every `feira` verb's `Run::run` entry-point:
/// `feira add` (cmd/add.rs), `feira build` (cmd/build.rs),
/// `feira chart` (cmd/chart.rs), `feira deploy` (cmd/deploy.rs),
/// `feira lock` (cmd/lock.rs), `feira nix` (cmd/nix.rs),
/// `feira publish` (cmd/publish.rs), `feira resolve`
/// (cmd/resolve.rs), and the `feira tofu render` helper plus the
/// `feira app` verbs' shared `load_aplicacao` helper (cmd/app.rs).
/// Ten inline copies of the same load-bearing fallback decision,
/// each one another place a future change to the "where does
/// `--path` default land?" discipline has to remember to touch —
/// the future M4 `feira reconcile` per-cluster diff verb the
/// absorption-roadmap acknowledges, a future `CAIXA_ROOT` env-var
/// override, a future "walk-up-until-`caixa.lisp`" discovery —
/// every one inherits the drift-budget if the choice lives at every
/// verb.
///
/// After the lift every per-verb entry-point routes through this
/// resolver: the fallback decision lives at exactly one call-site,
/// the literal `"."` string lands at exactly one definition (the
/// canonical [`CAIXA_ROOT_DEFAULT_DIRNAME`] constant, peer with the
/// [`CAIXA_MANIFEST_FILENAME`] constant on the manifest-filename
/// axis), and a future `feira` verb inherits the canonical shape
/// through this helper rather than re-derives a skewed copy. Mirrors
/// [`caixa_manifest_path`] on the per-caixa-root path-resolution
/// surface — together the two resolvers + the [`load_caixa`] /
/// [`load_yaml`] readers form the canonical per-verb IO entry-point
/// shape (`--path` → root → manifest path → typed Caixa).
pub(crate) fn caixa_root(path: Option<&Path>) -> PathBuf {
    path.map_or_else(
        || PathBuf::from(CAIXA_ROOT_DEFAULT_DIRNAME),
        Path::to_path_buf,
    )
}

/// Read + parse the per-caixa-root `caixa.lisp` into a typed
/// [`Caixa`].
///
/// The canonical per-verb IO entry-point — every `feira` verb that
/// consumes a typed `Caixa` routes through this helper, so the
/// `"reading {…}"` / `"parsing {…}"` context strings stay verbatim
/// across every verb's rendered diagnostic. The underlying
/// `tatara_lisp::LispError` (the `Caixa::from_lisp` error type) is
/// preserved through the anyhow `.with_context(...)` wrap and remains
/// reachable on the cause chain — peer with the per-renderer
/// typed-view discipline (`KindMismatch`, `ServicoCountMismatch`,
/// etc.) the substrate's per-Servico / per-Aplicacao entry-points
/// already follow.
pub(crate) fn load_caixa(root: &Path) -> Result<Caixa> {
    let manifest = caixa_manifest_path(root);
    let src = std::fs::read_to_string(&manifest)
        .with_context(|| format!("reading {}", manifest.display()))?;
    Caixa::from_lisp(&src).with_context(|| format!("parsing {}", manifest.display()))
}

/// Read + parse a YAML file at `path` into an untyped
/// [`serde_yaml::Value`].
///
/// Closes the PRIME DIRECTIVE duplication on the per-verb YAML IO
/// axis. Before this lift the same four-line shape —
/// [`std::fs::read_to_string`] + [`serde_yaml::from_str`], both with
/// verbatim `"reading {…}"` / `"parsing {…}"` `.with_context(...)`
/// strings — appeared at every `feira` verb that consumes an
/// arbitrary YAML file: the per-Servico computeunit loader (`feira
/// chart` + `feira deploy` via [`super::chart::load_first_servico_yaml`])
/// and the cluster-side fleet-programs HelmRelease loader (`feira
/// deploy` itself). Two inline copies of the same load-bearing
/// diagnostic shape, each one another place a future change to the
/// `"reading {…}"` / `"parsing {…}"` discipline has to remember to
/// touch, and another place a future per-YAML `feira` verb (the
/// future `feira validate --strict` per-caixa admission verb, the
/// future M4 `feira reconcile` cluster-diff verb the
/// absorption-roadmap acknowledges) would re-derive a skewed copy.
///
/// After the lift every per-YAML `feira` verb routes through this
/// helper: the `"reading {…}"` / `"parsing {…}"` context strings live
/// at exactly one call-site, the underlying `serde_yaml::Error`
/// remains reachable on the anyhow cause chain (peer with the
/// per-renderer [`caixa_core::KindMismatch`] /
/// [`caixa_core::ServicoCountMismatch`] downcast discipline), and the
/// canonical entry-point shape is a single helper rather than two
/// independently-evolved copies. Mirrors [`load_caixa`] on the peer
/// `<root>/caixa.lisp` axis, completing the canonical per-verb IO
/// surface (the manifest loader + this YAML loader).
pub(crate) fn load_yaml(path: &Path) -> Result<serde_yaml::Value> {
    let src =
        std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
    serde_yaml::from_str(&src).with_context(|| format!("parsing {}", path.display()))
}

/// Validate a `feira` verb's `--cluster <name>` flag value at the
/// per-verb entry-point against the canonical DNS-1123 label shape every
/// downstream consumer (the K8s apiserver's per-Service / per-Pod /
/// per-CR `metadata.name` axis, the `:placement :clusters` typed slot,
/// the `<k8s-repo>/clusters/<cluster>/…` GitOps path join) demands.
///
/// Closes the PRIME DIRECTIVE duplication (THEORY.md §I.3.5 — "the
/// duplication budget is zero") on the per-verb `--cluster` axis. Before
/// this lift the same `--cluster` value passed through both
/// [`super::deploy::Deploy::run`] (the per-Servico `feira deploy`
/// per-cluster `<k8s-repo>/clusters/<cluster>/programs/release.yaml`
/// path-join at `cmd/deploy.rs:83`) and
/// [`super::app::DeployArgs::run`] (the whole-Aplicacao `feira app
/// deploy` per-cluster `<k8s-repo>/clusters/<cluster>/aplicacaos/<nome>/
/// manifests.yaml` path-join at `cmd/app.rs:156`) with no shape gate
/// at all — every footgun the author surface's `:placement :clusters`
/// slot already refuses through
/// [`caixa_core::AplicacaoError::PlacementClusterInvalid`] (the 6c8c00b
/// lift on the sibling typed-slot axis) silently passed at the verb
/// arg-entry. Two unprotected call-sites of the same load-bearing
/// shape, each one another place a future change to the canonical
/// cluster-name discipline has to remember to touch — the future M4
/// `feira reconcile --cluster <name>` cluster-diff verb the
/// absorption-roadmap acknowledges, the future per-cluster
/// `feira oci publish --cluster <name>` registry-push verb, the future
/// `feira app status --cluster <name>` introspection verb — each one
/// landing on a per-verb open-coded gate is a coordinated rewrite of
/// every verb-entry-point that either gets it right at every site or
/// quietly drifts at one.
///
/// Without the gate three authoring footguns silently passed:
///
///   - `--cluster ""` — the canonical "I forgot the flag value"
///     footgun. The path join landed at
///     `<k8s-repo>/clusters//programs/release.yaml`, which file-system-
///     normalizes to `<k8s-repo>/clusters/programs/release.yaml` and
///     either silently overwrote a sibling-dir file or surfaced a
///     non-existent-path error far from the source flag.
///   - `--cluster "rio/.."` — the path-traversal footgun. The path
///     join landed at `<k8s-repo>/clusters/rio/../programs/release.yaml`,
///     which file-system-normalizes to
///     `<k8s-repo>/clusters/programs/release.yaml` and overwrote the
///     sibling location with no diagnostic naming the escape.
///   - `--cluster "Rio"` / `--cluster "rio_cluster"` /
///     `--cluster "rio.cluster"` — the wrong-case / wrong-separator /
///     wrong-shape footguns the K8s apiserver refuses at admission
///     time on every `metadata.name` consumer downstream (the
///     `lareira-fleet-programs` HelmRelease's per-cluster
///     `name:`-keyed lookup, the per-Aplicacao Gateway/HTTPRoute's
///     `name:` field). Silent passage through the verb arg-entry
///     surfaced the failure at `kubectl apply` time as a "field is
///     invalid" rejection, far from the source `--cluster` flag.
///
/// After the lift every per-verb `--cluster` consumer routes through
/// this helper: the DNS-1123 label gate fires at the verb's entry-
/// point, the diagnostic carries the offending `--cluster <value>`
/// verbatim plus a parser-shaped reason naming the specific violation,
/// and the canonical "what shape must `--cluster` carry?" decision
/// lives at exactly one call-site. Mirrors the per-slot
/// [`caixa_core::AplicacaoError::PlacementClusterInvalid`] discipline
/// on the typed-Caixa author surface — the same DNS-1123 label shape
/// both the typed `:placement :clusters` slot and the per-verb
/// `--cluster` arg refuse drift against, sharing the lifted
/// [`caixa_core::is_dns_1123_label`] predicate so the substrate-wide
/// "valid cluster name" set is single-sourced.
pub(crate) fn validate_cluster_arg(cluster: &str) -> Result<()> {
    // Empty is gated first with a self-locating diagnostic — the
    // canonical "I forgot the flag value" footgun a bare
    // `--cluster ""` shape produces. Peer with the
    // [`caixa_core::AplicacaoError::PlacementClusterEmpty`] gate on
    // the typed-slot axis (the empty-first cascade every author-
    // surface DNS-1123 label slot follows on `:membros :caixa`,
    // `:placement :clusters`, `:children :caixa`, `:contratos :de` /
    // `:para`, `:entrada :para`).
    if cluster.is_empty() {
        bail!(
            "--cluster value is empty (every `feira` verb that joins \
             `<k8s-repo>/clusters/<cluster>/…` requires a non-empty \
             cluster name — e.g. `--cluster rio`, `--cluster mar`, \
             `--cluster plo`)"
        );
    }
    caixa_core::is_dns_1123_label(cluster).map_err(|reason| {
        anyhow::anyhow!(
            "--cluster {cluster:?} is not a valid DNS-1123 label: {reason} \
             (every cluster name lands as a path segment in \
             `<k8s-repo>/clusters/<cluster>/…` and as a K8s `metadata.name` \
             on downstream HelmRelease / Gateway / HTTPRoute consumers; the \
             apiserver rejects any non-DNS-1123-label name at admission time)"
        )
    })
}

/// Validate a `feira` verb's `--namespace <name>` flag value at the
/// per-verb entry-point against the canonical DNS-1123 label shape every
/// K8s Namespace `metadata.name` consumer demands.
///
/// Closes the PRIME DIRECTIVE duplication (THEORY.md §I.3.5 — "the
/// duplication budget is zero") on the per-verb `--namespace` axis,
/// peer with the [`validate_cluster_arg`] lift (feb3103) on the
/// sibling per-verb `--cluster` axis. Before this lift the same
/// `--namespace` value passed through fourteen unprotected
/// [`kube::Api::namespaced(client, &self.namespace)`] / per-CR
/// `metadata.namespace = Some(self.namespace)` call-sites across the
/// three cluster-touching verb families — `feira pool …`
/// (`PoolListArgs::run` / `PoolScaleArgs::run` / `PoolDeleteArgs::run` /
/// `PoolStatusArgs::run`, `cmd/pool.rs`), `feira ephemeral …`
/// (`GraphArgs::run` / `PlanArgs::run` / `UpArgs::run` /
/// `DownArgs::run` / `StatusArgs::run` / `ListArgs::run`,
/// `cmd/ephemeral.rs`), `feira allocation …` (`RequestArgs::run` /
/// `ReleaseArgs::run` / `ListArgs::run` / `StatusArgs::run`,
/// `cmd/allocation.rs`) — with no shape gate at all. Fourteen
/// unprotected call-sites of the same load-bearing shape, each one
/// another place a future change to the canonical namespace-name
/// discipline has to remember to touch — the future `feira pool
/// drain --namespace <ns>` per-pool reaper, the future `feira
/// allocation request --namespace <ns>` per-PR allocation verb the
/// absorption-roadmap acknowledges, the future `feira ephemeral
/// gc --namespace <ns>` cluster-wide ephemeral reaper — each one
/// landing on a per-verb open-coded gate is a coordinated rewrite of
/// every verb-entry-point that either gets it right at every site or
/// quietly drifts at one.
///
/// Without the gate three authoring footguns silently passed:
///
///   - `--namespace ""` — the canonical "I forgot the flag value"
///     footgun (the bare `--namespace=` flag on a CI invocation with
///     an unset shell variable). The K8s apiserver rejects empty
///     namespace values at admission time with a "field is required"
///     diagnostic far from the source flag; lifting the gate to the
///     verb entry-point surfaces the empty-value axis verbatim
///     ahead of the kube-rs round-trip.
///   - `--namespace "MyTeam"` / `--namespace "team_a"` /
///     `--namespace "team.a"` — the wrong-case / wrong-separator /
///     wrong-shape footguns the K8s apiserver refuses at admission
///     time on every namespaced `metadata.namespace` consumer
///     downstream (every EphemeralPool / EphemeralAllocation /
///     Process CR's apiserver-side admission rule). Silent passage
///     through the verb arg-entry surfaced the failure at kube-rs
///     RTT as a "Namespace MyTeam is invalid: metadata.namespace:
///     Invalid value" rejection, far from the source `--namespace`
///     flag.
///   - `--namespace "default/.."` — the path-traversal-shaped
///     footgun. The kube-rs Api builder URL-joins the namespace into
///     the API path (`/api/v1/namespaces/<ns>/…`); the bare `..`
///     segment either reaches the cluster-scope `/api/v1/<resource>`
///     accidentally on URL-normalize or fails kube-rs's per-request
///     `ApiResource` builder with no diagnostic naming the escape.
///     The DNS-1123 label gate refuses `/` and `.` outright with the
///     offending value named verbatim.
///
/// After the lift every per-verb `--namespace` consumer routes
/// through this helper: the DNS-1123 label gate fires at the verb's
/// entry-point, the diagnostic carries the offending `--namespace
/// <value>` verbatim plus a parser-shaped reason naming the specific
/// violation, and the canonical "what shape must `--namespace`
/// carry?" decision lives at exactly one call-site. Mirrors
/// [`validate_cluster_arg`] on the peer per-verb-arg axis — together
/// the two helpers form the canonical DNS-1123-label CLI-arg surface
/// every per-verb `metadata.name` / `metadata.namespace` entry-point
/// composes from, sharing the lifted [`caixa_core::is_dns_1123_label`]
/// predicate so the substrate-wide "valid K8s label-shaped CLI arg"
/// set is single-sourced across the typed-slot and CLI-arg surfaces.
///
/// The `"default"` literal — the canonical fallback every
/// `--namespace`-taking verb's clap `default_value = "default"`
/// attribute carries — passes the DNS-1123 label gate cleanly (six
/// lowercase letters), so a verb invoked without the flag inherits
/// the validated canonical default through this helper without
/// special-casing the absent-flag arm.
pub(crate) fn validate_namespace_arg(namespace: &str) -> Result<()> {
    // Empty is gated first with a self-locating diagnostic — the
    // canonical "I forgot the flag value" footgun a bare
    // `--namespace=` shape produces (a CI invocation with an unset
    // shell variable interpolated into the flag). Peer with the
    // [`validate_cluster_arg`] empty-arm on the sibling per-verb
    // axis.
    if namespace.is_empty() {
        bail!(
            "--namespace value is empty (every `feira` verb that scopes a \
             `kube::Api` to a namespaced resource requires a non-empty \
             namespace name — e.g. `--namespace default`, `--namespace \
             pleme-system`, or omit the flag to inherit the canonical \
             `\"default\"` fallback)"
        );
    }
    caixa_core::is_dns_1123_label(namespace).map_err(|reason| {
        anyhow::anyhow!(
            "--namespace {namespace:?} is not a valid DNS-1123 label: {reason} \
             (every K8s Namespace `metadata.name` is bounded by the DNS-1123 \
             label grammar; the apiserver rejects any non-DNS-1123-label name \
             at admission time on every namespaced CR `metadata.namespace` \
             axis downstream — EphemeralPool, EphemeralAllocation, Process)"
        )
    })
}

/// Validate a `feira` verb's positional `<nome>` argument value at the
/// per-verb entry-point against the canonical DNS-1123 label shape every
/// downstream consumer (the typed [`caixa_core::Caixa::nome`] /
/// [`caixa_core::Dep::nome`] axes, the rendered
/// `lareira-<nome>` Helm chart name, every K8s `metadata.name` materializer
/// the substrate emits) demands.
///
/// Closes the PRIME DIRECTIVE duplication (THEORY.md §I.3.5 — "the
/// duplication budget is zero") on the per-verb `<nome>` positional axis,
/// peer with the [`validate_cluster_arg`] / [`validate_namespace_arg`]
/// lifts on the sibling `--cluster` / `--namespace` flag axes. Before
/// this lift the same `<nome>` value passed through both
/// [`super::init::Init::run`] (the per-caixa scaffold's
/// `<nome>/lib/<nome>.lisp` path join) and [`super::add::Add::run`] (the
/// per-dep `:nome` slot landing in the rendered `caixa.lisp`) with no
/// shape gate at all — every footgun the author surface's `:nome` /
/// `:deps :nome` typed slots already refuse through
/// [`caixa_core::ManifestError::NomeInvalid`] (the 6c992f8 lift on the
/// top-level `:nome` typed axis) and
/// [`caixa_core::DepError::NomeInvalid`] (the 2420c44-trajectory lift on
/// the per-dep `:nome` axis) silently passed at the verb arg-entry. Two
/// unprotected call-sites of the same load-bearing shape, each one
/// another place a future change to the canonical caixa-name discipline
/// has to remember to touch — a future `feira fork <nome>` verb that
/// scaffolds a derivative caixa, a future `feira rename <nome>` verb
/// that rewrites the manifest's `:nome` slot, a future `feira oci pull
/// <nome>` verb that fetches a caixa by name from the future OCI
/// registry — each one landing on a per-verb open-coded gate is a
/// coordinated rewrite of every verb-entry-point that either gets it
/// right at every site or quietly drifts at one.
///
/// Without the gate four authoring footguns silently passed:
///
///   - `feira init ""` / `feira add ""` — the canonical "I forgot the
///     positional arg" footgun (a CI invocation with an unset shell
///     variable interpolated into the positional). On `feira init` the
///     bare empty `<nome>` joined to `./` as the target dir (the current
///     working directory) and to `./lib/.lisp` as the lisp entry file;
///     the scaffolded `caixa.lisp` carried `:nome ""` which the
///     downstream `caixa_core::Caixa::validate_nome` arm rejected at
///     `feira build` time, far from the source `feira init` invocation,
///     with the bad-shape diagnostic naming `:nome` rather than the
///     `<nome>` positional arg the author typed. On `feira add` the
///     bare empty `<nome>` landed verbatim as `:deps :nome ""` and the
///     same downstream cascade fired — both verbs surface the failure
///     after the manifest has already been mutated on disk, rather than
///     before any I/O is observable.
///   - `feira init "../escape"` / `feira init "lib/../escape"` — the
///     canonical path-traversal footgun. On `feira init` the bare
///     `<nome>` arg is used verbatim as the target directory
///     (`PathBuf::from(&nome)`) and as the lisp filename inside the
///     scaffolded `lib/<nome>.lisp` path; the bare `..` segment either
///     creates a sibling directory the operator didn't intend, or
///     scaffolds a `lib/../escape.lisp` whose `caixa.lisp` references a
///     library file at the parent of `lib/` — escaping the target dir
///     before any other I/O is observable. The DNS-1123 label gate
///     refuses `/`, `.`, and the bare `..` outright with the offending
///     value named verbatim, closing the canonical "I pasted a relative
///     path into the `<nome>` slot" footgun ahead of any directory
///     creation.
///   - `feira init "MyCaixa"` / `feira add "Caixa-Teia"` — the
///     wrong-case footgun (the canonical "I copied the README header"
///     typo). The K8s `metadata.name` axes downstream
///     (`lareira-<nome>` Chart.yaml `name:`, the `HelmRelease`
///     `release_name`, every per-Servico / per-Aplicacao CR
///     `metadata.name`) all enforce the DNS-1123-label rule at admission
///     time on a lowercase floor; silent passage through the verb
///     arg-entry surfaced the failure at `feira chart` /
///     `feira deploy` / `kubectl apply` time as a "field is invalid"
///     rejection, far from the source positional arg. The lifted
///     [`caixa_core::is_dns_1123_label`] predicate carries a
///     canonical-lowercase remediation hint in its reason wording ("use
///     {lower:?}") so the diagnostic names the canonical fix verbatim.
///   - `feira init "caixa_teia"` / `feira add "my_dep"` — the
///     wrong-separator footgun (the Python / Go module-name leak — both
///     use underscores; K8s `metadata.name` consumers accept only
///     hyphens). The lifted predicate's reason wording names `-` as the
///     canonical separator so the diagnostic surfaces the fix verbatim.
///
/// After the lift every per-verb `<nome>` positional consumer routes
/// through this helper: the DNS-1123 label gate fires at the verb's
/// entry-point before any directory creation / manifest mutation, the
/// diagnostic carries the offending `<nome>` verbatim plus a parser-
/// shaped reason naming the specific violation, and the canonical "what
/// shape must `<nome>` carry?" decision lives at exactly one call-site.
/// Mirrors the per-slot
/// [`caixa_core::ManifestError::NomeInvalid`] /
/// [`caixa_core::DepError::NomeInvalid`] discipline on the typed-Caixa
/// author surface — the same DNS-1123 label shape both the typed
/// `:nome` / `:deps :nome` slots and the per-verb `<nome>` positional
/// arg refuse drift against, sharing the lifted
/// [`caixa_core::is_dns_1123_label`] predicate so the substrate-wide
/// "valid caixa name" set is single-sourced across the typed-slot and
/// CLI-arg surfaces.
pub(crate) fn validate_nome_arg(nome: &str) -> Result<()> {
    // Empty is gated first with a self-locating diagnostic — the
    // canonical "I forgot the positional arg" footgun a bare
    // `feira init ""` / `feira add ""` shape produces (a CI invocation
    // with an unset shell variable interpolated into the positional).
    // Peer with the [`validate_cluster_arg`] / [`validate_namespace_arg`]
    // empty-arms on the sibling per-verb flag axes and with the
    // [`caixa_core::ManifestError::NomeEmpty`] /
    // [`caixa_core::DepError::NomeEmpty`] arms on the typed-slot axes.
    if nome.is_empty() {
        bail!(
            "<nome> value is empty (every `feira` verb that consumes a positional \
             caixa-name argument requires a non-empty DNS-1123 label — e.g. \
             `feira init hello-rio`, `feira add caixa-teia`)"
        );
    }
    caixa_core::is_dns_1123_label(nome).map_err(|reason| {
        anyhow::anyhow!(
            "<nome> {nome:?} is not a valid DNS-1123 label: {reason} \
             (a caixa's `:nome` lands as the `lareira-<nome>` K8s `metadata.name` \
             on every downstream chart/release/CR consumer; the apiserver rejects \
             any non-DNS-1123-label name at admission time, and `feira init` joins \
             the value verbatim into the `<nome>/lib/<nome>.lisp` scaffold path so \
             a path-traversal shape escapes the target dir before any other I/O is \
             observable)"
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use caixa_core::CaixaKind;
    use tempfile::tempdir;

    #[test]
    fn caixa_manifest_path_resolves_root_join_canonical_filename() {
        // The shape every per-verb call-site relied on (verbatim
        // `root.join("caixa.lisp")`) is now a single canonical
        // resolver. Pins that the path lands at `<root>/caixa.lisp`
        // and that the canonical manifest-filename constant stays
        // load-bearing for any future `feira` verb whose entry-point
        // joins the manifest by path.
        let root = PathBuf::from("/tmp/some-caixa");
        let p = caixa_manifest_path(&root);
        assert!(p.starts_with(&root), "manifest path must live under root");
        assert!(
            p.ends_with(CAIXA_MANIFEST_FILENAME),
            "manifest path must end with the canonical filename"
        );
        assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("caixa.lisp"));
    }

    #[test]
    fn load_caixa_accepts_well_formed_manifest() {
        // The canonical happy-path — a well-formed `caixa.lisp` at
        // `<root>/caixa.lisp` loads cleanly and the typed Caixa
        // round-trips its `:nome` + `:kind` slots. Peer with every
        // `feira` verb's `Run::run` entry-point: each one used to
        // open-code this four-line load before the lift, and now
        // routes through the canonical helper.
        let dir = tempdir().expect("tempdir");
        std::fs::write(
            dir.path().join(CAIXA_MANIFEST_FILENAME),
            r#"(defcaixa
                 :nome "hello"
                 :kind Biblioteca
                 :versao "0.1.0"
                 :bibliotecas ())"#,
        )
        .expect("write manifest");
        let caixa = load_caixa(dir.path()).expect("well-formed manifest must load");
        assert_eq!(caixa.nome, "hello");
        assert_eq!(caixa.kind, CaixaKind::Biblioteca);
        assert_eq!(caixa.versao, "0.1.0");
    }

    #[test]
    fn load_caixa_missing_manifest_diagnostic_names_path_with_reading_context() {
        // The diagnostic shape every per-verb consumer pins: the
        // `.with_context(|| format!("reading {}", manifest.display()))`
        // wrap renders the verbatim `"reading <path>"` string with the
        // resolved `caixa.lisp` path on missing-file failures. Before
        // this lift the same string appeared verbatim at every verb's
        // call-site — a future refactor of the helper can't silently
        // regress to a different context-string shape without this
        // pin firing.
        let dir = tempdir().expect("tempdir");
        let err = load_caixa(dir.path()).expect_err("missing manifest must error");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("reading"),
            "diagnostic must name the reading axis (got: {rendered:?})"
        );
        assert!(
            rendered.contains("caixa.lisp"),
            "diagnostic must name the canonical manifest filename (got: {rendered:?})"
        );
    }

    #[test]
    fn load_caixa_parse_error_diagnostic_names_path_with_parsing_context() {
        // The peer arm on the parse-failure axis: a malformed manifest
        // surfaces a diagnostic whose `.with_context` wrap names the
        // `"parsing <path>"` axis verbatim. Pins the canonical
        // context-string discipline on the parse-failure path so a
        // future refactor can't silently regress it independent of
        // the read-failure path.
        let dir = tempdir().expect("tempdir");
        std::fs::write(
            dir.path().join(CAIXA_MANIFEST_FILENAME),
            "this is not a defcaixa form at all",
        )
        .expect("write malformed manifest");
        let err = load_caixa(dir.path()).expect_err("malformed manifest must error");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("parsing"),
            "diagnostic must name the parsing axis (got: {rendered:?})"
        );
        assert!(
            rendered.contains("caixa.lisp"),
            "diagnostic must name the canonical manifest filename (got: {rendered:?})"
        );
    }

    #[test]
    fn load_yaml_accepts_well_formed_document() {
        // The canonical happy-path — a well-formed YAML document loads
        // cleanly and the parsed [`serde_yaml::Value`] round-trips its
        // canonical top-level scalar. Peer with every per-verb
        // YAML-consuming entry-point: the per-Servico computeunit
        // loader (`super::chart::load_first_servico_yaml`) and the
        // per-cluster fleet-programs HelmRelease loader (`feira
        // deploy`'s `Run::run`) used to open-code the same four-line
        // read+parse shape before this lift, and now route through
        // this canonical helper.
        let dir = tempdir().expect("tempdir");
        let p = dir.path().join("hello.yaml");
        std::fs::write(&p, "name: hello\nport: 8080\n").expect("write yaml");
        let value = load_yaml(&p).expect("well-formed yaml must load");
        assert_eq!(
            value.get("name").and_then(serde_yaml::Value::as_str),
            Some("hello"),
            "parsed yaml must round-trip the canonical top-level scalar"
        );
    }

    #[test]
    fn load_yaml_missing_file_diagnostic_names_path_with_reading_context() {
        // The diagnostic shape every per-verb consumer pins: the
        // `.with_context(|| format!("reading {}", path.display()))`
        // wrap renders the verbatim `"reading <path>"` string with
        // the resolved path on missing-file failures. Before this
        // lift the same string appeared verbatim at every per-verb
        // YAML call-site — a future refactor of the helper can't
        // silently regress to a different context-string shape
        // without this pin firing. Peer with the
        // `load_caixa_missing_manifest_diagnostic_names_path_with_
        // reading_context` pin on the sibling manifest-IO axis.
        let dir = tempdir().expect("tempdir");
        let p = dir.path().join("absent.yaml");
        let err = load_yaml(&p).expect_err("missing yaml must error");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("reading"),
            "diagnostic must name the reading axis (got: {rendered:?})"
        );
        assert!(
            rendered.contains("absent.yaml"),
            "diagnostic must name the offending path (got: {rendered:?})"
        );
    }

    #[test]
    fn load_yaml_parse_error_diagnostic_names_path_with_parsing_context() {
        // The peer arm on the parse-failure axis: a malformed YAML
        // surfaces a diagnostic whose `.with_context` wrap names the
        // `"parsing <path>"` axis verbatim. Pins the canonical
        // context-string discipline on the parse-failure path so a
        // future refactor can't silently regress it independent of
        // the read-failure path. Peer with the
        // `load_caixa_parse_error_diagnostic_names_path_with_parsing_
        // context` pin on the sibling manifest-IO axis.
        let dir = tempdir().expect("tempdir");
        let p = dir.path().join("torn.yaml");
        std::fs::write(&p, "name: torn\n  bad-indent: [unclosed\n").expect("write malformed yaml");
        let err = load_yaml(&p).expect_err("malformed yaml must error");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("parsing"),
            "diagnostic must name the parsing axis (got: {rendered:?})"
        );
        assert!(
            rendered.contains("torn.yaml"),
            "diagnostic must name the offending path (got: {rendered:?})"
        );
    }

    #[test]
    fn load_yaml_parse_error_preserves_underlying_serde_yaml_error_on_chain() {
        // Peer to the parse-context pin above: the anyhow
        // `.with_context(...)` wrap preserves the underlying
        // `serde_yaml::Error` on the cause chain — a future
        // structured-output mode (`feira chart --json`, the future
        // `feira validate --strict` per-caixa admission verb, the M4
        // `feira reconcile --json` cluster-diff verb) can downcast
        // through the chain and read the typed payload directly, peer
        // with the per-renderer [`caixa_core::KindMismatch`] /
        // [`caixa_core::ServicoCountMismatch`] typed-view downcast
        // discipline and with the
        // `load_caixa_parse_error_preserves_underlying_lisp_error_on_
        // chain` pin on the sibling manifest-IO axis.
        let dir = tempdir().expect("tempdir");
        let p = dir.path().join("torn.yaml");
        std::fs::write(&p, "name: torn\n  bad-indent: [unclosed\n").expect("write malformed yaml");
        let err = load_yaml(&p).expect_err("malformed yaml must error");
        let typed_reachable = err
            .chain()
            .any(|e| e.downcast_ref::<serde_yaml::Error>().is_some());
        assert!(
            typed_reachable,
            "underlying serde_yaml::Error must remain reachable on the anyhow chain"
        );
    }

    #[test]
    fn caixa_root_with_none_resolves_to_canonical_cwd_dirname() {
        // The canonical fallback arm — an absent `--path` flag
        // resolves to the canonical CWD-default dirname literal
        // ([`CAIXA_ROOT_DEFAULT_DIRNAME`]). Pins the load-bearing
        // fallback shape every per-verb call-site relied on before
        // this lift (`self.path.clone().unwrap_or_else(||
        // PathBuf::from("."))`); a future refactor of the helper
        // can't silently regress to a different fallback (an
        // absolute-path default, a CWD-via-env-var default, a
        // panic-on-absent) without this pin firing.
        let p = caixa_root(None);
        assert_eq!(
            p,
            PathBuf::from(CAIXA_ROOT_DEFAULT_DIRNAME),
            "absent --path must fall back to the canonical CWD-default dirname literal"
        );
        assert_eq!(
            p.as_os_str(),
            std::ffi::OsStr::new("."),
            "the canonical CWD-default dirname literal must remain `.` verbatim"
        );
    }

    #[test]
    fn caixa_root_with_some_returns_caller_path_clone() {
        // The peer arm — when `--path` is present the resolver
        // returns a clone of the borrowed [`Path`], not the
        // canonical fallback. Pins that a caller-provided
        // `--path /some/absolute/caixa` (the common CI shape) round-
        // trips through the helper unchanged — peer with the
        // `:nome` round-trip pin on `load_caixa_accepts_well_formed_
        // manifest`.
        let caller = PathBuf::from("/tmp/some/explicit/caixa-root");
        let resolved = caixa_root(Some(&caller));
        assert_eq!(
            resolved, caller,
            "explicit --path must pass through the resolver unchanged"
        );
    }

    #[test]
    fn caixa_root_with_some_relative_path_preserves_shape() {
        // Defensive coverage: relative `--path` shapes (the common
        // shell-invocation form, e.g. `feira chart --path
        // ./subcaixa`) round-trip the resolver verbatim — a future
        // refactor can't silently canonicalize or absolutize the
        // path without this pin firing. The canonical layout
        // discipline (`StandardLayout::verify`) joins this root with
        // `:bibliotecas` / `:exe` / `:servicos` entries downstream,
        // so any silent canonicalization would break the
        // per-caixa-root layout invariant pins peer-with
        // `caixa_manifest_path_resolves_root_join_canonical_filename`
        // on the manifest axis.
        let rel = PathBuf::from("./subcaixa");
        let resolved = caixa_root(Some(&rel));
        assert_eq!(
            resolved, rel,
            "relative --path must pass through the resolver verbatim, no canonicalize"
        );
    }

    #[test]
    fn caixa_root_canonical_dirname_constant_pins_dot_literal() {
        // The canonical-constant arm — pins
        // [`CAIXA_ROOT_DEFAULT_DIRNAME`] at the verbatim `"."`
        // literal. Peer with the
        // [`CAIXA_MANIFEST_FILENAME`]-pins-`"caixa.lisp"` discipline
        // on the manifest-filename axis: a future refactor of the
        // helper can't silently regress the canonical fallback
        // dirname to a different literal (`""`, `"./"`,
        // `std::env::current_dir().unwrap()`) independent of the
        // per-verb call-sites that already route through it.
        assert_eq!(
            CAIXA_ROOT_DEFAULT_DIRNAME, ".",
            "canonical CWD-default dirname literal must remain `.` verbatim"
        );
    }

    #[test]
    fn validate_cluster_arg_accepts_canonical_dns_1123_label_names() {
        // The canonical happy-path arm — the three canonical pleme-io
        // cluster names (`rio`, `mar`, `plo`) the `feira deploy` /
        // `feira app deploy` help text already documents pass the lifted
        // DNS-1123 label gate cleanly. Peer with every per-slot
        // DNS-1123-label call-site's happy-path pin
        // (`validate_placement_cluster` on the typed `:placement
        // :clusters` slot, `validate_membro_caixa` on `:membros :caixa`,
        // `validate_child_caixa` on `:children :caixa`).
        for cluster in ["rio", "mar", "plo", "us-east-1", "eu-west-2", "a", "a0"] {
            validate_cluster_arg(cluster)
                .unwrap_or_else(|_| panic!("canonical cluster name {cluster:?} must accept"));
        }
    }

    #[test]
    fn validate_cluster_arg_rejects_empty_with_self_locating_diagnostic() {
        // The canonical "I forgot the flag value" footgun a bare
        // `--cluster ""` shape produces — silently passing the empty
        // string would join to `<k8s-repo>/clusters//programs/release.yaml`,
        // which file-system-normalizes to a sibling-dir path. The gate
        // refuses with a self-locating diagnostic naming the `--cluster`
        // flag and the canonical cluster-name shape, peer with the
        // [`caixa_core::AplicacaoError::PlacementClusterEmpty`] arm on
        // the typed-slot axis.
        let err = validate_cluster_arg("").expect_err("empty --cluster must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("--cluster"),
            "diagnostic must name the offending flag (got: {rendered:?})"
        );
        assert!(
            rendered.contains("empty"),
            "diagnostic must name the empty-value axis (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_cluster_arg_rejects_path_traversal_with_named_value() {
        // The path-traversal footgun: `--cluster "rio/.."` joined at
        // `<k8s-repo>/clusters/rio/../programs/release.yaml` normalizes
        // to a sibling-dir overwrite with no diagnostic naming the
        // escape. The DNS-1123 label gate refuses `/` and `.` outright,
        // so the parent-escape attempt surfaces as a
        // "contains `/` (DNS-1123 labels allow only `[a-z0-9-]`)" or
        // "contains `.` (a single DNS-1123 label is not a subdomain)"
        // shape — peer with the `validate_placement_cluster` rejection
        // on the typed-slot axis. The diagnostic names the offending
        // `--cluster` value verbatim so the operator can grep their
        // shell history for the bad flag.
        for bad in ["rio/..", "../rio", "rio/sub", "."] {
            let err = match validate_cluster_arg(bad) {
                Err(e) => e,
                Ok(()) => panic!("path-traversal {bad:?} must reject"),
            };
            let rendered = format!("{err:?}");
            assert!(
                rendered.contains("--cluster"),
                "diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
            );
            assert!(
                rendered.contains(bad),
                "diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
            );
        }
    }

    #[test]
    fn validate_cluster_arg_rejects_uppercase_with_named_value_and_canonical_lowercase_hint() {
        // The wrong-case footgun: `--cluster "Rio"` lands as a K8s
        // `metadata.name` on the downstream `lareira-fleet-programs`
        // HelmRelease's per-cluster `name:`-keyed lookup, which the
        // apiserver rejects at admission time with a "field is invalid"
        // diagnostic far from the source flag. The lifted
        // `is_dns_1123_label` predicate carries a canonical-lowercase
        // hint in its reason wording ("use {lower:?}") so the
        // diagnostic names the canonical fix verbatim — peer with the
        // `validate_placement_cluster` rejection on the typed-slot
        // axis. Pins both the offending value and the canonical
        // lowercase remediation in the rendered diagnostic.
        let err = validate_cluster_arg("Rio").expect_err("uppercase --cluster must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("--cluster"),
            "diagnostic must name the offending flag (got: {rendered:?})"
        );
        assert!(
            rendered.contains("Rio"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
        assert!(
            rendered.contains("rio"),
            "diagnostic must surface the canonical lowercase remediation (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_cluster_arg_rejects_underscore_with_named_value_and_hyphen_hint() {
        // The wrong-separator footgun: `--cluster "rio_cluster"` is a
        // common author error (the K8s `metadata.name` axis allows
        // hyphen-separated tokens, not snake_case). The lifted
        // `is_dns_1123_label` predicate's reason wording names `-` as
        // the canonical separator ("use `-` instead") so the
        // diagnostic surfaces the fix verbatim — peer with the
        // `validate_placement_cluster` rejection on the typed-slot
        // axis.
        let err =
            validate_cluster_arg("rio_cluster").expect_err("underscore --cluster must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("--cluster"),
            "diagnostic must name the offending flag (got: {rendered:?})"
        );
        assert!(
            rendered.contains("rio_cluster"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
        assert!(
            rendered.contains('`'),
            "diagnostic must surface the hyphen-separator remediation (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_cluster_arg_rejects_leading_hyphen_with_named_value() {
        // The boundary-char footgun: `--cluster "-rio"` violates the
        // DNS-1123 label rule that names must start and end with an
        // alphanumeric (the K8s apiserver rejects leading / trailing
        // `-` outright at admission time). The lifted predicate's
        // reason wording names the boundary rule verbatim — peer with
        // the `validate_placement_cluster` rejection on the typed-slot
        // axis.
        for bad in ["-rio", "rio-"] {
            let err = match validate_cluster_arg(bad) {
                Err(e) => e,
                Ok(()) => panic!("boundary-`-` {bad:?} must reject"),
            };
            let rendered = format!("{err:?}");
            assert!(
                rendered.contains("--cluster"),
                "diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
            );
            assert!(
                rendered.contains(bad),
                "diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
            );
        }
    }

    #[test]
    fn validate_namespace_arg_accepts_canonical_dns_1123_label_names() {
        // The canonical happy-path arm — the K8s-canonical
        // `"default"` namespace literal every `--namespace`-taking
        // verb's `default_value = "default"` clap attribute carries
        // passes the lifted DNS-1123 label gate cleanly, alongside
        // the realistic operator-side shapes (`kube-system`,
        // `pleme-system`, `cert-manager`, `argocd`) and DNS-1123 edge
        // shapes (`a`, `a0`). Peer with the
        // [`validate_cluster_arg`] happy-path pin on the sibling
        // per-verb axis — the same DNS-1123 label discipline both
        // helpers gate against, single-sourced through the lifted
        // [`caixa_core::is_dns_1123_label`] predicate.
        for namespace in [
            "default",
            "kube-system",
            "pleme-system",
            "cert-manager",
            "argocd",
            "a",
            "a0",
        ] {
            validate_namespace_arg(namespace)
                .unwrap_or_else(|_| panic!("canonical namespace {namespace:?} must accept"));
        }
    }

    #[test]
    fn validate_namespace_arg_rejects_empty_with_self_locating_diagnostic() {
        // The canonical "I forgot the flag value" footgun a bare
        // `--namespace=` shape produces (a CI invocation with an
        // unset shell variable interpolated into the flag). Silently
        // passing the empty string would surface at the kube-rs API
        // round-trip as a "Namespace is required" rejection far
        // from the source flag. The gate refuses with a self-
        // locating diagnostic naming the `--namespace` flag and the
        // empty-value axis, peer with the
        // [`validate_cluster_arg`] empty-arm.
        let err = validate_namespace_arg("").expect_err("empty --namespace must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("--namespace"),
            "diagnostic must name the offending flag (got: {rendered:?})"
        );
        assert!(
            rendered.contains("empty"),
            "diagnostic must name the empty-value axis (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_namespace_arg_rejects_path_traversal_with_named_value() {
        // The path-traversal-shaped footgun: `--namespace
        // "default/.."` URL-joined into the kube-rs API path
        // (`/api/v1/namespaces/default/../<resource>`) either
        // reaches the cluster-scope `/api/v1/<resource>` on
        // URL-normalize or surfaces a per-request `ApiResource`
        // builder error with no diagnostic naming the escape. The
        // DNS-1123 label gate refuses `/` and `.` outright, so the
        // parent-escape attempt surfaces with the offending value
        // named verbatim — peer with the [`validate_cluster_arg`]
        // path-traversal arm on the sibling per-verb axis.
        for bad in ["default/..", "../default", "default/sub", "."] {
            let err = match validate_namespace_arg(bad) {
                Err(e) => e,
                Ok(()) => panic!("path-traversal {bad:?} must reject"),
            };
            let rendered = format!("{err:?}");
            assert!(
                rendered.contains("--namespace"),
                "diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
            );
            assert!(
                rendered.contains(bad),
                "diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
            );
        }
    }

    #[test]
    fn validate_namespace_arg_rejects_uppercase_with_named_value_and_canonical_lowercase_hint() {
        // The wrong-case footgun: `--namespace "MyTeam"` lands as
        // the K8s `metadata.namespace` axis on every namespaced CR
        // (EphemeralPool / EphemeralAllocation / Process) the kube-rs
        // round-trip targets, which the apiserver rejects at
        // admission time with a "Namespace MyTeam is invalid" /
        // "metadata.namespace: Invalid value" diagnostic far from
        // the source flag. The lifted `is_dns_1123_label` predicate
        // carries a canonical-lowercase hint in its reason wording
        // ("use {lower:?}") so the diagnostic names the canonical
        // fix verbatim — peer with the
        // [`validate_cluster_arg`] uppercase arm.
        let err = validate_namespace_arg("MyTeam").expect_err("uppercase --namespace must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("--namespace"),
            "diagnostic must name the offending flag (got: {rendered:?})"
        );
        assert!(
            rendered.contains("MyTeam"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
        assert!(
            rendered.contains("myteam"),
            "diagnostic must surface the canonical lowercase remediation (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_namespace_arg_rejects_underscore_with_named_value_and_hyphen_hint() {
        // The wrong-separator footgun: `--namespace "team_a"` is a
        // common author error (the K8s `metadata.namespace` axis
        // allows hyphen-separated tokens, not snake_case). The lifted
        // `is_dns_1123_label` predicate's reason wording names `-` as
        // the canonical separator ("use `-` instead") so the
        // diagnostic surfaces the fix verbatim — peer with the
        // [`validate_cluster_arg`] underscore arm.
        let err = validate_namespace_arg("team_a").expect_err("underscore --namespace must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("--namespace"),
            "diagnostic must name the offending flag (got: {rendered:?})"
        );
        assert!(
            rendered.contains("team_a"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
        assert!(
            rendered.contains('`'),
            "diagnostic must surface the hyphen-separator remediation (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_namespace_arg_rejects_leading_hyphen_with_named_value() {
        // The boundary-char footgun: `--namespace "-default"`
        // violates the DNS-1123 label rule that names must start and
        // end with an alphanumeric (the K8s apiserver rejects
        // leading / trailing `-` outright at admission time). The
        // lifted predicate's reason wording names the boundary rule
        // verbatim — peer with the [`validate_cluster_arg`]
        // boundary-hyphen arm.
        for bad in ["-default", "default-"] {
            let err = match validate_namespace_arg(bad) {
                Err(e) => e,
                Ok(()) => panic!("boundary-`-` {bad:?} must reject"),
            };
            let rendered = format!("{err:?}");
            assert!(
                rendered.contains("--namespace"),
                "diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
            );
            assert!(
                rendered.contains(bad),
                "diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
            );
        }
    }

    #[test]
    fn validate_namespace_arg_accepts_canonical_default_fallback_literal() {
        // Pins that the canonical fallback literal every
        // `--namespace`-taking verb's clap `default_value =
        // "default"` attribute carries (six lowercase letters)
        // passes the gate cleanly. A future refactor of either the
        // clap default or the gate that drifts the two out of
        // alignment — say, lowercasing the gate's reject set or
        // renaming the canonical fallback — surfaces as a test
        // regression here, ahead of every per-verb's first
        // namespace-less invocation. Peer with the [`load_caixa`]
        // parse-context pin on the sibling per-verb IO axis.
        validate_namespace_arg("default").expect("canonical default literal must accept");
    }

    #[test]
    fn validate_nome_arg_accepts_canonical_dns_1123_label_names() {
        // The canonical happy-path arm — the in-tree fixture
        // names (`hello-rio`, `caixa-teia`, `iac-forge-ir`),
        // every example caixa name (`checkout`, `cart`,
        // `catalog`, `payment`), and the DNS-1123 edge shapes
        // (`a`, `a0`) all pass the lifted gate cleanly. Peer
        // with [`validate_cluster_arg`] /
        // [`validate_namespace_arg`] happy-path pins on the
        // sibling per-verb axes — the same DNS-1123 label
        // discipline all three helpers gate against,
        // single-sourced through the lifted
        // [`caixa_core::is_dns_1123_label`] predicate.
        for nome in [
            "hello-rio",
            "caixa-teia",
            "iac-forge-ir",
            "checkout",
            "cart",
            "catalog",
            "payment",
            "a",
            "a0",
        ] {
            validate_nome_arg(nome)
                .unwrap_or_else(|_| panic!("canonical caixa name {nome:?} must accept"));
        }
    }

    #[test]
    fn validate_nome_arg_rejects_empty_with_self_locating_diagnostic() {
        // The canonical "I forgot the positional arg" footgun a
        // bare `feira init ""` / `feira add ""` shape produces
        // (a CI invocation with an unset shell variable
        // interpolated into the positional). Silently passing
        // the empty string would scaffold `./` as the target
        // dir and `./lib/.lisp` as the lisp entry on
        // `feira init`, and would land `:deps :nome ""` in the
        // manifest on `feira add` — both surface their failure
        // far from the source positional. The gate refuses
        // with a self-locating diagnostic naming the `<nome>`
        // positional and the empty-value axis, peer with the
        // [`validate_cluster_arg`] / [`validate_namespace_arg`]
        // empty-arms on the sibling per-verb flag axes.
        let err = validate_nome_arg("").expect_err("empty <nome> must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("<nome>"),
            "diagnostic must name the offending positional (got: {rendered:?})"
        );
        assert!(
            rendered.contains("empty"),
            "diagnostic must name the empty-value axis (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_nome_arg_rejects_path_traversal_with_named_value() {
        // The canonical path-traversal footgun: `feira init
        // "../escape"` would use the bare `<nome>` arg verbatim
        // as the target dir (`PathBuf::from(&nome)`) and as
        // the lisp filename inside `lib/<nome>.lisp` — the
        // bare `..` segment escapes the target dir before any
        // other I/O is observable. The DNS-1123 label gate
        // refuses `/`, `.`, and the bare `..` outright, so the
        // parent-escape attempt surfaces with the offending
        // value named verbatim — peer with the
        // [`validate_cluster_arg`] / [`validate_namespace_arg`]
        // path-traversal arms on the sibling per-verb axes.
        for bad in ["../escape", "lib/../escape", "foo/bar", ".", ".."] {
            let err = match validate_nome_arg(bad) {
                Err(e) => e,
                Ok(()) => panic!("path-traversal {bad:?} must reject"),
            };
            let rendered = format!("{err:?}");
            assert!(
                rendered.contains("<nome>"),
                "diagnostic must name the offending positional for {bad:?} (got: {rendered:?})"
            );
            assert!(
                rendered.contains(bad),
                "diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
            );
        }
    }

    #[test]
    fn validate_nome_arg_rejects_uppercase_with_named_value_and_canonical_lowercase_hint() {
        // The wrong-case footgun: `feira init "MyCaixa"` /
        // `feira add "Caixa-Teia"` (the canonical "I copied
        // the README header" typo). The K8s `metadata.name`
        // axes downstream (`lareira-<nome>` Chart.yaml `name:`,
        // every per-Servico / per-Aplicacao CR `metadata.name`)
        // all enforce the DNS-1123-label rule at admission
        // time on a lowercase floor; silent passage through
        // the verb arg-entry surfaced the failure at
        // `feira chart` / `feira deploy` / `kubectl apply`
        // time as a "field is invalid" rejection, far from
        // the source positional. The lifted
        // [`caixa_core::is_dns_1123_label`] predicate carries
        // a canonical-lowercase remediation hint in its reason
        // wording — peer with the
        // [`validate_cluster_arg`] uppercase arm.
        let err = validate_nome_arg("MyCaixa").expect_err("uppercase <nome> must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("<nome>"),
            "diagnostic must name the offending positional (got: {rendered:?})"
        );
        assert!(
            rendered.contains("MyCaixa"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
        assert!(
            rendered.contains("mycaixa"),
            "diagnostic must surface the canonical lowercase remediation (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_nome_arg_rejects_underscore_with_named_value_and_hyphen_hint() {
        // The wrong-separator footgun: `feira init
        // "caixa_teia"` / `feira add "my_dep"` (the Python /
        // Go module-name leak — both use underscores; K8s
        // `metadata.name` consumers accept only hyphens). The
        // lifted predicate's reason wording names `-` as the
        // canonical separator so the diagnostic surfaces the
        // fix verbatim — peer with the [`validate_cluster_arg`]
        // underscore arm.
        let err = validate_nome_arg("caixa_teia").expect_err("underscore <nome> must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("<nome>"),
            "diagnostic must name the offending positional (got: {rendered:?})"
        );
        assert!(
            rendered.contains("caixa_teia"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
        assert!(
            rendered.contains('`'),
            "diagnostic must surface the hyphen-separator remediation (got: {rendered:?})"
        );
    }

    #[test]
    fn validate_nome_arg_rejects_leading_hyphen_with_named_value() {
        // The boundary-char footgun: `feira init "-hello"`
        // violates the DNS-1123 label rule that names must
        // start and end with an alphanumeric (the K8s
        // apiserver rejects leading / trailing `-` outright
        // at admission time, and on `feira init` a leading
        // `-` is structurally indistinguishable from a clap
        // flag prefix without `--` separator discipline). The
        // lifted predicate's reason wording names the boundary
        // rule verbatim — peer with the
        // [`validate_cluster_arg`] / [`validate_namespace_arg`]
        // boundary-hyphen arms.
        for bad in ["-hello", "hello-"] {
            let err = match validate_nome_arg(bad) {
                Err(e) => e,
                Ok(()) => panic!("boundary-`-` {bad:?} must reject"),
            };
            let rendered = format!("{err:?}");
            assert!(
                rendered.contains("<nome>"),
                "diagnostic must name the offending positional for {bad:?} (got: {rendered:?})"
            );
            assert!(
                rendered.contains(bad),
                "diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
            );
        }
    }

    #[test]
    fn validate_nome_arg_rejects_space_with_named_value() {
        // The paste-from-doc footgun: `feira init "my caixa"`
        // (an embedded space from a paste-from-aligned-doc).
        // The K8s `metadata.name` axes enforce the DNS-1123
        // label rule that names contain only `[a-z0-9-]`; the
        // embedded space surfaces at the apiserver-side
        // admission rule as a "field is invalid" rejection
        // far from the source positional. The lifted gate
        // refuses the embedded space with the offending value
        // named verbatim.
        let err = validate_nome_arg("my caixa").expect_err("space <nome> must reject");
        let rendered = format!("{err:?}");
        assert!(
            rendered.contains("<nome>"),
            "diagnostic must name the offending positional (got: {rendered:?})"
        );
        assert!(
            rendered.contains("my caixa"),
            "diagnostic must name the offending value verbatim (got: {rendered:?})"
        );
    }

    #[test]
    fn load_caixa_parse_error_preserves_underlying_lisp_error_on_chain() {
        // Peer to the parse-context pin above: the anyhow
        // `.with_context(...)` wrap preserves the underlying
        // `tatara_lisp::LispError` on the cause chain — a future
        // structured-output mode (`feira app graph --json`, the M4
        // `feira reconcile --json` diff verb) can downcast through
        // the chain and read the typed payload directly, peer with
        // the per-renderer `KindMismatch` / `ServicoCountMismatch`
        // typed-view downcast discipline.
        let dir = tempdir().expect("tempdir");
        std::fs::write(dir.path().join(CAIXA_MANIFEST_FILENAME), "(((((")
            .expect("write malformed manifest");
        let err = load_caixa(dir.path()).expect_err("malformed manifest must error");
        let typed_reachable = err
            .chain()
            .any(|e| e.downcast_ref::<tatara_lisp::LispError>().is_some());
        assert!(
            typed_reachable,
            "underlying tatara_lisp::LispError must remain reachable on the anyhow chain"
        );
    }
}