holger-traits 0.6.10

holger's shared vocabulary: ArtifactId, RepositoryBackendTrait, ZnippyPlugin discriminants, and ArtifactFormat — including `ArtifactFormat::home()`, the format→crate map that answers which crate serves PyPI when grepping cannot.
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
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
//! Foundational contracts and shared types for holger — the seam every backend,
//! upstream, and connector implements, plus the coordinate/format vocabulary they
//! all speak. This crate deliberately sits at the bottom of the dep graph and
//! stays thin: it depends on `znippy-common` (for the source-of-truth type ids)
//! but NOT on the sibling `znippy-plugin-*` crates.
//!
//! The three server-side seams:
//!   * [`RepositoryBackendTrait`] — a repo that serves an ecosystem's native index
//!     from a read-only znippy archive (or disk); `has_archive`/`archive_*` default
//!     to "no archive" so file-backed and writable repos inherit them unchanged.
//!   * [`RemoteUpstream`] — a read-oriented `RepositoryBackendTrait` that pull-through-
//!     caches from Nexus/Artifactory/holger/crates.io; drops into `ProxyBackend`
//!     where `fetch()` is the read and a writable primary write-through-caches the hit.
//!     Its HTTP is impl-side and SYNC (`ureq`), never `reqwest` — the server request
//!     path runs inside the async hyper handler, where a blocked-on `reqwest` panics.
//!   * [`HolgerObject`] — the transport-agnostic facade (in-process engine vs gRPC
//!     client, one `Arc<dyn>` handle), the Rust mirror of `holger.proto`.
//!
//! Agent-side, [`ConnectorTrait`] moves assets between a Source and Target; its
//! [`UpstreamAuth`] cousin here is the *server-side* auth enum, distinct from the
//! agent's `ConnectorAuth`.
//!
//! GOTCHA: znippy is the single source of truth for [`ArtifactFormat::znippy_type_id`]
//! discriminants — most are derived by calling the znippy handler's `type_id()`
//! directly so they can't drift, but `Pip` (2) and `Maven3` (3) stay hardcoded
//! literals because their native handlers live in the plugin crates this crate
//! won't depend on. The `znippy_type_ids_match_*` tests guard those two.

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

/// holger's own pin on the znippy on-disk archive format it will parse — the
/// reader-side gate that refuses a *newer*-than-known archive with a typed,
/// request-facing error instead of letting an unknown layout reach the parser.
/// Lives here so every `znippy-*` repository backend (all of which already depend
/// on this crate) consumes the one implementation.
pub mod znippy_format;

pub use znippy_format::{
    ArchiveFormatError, FormatGate, MAX_SUPPORTED_ZNIPPY_FORMAT, ZNIPPY_FORMAT_VERSION_KEY,
    ensure_supported_format, recorded_format_version,
};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ArtifactId {
    pub namespace: Option<String>,
    pub name: String,
    pub version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactFormat {
    Maven3,
    Pip,
    Rust,
    Go,
    Nuget,
    Npm,
    Gem,
    Deb,
    Rpm,
    Helm,
    Docker,
    Conda,
    Composer,
    /// znippy's own native package format (a merged/signed `.znippy` package
    /// served as the artifact itself, rather than a backing store for some
    /// other ecosystem). The one format that is znippy all the way down.
    Znippy,
    Raw,
}

impl ArtifactFormat {
    /// Canonical znippy `pkg_type` / DenseUnion discriminant for this format.
    ///
    /// znippy is the **single source of truth** for these numbers (see
    /// `znippy-common`'s native + skeleton handlers). Every variant whose handler
    /// is reachable from `znippy-common` calls that handler's
    /// [`ArchiveTypePlugin::type_id`] directly — including `Rust`, whose native
    /// `cargo_native::CargoPlugin` lives in `znippy-common` — so the two can never
    /// drift (the test below asserts the equality as data). Only `Pip` (2) and
    /// `Maven3` (3) remain literals: their native handlers live in the sibling
    /// `znippy-plugin-python` / `znippy-plugin-maven` crates, which this
    /// foundational `traits` crate deliberately does NOT depend on (keeping the
    /// base dep graph thin). The `znippy_type_ids_match_*` tests assert those two
    /// literals still equal znippy's published discriminants.
    pub fn znippy_type_id(&self) -> i8 {
        use znippy_common::plugins::{cargo_native, skeletons};
        use znippy_common::plugin::ArchiveTypePlugin;
        match self {
            // Native cargo handler lives in znippy-common — derive, don't hardcode.
            ArtifactFormat::Rust => cargo_native::CargoPlugin::new().type_id(),
            ArtifactFormat::Pip => 2,
            ArtifactFormat::Maven3 => 3,
            ArtifactFormat::Go => skeletons::GoPlugin.type_id(),
            ArtifactFormat::Nuget => skeletons::NugetPlugin.type_id(),
            // npm is a real native handler now (`plugins::npm_native`), not a skeleton.
            ArtifactFormat::Npm => znippy_common::plugins::npm_native::NpmPlugin.type_id(),
            // rpm is a real native handler now (`plugins::rpm_native`), not a skeleton.
            ArtifactFormat::Rpm => znippy_common::plugins::rpm_native::RpmPlugin.type_id(),
            // deb is a real native handler now (`plugins::deb_native`), not a skeleton.
            ArtifactFormat::Deb => znippy_common::plugins::deb_native::DebPlugin.type_id(),
            // gem is a real native handler now (`plugins::gem_native`), not a skeleton.
            ArtifactFormat::Gem => znippy_common::plugins::gem_native::GemPlugin.type_id(),
            ArtifactFormat::Docker => skeletons::DockerPlugin.type_id(),
            ArtifactFormat::Helm => skeletons::HelmPlugin.type_id(),
            // conda is a real native handler now (`plugins::conda_native`), not a skeleton.
            ArtifactFormat::Conda => znippy_common::plugins::conda_native::CondaPlugin.type_id(),
            ArtifactFormat::Composer => skeletons::ComposerPlugin.type_id(),
            // znippy's own container format — not one of the ecosystem handler
            // discriminants, so it gets a dedicated reserved id.
            ArtifactFormat::Znippy => 100,
            ArtifactFormat::Raw => 0,
        }
    }

    /// Parse a Nexus/Artifactory/CLI format string into an `ArtifactFormat`.
    pub fn from_format_str(s: &str) -> Option<Self> {
        Some(match s.to_lowercase().as_str() {
            "maven2" | "maven" | "maven3" | "java" => ArtifactFormat::Maven3,
            "pip" | "pypi" | "python" => ArtifactFormat::Pip,
            "rust" | "cargo" | "crates.io" | "cratesio" => ArtifactFormat::Rust,
            "go" | "golang" => ArtifactFormat::Go,
            "nuget" | "dotnet" => ArtifactFormat::Nuget,
            "npm" | "node" => ArtifactFormat::Npm,
            "gem" | "ruby" | "rubygems" => ArtifactFormat::Gem,
            "deb" | "debian" | "apt" => ArtifactFormat::Deb,
            "rpm" | "yum" | "dnf" => ArtifactFormat::Rpm,
            "helm" | "chart" => ArtifactFormat::Helm,
            "docker" | "oci" => ArtifactFormat::Docker,
            "conda" | "anaconda" => ArtifactFormat::Conda,
            "composer" | "php" => ArtifactFormat::Composer,
            "znippy" | "snippy" => ArtifactFormat::Znippy,
            // Artifactory calls this repo class "Generic"; holger calls it Raw.
            "raw" | "generic" => ArtifactFormat::Raw,
            _ => return None,
        })
    }

    /// **Where this format's backend actually LIVES.** The answer to "I know the
    /// capability by one name — which crate implements it?"
    ///
    /// # Why this exists (a real, repeated cost)
    ///
    /// A capability in holger routinely carries FOUR different names, and the one
    /// in the docs is usually not the one in the source tree. PyPI is the worst
    /// case, and it has already caused two wrong answers and a nearly-started
    /// 677-LOC re-implementation of code that was sitting right there:
    ///
    /// | where | the name it uses |
    /// |---|---|
    /// | README / design docs / the UI | **PyPI** |
    /// | `ron_repo_type` an operator writes | `pip` |
    /// | this enum + the backend struct | `Pip` / `PipRepoZnippy` |
    /// | the crate + its directory | **`znippy-python`** / `holger-znippy-python-repository` |
    ///
    /// So `grep -ri pypi server/` returns **nothing from the implementation**, and
    /// `grep -ri python` finds only a directory. [`from_format_str`] already maps
    /// the spoken aliases onto the variant, but it stops there — it never says
    /// where the code is. That last hop is the one that was missing, so this is it.
    ///
    /// `crate_dir` is relative to `server/repository/`, or empty for a format whose
    /// backend is not a separate repository crate (`Raw` lives in
    /// `server/lib/src/raw.rs`). `also` names a SECOND backend for the same format
    /// where one exists (a file-backed peer of the znippy-backed one).
    ///
    /// The mapping is asserted against the real directory layout by
    /// `every_format_names_a_crate_that_exists`, so a renamed crate breaks the
    /// build instead of quietly re-opening the trap.
    ///
    /// [`from_format_str`]: ArtifactFormat::from_format_str
    pub fn home(&self) -> FormatHome {
        // (config_key, aliases, crate_dir, package, also)
        let (config_key, aliases, crate_dir, package, also) = match self {
            ArtifactFormat::Maven3 => {
                ("maven3", &["maven", "maven2", "java"][..], "znippy-maven", "holger-znippy-maven-repository", None)
            }
            // The four-name case documented above: docs say PyPI, config says pip,
            // code says Pip, the crate says python.
            ArtifactFormat::Pip => {
                ("pip", &["pypi", "python"][..], "znippy-python", "holger-znippy-python-repository", None)
            }
            // Note the word order flips between dir and package here too:
            // `znippy-rust/` is `holger-rust-znippy-repository`.
            ArtifactFormat::Rust => {
                ("rust", &["cargo", "crates.io", "cratesio"][..], "znippy-rust", "holger-rust-znippy-repository", Some(("file-rust", "holger-rust-file-repository")))
            }
            ArtifactFormat::Go => ("go", &["golang"][..], "znippy-go", "holger-znippy-go-repository", None),
            ArtifactFormat::Nuget => ("nuget", &["dotnet"][..], "znippy-nuget", "holger-znippy-nuget-repository", None),
            ArtifactFormat::Npm => ("npm", &["node"][..], "znippy-npm", "holger-znippy-npm-repository", None),
            ArtifactFormat::Gem => ("gem", &["ruby", "rubygems"][..], "znippy-gem", "holger-znippy-gem-repository", None),
            ArtifactFormat::Deb => ("deb", &["debian", "apt"][..], "znippy-deb", "holger-znippy-deb-repository", None),
            ArtifactFormat::Rpm => ("rpm", &["yum", "dnf"][..], "znippy-rpm", "holger-znippy-rpm-repository", None),
            ArtifactFormat::Helm => ("helm", &["chart"][..], "znippy-helm", "holger-znippy-helm-repository", None),
            // `oci` is the config alias AND a second, file-backed implementation.
            ArtifactFormat::Docker => {
                ("docker", &["oci"][..], "znippy-docker", "holger-znippy-docker-repository", Some(("file-oci", "holger-file-oci-repository")))
            }
            ArtifactFormat::Conda => ("conda", &["anaconda"][..], "znippy-conda", "holger-znippy-conda-repository", None),
            ArtifactFormat::Composer => ("composer", &["php"][..], "znippy-composer", "holger-znippy-composer-repository", None),
            // znippy-as-the-artifact. The crate is `znippy-package`, not `znippy`.
            ArtifactFormat::Znippy => {
                ("znippy", &["snippy"][..], "znippy-package", "holger-znippy-package-repository", None)
            }
            // Not a repository crate: the backend is `server/lib/src/raw.rs`.
            ArtifactFormat::Raw => ("raw", &["generic"][..], "", "holger-server-lib", None),
        };
        FormatHome { format: self.clone(), config_key, aliases, crate_dir, package, also }
    }
}

/// Where one [`ArtifactFormat`]'s backend lives, plus every other name the same
/// capability is called by. See [`ArtifactFormat::home`] for why this is needed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatHome {
    /// The format itself.
    pub format: ArtifactFormat,
    /// The canonical `ron_repo_type` string an operator writes in a config.
    pub config_key: &'static str,
    /// The OTHER names this capability is known by — docs, UI labels, ecosystem
    /// spellings. Searching any of them should lead here.
    pub aliases: &'static [&'static str],
    /// Directory under `server/repository/`, or `""` when the backend is not a
    /// separate repository crate.
    pub crate_dir: &'static str,
    /// The cargo package name of that crate.
    pub package: &'static str,
    /// A second `(crate_dir, package)` backend for the same format, when one
    /// exists (e.g. the file-backed peer of a znippy-backed repo).
    pub also: Option<(&'static str, &'static str)>,
}

impl FormatHome {
    /// Every name this capability answers to — the config key first, then the
    /// aliases. Handy for a "did you mean?" or a docs table.
    pub fn all_names(&self) -> Vec<&'static str> {
        std::iter::once(self.config_key).chain(self.aliases.iter().copied()).collect()
    }
}

/// Every format holger knows, in declaration order — so a caller can enumerate
/// the capability set instead of hand-maintaining a second list.
pub const ALL_ARTIFACT_FORMATS: &[ArtifactFormat] = &[
    ArtifactFormat::Maven3,
    ArtifactFormat::Pip,
    ArtifactFormat::Rust,
    ArtifactFormat::Go,
    ArtifactFormat::Nuget,
    ArtifactFormat::Npm,
    ArtifactFormat::Gem,
    ArtifactFormat::Deb,
    ArtifactFormat::Rpm,
    ArtifactFormat::Helm,
    ArtifactFormat::Docker,
    ArtifactFormat::Conda,
    ArtifactFormat::Composer,
    ArtifactFormat::Znippy,
    ArtifactFormat::Raw,
];

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum StorageType {
    Znippy,
    Rocksdb,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum RepositoryType {
    Rust,
    Pip,
    Maven3,
    Go,
    Nuget,
    Npm,
    Gem,
    Deb,
    Rpm,
    Helm,
    Docker,
    Conda,
    Composer,
    Znippy,
    Raw,
}

impl RepositoryType {
    pub fn endpoint_name(&self) -> &'static str {
        match self {
            RepositoryType::Rust => "rust",
            RepositoryType::Pip => "pip",
            RepositoryType::Maven3 => "maven3",
            RepositoryType::Go => "go",
            RepositoryType::Nuget => "nuget",
            RepositoryType::Npm => "npm",
            RepositoryType::Gem => "gem",
            RepositoryType::Deb => "deb",
            RepositoryType::Rpm => "rpm",
            RepositoryType::Helm => "helm",
            RepositoryType::Docker => "docker",
            RepositoryType::Conda => "conda",
            RepositoryType::Composer => "composer",
            RepositoryType::Znippy => "znippy",
            RepositoryType::Raw => "raw",
        }
    }
}

/// Stats for the znippy archive backing a repository (mirror of the proto
/// `ArchiveInfoResponse`). `archive_path` is the repository/archive name — a
/// human-readable handle, not a filesystem path. Defaults to "no archive" (a
/// backend that isn't archive-backed reports zero files / zero bytes).
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArchiveInfo {
    pub file_count: u64,
    pub total_uncompressed_bytes: u64,
    pub archive_path: String,
}

#[async_trait]
pub trait RepositoryBackendTrait: Send + Sync {
    fn name(&self) -> &str;
    fn format(&self) -> ArtifactFormat;
    fn is_writable(&self) -> bool;

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>>;
    fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()>;

    /// List the raw file paths inside the backing znippy archive, optionally
    /// filtered to those starting with `prefix`. These are archive-raw paths
    /// (format-agnostic), not parsed artifact ids. Default: empty (a backend
    /// with no archive — or that can't enumerate one — returns nothing).
    fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
        let _ = prefix;
        Ok(Vec::new())
    }

    /// Stats for the backing znippy archive (file count + total uncompressed
    /// bytes + name). Default: the empty [`ArchiveInfo`] (no archive).
    fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
        Ok(ArchiveInfo::default())
    }

    /// List artifacts this backend holds, newest/any order, optionally filtered
    /// by a substring of the artifact name, capped at `limit`. Default: empty
    /// (a backend that can't enumerate returns nothing — the contract stays
    /// valid, the listing just stays empty).
    fn list(&self, name_filter: Option<&str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
        let _ = (name_filter, limit);
        Ok(Vec::new())
    }

    /// Whether this backend is backed by a znippy archive. Defaults to `false`
    /// (file-backed / writable repos carry no archive). Znippy-backed repos
    /// override this to return `true` when their archive reader is loaded.
    fn has_archive(&self) -> bool {
        false
    }

    /// Reclaim one artifact's bytes from this backend — the write side of the
    /// retention/GC executor (the planner in `holger-server-lib::retention`
    /// decides *what*; this carries the decision out for a single coordinate).
    ///
    /// **Safe by contract:** `expected_content_id` is the lowercase-hex content
    /// digest the caller (executor) computed for the bytes it intends to drop; an
    /// implementation MUST recompute the digest of what is actually stored and
    /// refuse the unlink (returning `Err`) if it does not match — so a store that
    /// changed under the plan (a re-upload, a swap, a path collision) is never
    /// deleted blind. A missing artifact is an error (nothing to reclaim), never a
    /// silent success.
    ///
    /// Default: `Err` — holger's backing store is **immutable** znippy archives,
    /// which have no in-place delete (retention there is a whole-generation
    /// archive-set drop, out of band). Only the writable, loose-file backends
    /// override this. Immutable/read-only backends fail **closed** here rather
    /// than pretend to reclaim.
    fn delete_artifact(&self, id: &ArtifactId, expected_content_id: &str) -> anyhow::Result<()> {
        let _ = (id, expected_content_id);
        anyhow::bail!(
            "backend '{}' does not support in-place artifact deletion (immutable store); \
             retention here is a whole-generation archive-set drop, not a per-artifact unlink",
            self.name()
        )
    }

    // (status, headers, body) is the established HTTP-door contract across ~21
    // backends; factoring it into a type alias would churn every implementer for a
    // style lint newly raised by rust 1.96 clippy. Suppress in place.
    #[allow(clippy::type_complexity)]
    fn handle_http2_request(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)>;

    /// The artifact coordinate a raw HTTP `suburl` addresses, when the backend can
    /// map its own path scheme back to a coordinate (the inverse of the path it
    /// serves from). Used by the **serve-time quarantine gate** to look up an
    /// artifact's properties before the bytes are served over HTTP. Default `None`
    /// — a backend whose native protocol paths don't map cleanly to a single
    /// coordinate (npm packuments, Maven metadata, …) is simply not HTTP-gated by
    /// coordinate (the gRPC `fetch_artifact` path, which carries an explicit
    /// coordinate, always is). `suburl` includes the leading `/<repo>/…`.
    fn coordinate_for_path(&self, suburl: &str) -> Option<ArtifactId> {
        let _ = suburl;
        None
    }
}

// === Remote-cache upstream abstraction (server-side, sync request path) ===
//
// A remote-cache *upstream* lets a holger repository transparently fetch-and-
// cache an artifact from an upstream registry (Nexus, Artifactory, another
// holger, crates.io) on a local miss. It is a read-oriented
// [`RepositoryBackendTrait`] so it drops straight into the existing
// `ProxyBackend` pull-through cache: `fetch()` returns the upstream bytes and a
// writable primary write-through-caches them.
//
// This lives in `traits` (the single source of truth) so every concrete impl —
// `NexusUpstream`, `ArtifactoryUpstream`, … in `holger-server-lib` — shares one
// auth enum, one metadata shape, and one coordinate→URL path mapping. The HTTP
// itself is implemented impl-side with a SYNC, runtime-agnostic client (`ureq`),
// because the server request path runs inside the async hyper handler where a
// blocked-on `reqwest` would panic.

/// How a remote-cache upstream authenticates with its registry. Promoted into
/// `traits` as the single source of truth shared by every upstream impl
/// (Nexus = Basic, Artifactory = Basic/Bearer/API-key, holger = Bearer).
///
/// Distinct from the agent-side `ConnectorAuth` (async/reqwest, agent-only),
/// which keeps its own OIDC-token-fetch semantics; this enum is the server-side
/// upstream contract.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Default)]
pub enum UpstreamAuth {
    /// No authentication.
    #[default]
    None,
    /// HTTP Basic auth (Nexus, Artifactory).
    Basic { username: String, password: String },
    /// Bearer token (holger OIDC, Artifactory access token).
    Bearer { token: String },
    /// Custom API-key header, e.g. Artifactory `X-JFrog-Art-Api-Key`.
    ApiKey { header: String, key: String },
    /// mTLS client cert (built into the HTTP client, no per-request header).
    Mtls { cert_pem: String, key_pem: String },
}


/// Size + checksum(s) + content-type for an upstream artifact. Lets the proxy
/// verify a fetched body before write-through caching and answer index/metadata
/// probes without downloading the body.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactMeta {
    pub size: Option<u64>,
    pub sha256: Option<String>,
    pub content_type: Option<String>,
}

/// A read-oriented remote-cache upstream: a [`RepositoryBackendTrait`] that
/// knows its upstream base URL + auth and can cheaply probe existence/metadata.
/// Plugs into `ProxyBackend` unchanged — `fetch()` is the read, a writable
/// primary caches the hit.
pub trait RemoteUpstream: RepositoryBackendTrait {
    /// The upstream base, e.g. `https://nexus.corp` or `https://jfrog.corp`.
    fn base_url(&self) -> &str;

    /// Credentials attached to upstream requests.
    fn auth(&self) -> &UpstreamAuth;

    /// Cheap existence probe. Default: a `fetch` that discards the body — impls
    /// should override with a HEAD where the registry supports one.
    fn exists(&self, id: &ArtifactId) -> anyhow::Result<bool> {
        Ok(self.fetch(id)?.is_some())
    }

    /// Size + checksum + content-type, used to verify before write-through
    /// caching. `Ok(None)` when the upstream has no such artifact.
    fn metadata(&self, id: &ArtifactId) -> anyhow::Result<Option<ArtifactMeta>>;
}

/// Map an artifact coordinate to the upstream layout path for `fmt`. This is the
/// per-ecosystem translation Nexus/Artifactory share (maven2/npm/pypi/… layouts
/// relative to the repository root). Joined onto the registry-specific content
/// base (`{base}/repository/{repo}/…` for Nexus, `{base}/artifactory/{repo}/…`
/// for Artifactory) by the impl.
pub fn upstream_path(fmt: ArtifactFormat, id: &ArtifactId) -> String {
    match fmt {
        ArtifactFormat::Rust => format!("{n}/{n}-{v}.crate", n = id.name, v = id.version),
        ArtifactFormat::Maven3 => {
            // group.id → group/id ; classic maven2 layout.
            let g = id.namespace.as_deref().unwrap_or("").replace('.', "/");
            format!("{g}/{a}/{v}/{a}-{v}.jar", a = id.name, v = id.version)
        }
        ArtifactFormat::Pip => {
            format!("packages/source/{n}/{n}-{v}.tar.gz", n = id.name, v = id.version)
        }
        ArtifactFormat::Npm => format!("{n}/-/{n}-{v}.tgz", n = id.name, v = id.version),
        ArtifactFormat::Nuget => {
            let n = id.name.to_lowercase();
            format!("{n}/{v}/{n}.{v}.nupkg", n = n, v = id.version)
        }
        ArtifactFormat::Gem => format!("gems/{n}-{v}.gem", n = id.name, v = id.version),
        _ => format!("{}/{}", id.name, id.version),
    }
}

// === Connector trait for external systems (Nexus, Artifactory, crates.io) ===

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteRepository {
    pub name: String,
    pub format: String,
    pub repo_type: String,
    pub url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteAsset {
    pub path: String,
    pub download_url: String,
    pub content_type: Option<String>,
    pub size: Option<u64>,
}

#[async_trait]
pub trait ConnectorTrait: Send + Sync {
    fn name(&self) -> &str;

    async fn list_repositories(&self) -> anyhow::Result<Vec<RemoteRepository>>;

    async fn list_assets(&self, repository: &str) -> anyhow::Result<Vec<RemoteAsset>>;

    async fn download_asset(&self, asset: &RemoteAsset) -> anyhow::Result<Vec<u8>>;

    async fn upload_asset(
        &self,
        repository: &str,
        path: &str,
        data: &[u8],
    ) -> anyhow::Result<()>;
}


// === HolgerObject: the transport-agnostic facade ===
//
// `HolgerObject` is the single Rust contract an external partner programs
// against. Hold an `Arc<dyn HolgerObject>` and you don't care what's behind the
// vtable — exactly like a Java interface reference. Two transports implement it
// today:
//   * an in-process adapter that calls the local Holger engine directly, and
//   * a remote adapter that forwards each call over gRPC (tonic client).
// The language-neutral contract for non-Rust / remote partners is `holger.proto`
// (the generated client/server stubs); this trait mirrors it for Rust callers.

/// Summary of one configured repository (mirror of the proto `RepositoryInfo`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepositoryInfo {
    pub name: String,
    pub repo_type: String,
    pub writable: bool,
    pub has_archive: bool,
}

/// One artifact in a repository listing (mirror of the proto `ArtifactEntry`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactEntry {
    pub id: ArtifactId,
    pub size_bytes: i64,
    pub content_type: String,
}

/// Server health (mirror of the proto `HealthResponse`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Health {
    pub status: String,
    pub version: String,
    pub uptime_seconds: i64,
}

/// Server operating profile (mirror of the proto `ServerProfileResponse`).
///
/// This is *server truth*: whether the server accepts writes at all, derived
/// from the live route table (or an explicit sealed-mode override). The UI
/// drives its static "silent running" chrome off this rather than a local
/// toggle.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ServerProfile {
    /// "static" (sealed / read-only) or "dynamic" (writable / proxy).
    pub profile: String,
    /// True when the server accepts no writes on any repository.
    pub read_only: bool,
    /// Human label for the UI chrome / badge.
    pub label: String,
    /// Number of configured repositories that accept writes.
    pub writable_repo_count: i32,
}

impl Default for ServerProfile {
    /// A conservative dynamic (writable) default for transports that don't
    /// report a profile.
    fn default() -> Self {
        Self {
            profile: "dynamic".into(),
            read_only: false,
            label: "DYNAMIC / WRITABLE".into(),
            writable_repo_count: 0,
        }
    }
}

/// A cross-repo search query (mirror of the proto `SearchRequest`). Every axis
/// is optional (empty = unconstrained); axes AND-compose on the server.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SearchQuery {
    /// Case-insensitive substring match on the artifact name. Empty = any.
    pub name: String,
    /// Case-insensitive substring match on the namespace. Empty = any.
    pub namespace: String,
    /// Exact version match. Empty = any.
    pub version: String,
    /// Case-insensitive substring match on a raw archive file path.
    pub path: String,
    /// Restrict to these repositories. Empty = every configured repo.
    pub repositories: Vec<String>,
    /// Cap hits per kind. 0 ⇒ the server default.
    pub limit: i32,
}

/// One artifact matched by a search, tagged with the repo it was found in
/// (mirror of the proto `ArtifactHit`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SearchArtifactHit {
    pub repository: String,
    pub id: ArtifactId,
    pub size_bytes: i64,
    pub content_type: String,
}

/// One raw archive path matched by the `path` axis (mirror of the proto `PathHit`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SearchPathHit {
    pub repository: String,
    pub path: String,
}

/// A search result page (mirror of the proto `SearchResponse`).
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SearchResults {
    pub artifacts: Vec<SearchArtifactHit>,
    pub paths: Vec<SearchPathHit>,
    /// True when the result hit the limit cap and more hits exist.
    pub truncated: bool,
}

/// A hosted SBOM document for one artifact coordinate (mirror of the proto
/// `FetchSbomResponse`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SbomDoc {
    /// The document bytes (byte-identical to what was attached).
    pub document: Vec<u8>,
    pub size_bytes: i64,
    /// blake3 hex of the document (its content address).
    pub content_address: String,
}

/// One custom property on an artifact coordinate: a key with its ordered values
/// (mirror of the proto `PropertyEntry`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PropertyKv {
    pub key: String,
    pub values: Vec<String>,
}

/// Transport-agnostic Holger facade. Callable from the CLI or a UI backend over
/// either transport (direct in-process or gRPC) behind the same handle.
#[async_trait]
pub trait HolgerObject: Send + Sync {
    /// Fetch an artifact by id from a named repository.
    async fn fetch(&self, repository: &str, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>>;

    /// Store an artifact (write-enabled repositories only).
    async fn put(&self, repository: &str, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()>;

    /// List all configured repositories.
    async fn list_repositories(&self) -> anyhow::Result<Vec<RepositoryInfo>>;

    /// List artifacts in a repository, optionally filtered by a `name_filter`
    /// substring, capped at `limit`, paged via an opaque `page_token`. Returns
    /// the page of entries plus the `next_page_token` (empty when exhausted).
    ///
    /// The default implementation returns an empty page, so transports that do
    /// not (yet) surface a listing — e.g. the in-process [`HolgerObject`]s whose
    /// server returns empty today — inherit it unchanged. Override it where the
    /// transport carries a real `ListArtifacts` call (the gRPC remote does).
    async fn list_artifacts(
        &self,
        repository: &str,
        name_filter: Option<String>,
        limit: u32,
        page_token: Option<String>,
    ) -> anyhow::Result<(Vec<ArtifactEntry>, String)> {
        let _ = (repository, name_filter, limit, page_token);
        Ok((Vec::new(), String::new()))
    }

    /// List the raw file paths inside the znippy archive backing `repository`,
    /// optionally filtered to those starting with `prefix`. These are
    /// archive-raw paths (format-agnostic), not parsed artifact ids.
    ///
    /// The default returns an empty list, so transports that don't (yet)
    /// surface archive browsing inherit it unchanged. Override it where the
    /// transport carries a real `ListArchiveFiles` call (the gRPC remote and
    /// in-process local both do).
    async fn list_archive_files(
        &self,
        repository: &str,
        prefix: Option<String>,
    ) -> anyhow::Result<Vec<String>> {
        let _ = (repository, prefix);
        Ok(Vec::new())
    }

    /// Stats for the znippy archive backing `repository` (file count + total
    /// uncompressed bytes + name). Default: the empty [`ArchiveInfo`].
    async fn archive_info(&self, repository: &str) -> anyhow::Result<ArchiveInfo> {
        let _ = repository;
        Ok(ArchiveInfo::default())
    }

    /// Server health / version / uptime.
    async fn health(&self) -> anyhow::Result<Health>;

    /// Server operating profile (static/read-only vs dynamic/writable).
    ///
    /// The default returns a dynamic (writable) profile so transports that
    /// don't report one inherit it unchanged. Override it where the transport
    /// can derive real server truth (the gRPC remote via `AdminService.
    /// ServerProfile`, the in-process local from its route table).
    async fn server_profile(&self) -> anyhow::Result<ServerProfile> {
        Ok(ServerProfile::default())
    }

    /// Cross-repo artifact search (`SearchService.Search`) — an open read.
    ///
    /// The default returns the empty result set, so transports that don't (yet)
    /// surface search inherit it unchanged. Override it where the transport
    /// carries the real RPC (the gRPC remote does).
    async fn search(&self, query: &SearchQuery) -> anyhow::Result<SearchResults> {
        let _ = query;
        Ok(SearchResults::default())
    }

    /// Fetch the hosted SBOM document for a coordinate (`SbomService.FetchSbom`)
    /// — an open read; `Ok(None)` when none is attached. Default: none.
    async fn fetch_sbom(
        &self,
        repository: &str,
        id: &ArtifactId,
    ) -> anyhow::Result<Option<SbomDoc>> {
        let _ = (repository, id);
        Ok(None)
    }

    /// Read all custom properties on a coordinate
    /// (`PropertyService.GetProperties`) — an open read; empty when none.
    /// Default: empty.
    async fn get_properties(
        &self,
        repository: &str,
        id: &ArtifactId,
    ) -> anyhow::Result<Vec<PropertyKv>> {
        let _ = (repository, id);
        Ok(Vec::new())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Emit one functional-status row for a real check. Gated behind
    /// `--features testmatrix` so release builds strip it (dep is optional).
    #[cfg(feature = "testmatrix")]
    fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
        nornir_testmatrix::functional_status(component, check, ok, detail);
    }

    #[test]
    fn znippy_type_ids_match_znippy_skeletons() {
        // znippy is the source of truth — these literals must equal the znippy
        // handler discriminants, or the mapping has drifted.
        assert_eq!(ArtifactFormat::Go.znippy_type_id(), 4);
        assert_eq!(ArtifactFormat::Nuget.znippy_type_id(), 5);
        assert_eq!(ArtifactFormat::Npm.znippy_type_id(), 6);
        assert_eq!(ArtifactFormat::Rpm.znippy_type_id(), 8);
        assert_eq!(ArtifactFormat::Deb.znippy_type_id(), 9);
        assert_eq!(ArtifactFormat::Gem.znippy_type_id(), 11);
        assert_eq!(ArtifactFormat::Docker.znippy_type_id(), 12);
        assert_eq!(ArtifactFormat::Helm.znippy_type_id(), 13);
        assert_eq!(ArtifactFormat::Conda.znippy_type_id(), 14);
        assert_eq!(ArtifactFormat::Composer.znippy_type_id(), 17);
        // natives
        assert_eq!(ArtifactFormat::Rust.znippy_type_id(), 1);
        assert_eq!(ArtifactFormat::Pip.znippy_type_id(), 2);
        assert_eq!(ArtifactFormat::Maven3.znippy_type_id(), 3);

        #[cfg(feature = "testmatrix")]
        {
            let ok = ArtifactFormat::Helm.znippy_type_id() == 13
                && ArtifactFormat::Rust.znippy_type_id() == 1
                && ArtifactFormat::Composer.znippy_type_id() == 17;
            fstatus(
                "traits",
                "znippy_type_ids_match",
                ok,
                &format!(
                    "Rust=1 Pip=2 Maven3=3 Helm=13 Composer=17 (helm={})",
                    ArtifactFormat::Helm.znippy_type_id()
                ),
            );
        }
    }

    /// Drift guard sourced from znippy itself: instead of comparing against
    /// hand-typed literals, assert each format's `znippy_type_id()` equals the
    /// `type_id()` of the corresponding znippy-common handler. If holger's table
    /// (or znippy's handler register) ever drifts, this fails — proving the two
    /// stay a single source of truth, as data rather than by eyeballing.
    ///
    /// Only the handlers reachable from `znippy-common` are covered (every native
    /// + skeleton handler except the maven/python natives, which live in the
    /// sibling plugin crates that `traits` does not depend on — those two stay
    /// guarded by the literal assertions in `znippy_type_ids_match_znippy_skeletons`).
    #[test]
    fn znippy_type_ids_derive_from_znippy_handlers_not_holger_copies() {
        use znippy_common::plugin::ArchiveTypePlugin;
        use znippy_common::plugins::{
            cargo_native::CargoPlugin, conda_native::CondaPlugin, deb_native::DebPlugin,
            gem_native::GemPlugin, npm_native::NpmPlugin, rpm_native::RpmPlugin, skeletons,
        };

        // (format, znippy handler's own type_id) — the right-hand side is znippy's
        // source of truth, never a literal copied into holger.
        let pairs: &[(ArtifactFormat, i8)] = &[
            // Rust is now derived from cargo_native (in znippy-common) rather than
            // a hardcoded `1` — this asserts the derivation, not a copy.
            (ArtifactFormat::Rust, CargoPlugin::new().type_id()),
            (ArtifactFormat::Npm, NpmPlugin.type_id()),
            (ArtifactFormat::Gem, GemPlugin.type_id()),
            (ArtifactFormat::Conda, CondaPlugin.type_id()),
            (ArtifactFormat::Rpm, RpmPlugin.type_id()),
            (ArtifactFormat::Deb, DebPlugin.type_id()),
            (ArtifactFormat::Go, skeletons::GoPlugin.type_id()),
            (ArtifactFormat::Nuget, skeletons::NugetPlugin.type_id()),
            (ArtifactFormat::Docker, skeletons::DockerPlugin.type_id()),
            (ArtifactFormat::Helm, skeletons::HelmPlugin.type_id()),
            (ArtifactFormat::Composer, skeletons::ComposerPlugin.type_id()),
        ];

        let mut all_ok = true;
        for (fmt, znippy_id) in pairs {
            let holger_id = fmt.znippy_type_id();
            if holger_id != *znippy_id {
                all_ok = false;
            }
            assert_eq!(
                holger_id, *znippy_id,
                "{fmt:?}: holger znippy_type_id()={holger_id} drifted from znippy handler type_id()={znippy_id}"
            );
        }

        #[cfg(feature = "testmatrix")]
        fstatus(
            "traits",
            "znippy_type_ids_derive_from_handlers",
            all_ok,
            &format!("{} formats derive their discriminant from znippy handlers", pairs.len()),
        );
        let _ = all_ok;
    }

    #[test]
    fn upstream_path_maps_per_ecosystem_layout() {
        let id = ArtifactId { namespace: None, name: "serde".into(), version: "1.0.0".into() };
        assert_eq!(upstream_path(ArtifactFormat::Rust, &id), "serde/serde-1.0.0.crate");
        assert_eq!(upstream_path(ArtifactFormat::Npm, &id), "serde/-/serde-1.0.0.tgz");
        assert_eq!(upstream_path(ArtifactFormat::Pip, &id), "packages/source/serde/serde-1.0.0.tar.gz");

        let mvn = ArtifactId {
            namespace: Some("com.example".into()),
            name: "lib".into(),
            version: "2.1".into(),
        };
        assert_eq!(upstream_path(ArtifactFormat::Maven3, &mvn), "com/example/lib/2.1/lib-2.1.jar");
    }

    #[test]
    fn upstream_auth_defaults_to_none() {
        assert_eq!(UpstreamAuth::default(), UpstreamAuth::None);
        assert_ne!(
            UpstreamAuth::Bearer { token: "t".into() },
            UpstreamAuth::Basic { username: "u".into(), password: "p".into() }
        );
    }

    /// **The naming-mismatch guard.** Every format must name a crate that really
    /// exists on disk, and every name it is known by must resolve back to it.
    ///
    /// RED when a crate is renamed/moved without updating [`ArtifactFormat::home`]
    /// — which is the exact moment the trap re-opens: the docs keep saying "PyPI",
    /// the map keeps saying `znippy-python`, and the directory has become something
    /// else. A stale signpost is worse than none, so this fails the build instead.
    #[test]
    fn every_format_names_a_crate_that_exists() {
        // `traits/` sits beside `server/`, so the repo root is one level up.
        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .expect("traits/ has a parent")
            .to_path_buf();

        for fmt in ALL_ARTIFACT_FORMATS {
            let home = fmt.home();

            // 1. The canonical config key and EVERY alias must parse back to this
            //    same format — so searching any spoken name lands on this map.
            for name in home.all_names() {
                assert_eq!(
                    ArtifactFormat::from_format_str(name),
                    Some(fmt.clone()),
                    "{fmt:?}: the name {name:?} must resolve back to it"
                );
            }

            // 2. The named crate directory must EXIST (and be a real crate).
            for (dir, package) in std::iter::once((home.crate_dir, home.package))
                .chain(home.also)
                .filter(|(d, _)| !d.is_empty())
            {
                let manifest = repo_root.join("server/repository").join(dir).join("Cargo.toml");
                assert!(
                    manifest.is_file(),
                    "{fmt:?}: home() points at server/repository/{dir}, which does not exist \
                     (renamed? then update ArtifactFormat::home)"
                );
                let text = std::fs::read_to_string(&manifest).expect("read Cargo.toml");
                assert!(
                    text.lines().any(|l| l.trim() == format!("name = \"{package}\"")),
                    "{fmt:?}: server/repository/{dir} is not package {package:?} \
                     (renamed? then update ArtifactFormat::home)"
                );
            }
        }
    }

    /// The specific case that cost real time: PyPI is spelled four ways, and the
    /// implementation directory matches NONE of the two an operator or a reader
    /// would try first. Pin it by name so the map is not silently "simplified".
    #[test]
    fn the_pypi_four_name_split_is_pinned() {
        let home = ArtifactFormat::Pip.home();
        assert_eq!(home.config_key, "pip", "operators write ron_repo_type: \"pip\"");
        assert!(home.aliases.contains(&"pypi"), "the docs/UI name must be findable");
        assert!(home.aliases.contains(&"python"), "the crate's own name must be findable");
        assert_eq!(
            home.crate_dir, "znippy-python",
            "the implementation lives under a THIRD name — that is the whole point"
        );
        // The trap, stated as an assertion: neither name the reader reaches for
        // first is the directory name.
        assert_ne!(home.crate_dir, home.config_key);
        assert_ne!(home.crate_dir, "pypi");
    }

    #[test]
    fn format_str_roundtrips_aliases() {
        assert_eq!(ArtifactFormat::from_format_str("cargo"), Some(ArtifactFormat::Rust));
        assert_eq!(ArtifactFormat::from_format_str("golang"), Some(ArtifactFormat::Go));
        assert_eq!(ArtifactFormat::from_format_str("dotnet"), Some(ArtifactFormat::Nuget));
        assert_eq!(ArtifactFormat::from_format_str("oci"), Some(ArtifactFormat::Docker));
        assert_eq!(ArtifactFormat::from_format_str("nope"), None);

        #[cfg(feature = "testmatrix")]
        {
            let ok = ArtifactFormat::from_format_str("cargo") == Some(ArtifactFormat::Rust)
                && ArtifactFormat::from_format_str("oci") == Some(ArtifactFormat::Docker)
                && ArtifactFormat::from_format_str("nope").is_none();
            fstatus(
                "traits",
                "format_str_aliases_roundtrip",
                ok,
                "cargo->Rust golang->Go dotnet->Nuget oci->Docker nope->None",
            );
        }
    }
}