use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcosystemCapabilities {
pub keyword: String,
pub forge_dispatch: bool,
pub reverse_extractor: bool,
pub validator: bool,
pub green_ci_starter: bool,
pub url_discover: bool,
pub preserved_fields: Vec<String>,
}
impl EcosystemCapabilities {
pub fn fully_capable(&self) -> bool {
self.forge_dispatch
&& self.reverse_extractor
&& self.validator
&& self.green_ci_starter
&& self.url_discover
}
}
pub const ALL_ECOSYSTEMS: &[&str] = &[
"rust-single-crate", "rust-workspace",
"npm", "js-pnpm", "js-deno",
"python", "python-pdm", "python-pipenv", "python-conda",
"helm", "github-action",
"go", "zig", "fortran-fpm",
"java-maven", "java-gradle-kts", "scala-sbt", "clojure-deps",
"dotnet-csproj", "swift-spm", "ocaml-dune",
"haskell-cabal", "gleam", "racket-info",
"elixir-mix", "ruby-gem", "lua-rockspec", "nim-nimble",
"crystal", "dart", "composer", "julia", "r-description",
"ada-alire",
"cpp-conan", "cpp-vcpkg", "cpp-meson", "cpp-cmake",
"nix-flake",
"tlisp-library",
];
pub fn query(keyword: &str) -> EcosystemCapabilities {
EcosystemCapabilities {
keyword: keyword.to_string(),
forge_dispatch: has_forge_dispatch(keyword),
reverse_extractor: has_reverse_extractor(keyword),
validator: has_validator(keyword),
green_ci_starter: has_green_ci_starter(keyword),
url_discover: has_url_discover(keyword),
preserved_fields: crate::fidelity::compared_fields_for(keyword)
.iter().map(|s| (*s).to_string()).collect(),
}
}
pub fn query_all() -> Vec<EcosystemCapabilities> {
ALL_ECOSYSTEMS.iter().map(|e| query(e)).collect()
}
fn has_forge_dispatch(keyword: &str) -> bool {
matches!(keyword,
"rust-single-crate" | "rust-workspace" | "npm" | "python" | "helm" |
"github-action" | "go" | "crystal" | "dart" | "composer" | "julia" |
"java-maven" | "dotnet-csproj" | "ocaml-dune" | "java-gradle-kts" |
"swift-spm" | "elixir-mix" | "ruby-gem" | "zig" | "nim-nimble" |
"scala-sbt" | "clojure-deps" | "r-description" | "lua-rockspec" |
"cpp-conan" | "python-conda" | "python-pipenv" | "python-pdm" |
"js-deno" | "js-pnpm" | "cpp-vcpkg" | "cpp-meson" | "cpp-cmake" |
"fortran-fpm" | "gleam" | "ada-alire" | "haskell-cabal" | "racket-info"
)
}
fn has_reverse_extractor(keyword: &str) -> bool {
matches!(keyword,
"rust-single-crate" | "rust-workspace" | "npm" | "js-pnpm" |
"python" | "python-pdm" | "helm" | "go" | "ruby-gem" |
"ocaml-dune" | "dotnet-csproj" | "swift-spm" | "elixir-mix" |
"scala-sbt" | "clojure-deps" |
"crystal" | "dart" | "composer" | "julia" |
"zig" | "fortran-fpm" | "gleam" | "racket-info" |
"java-maven" | "js-deno" | "cpp-vcpkg" | "python-conda" | "ada-alire" |
"java-gradle-kts" | "cpp-cmake" | "cpp-meson" | "cpp-conan" |
"haskell-cabal" | "nim-nimble" | "lua-rockspec" | "r-description" |
"python-pipenv" | "github-action" | "nix-flake" | "tlisp-library"
)
}
fn has_validator(keyword: &str) -> bool {
matches!(keyword,
"rust-single-crate" | "rust-workspace" |
"npm" | "js-pnpm" |
"python" | "python-pdm" |
"helm" |
"go" |
"ruby-gem" |
"ocaml-dune" |
"dotnet-csproj" |
"swift-spm" |
"elixir-mix" |
"scala-sbt" |
"clojure-deps" |
"crystal" |
"dart" |
"composer" |
"julia" |
"zig" |
"fortran-fpm" |
"gleam" |
"racket-info" |
"java-maven" |
"js-deno" |
"cpp-vcpkg" |
"python-conda" |
"ada-alire" |
"java-gradle-kts" |
"cpp-cmake" |
"cpp-meson" |
"cpp-conan" |
"haskell-cabal" |
"nim-nimble" |
"lua-rockspec" |
"r-description" |
"python-pipenv" |
"github-action"
)
}
fn has_green_ci_starter(keyword: &str) -> bool {
matches!(keyword,
"rust-single-crate" | "rust-workspace" |
"go" |
"npm" | "js-pnpm" |
"python" | "python-pdm" |
"helm" |
"ruby-gem" |
"ocaml-dune" |
"dotnet-csproj" |
"swift-spm" |
"elixir-mix" |
"scala-sbt" |
"clojure-deps" |
"crystal" |
"dart" |
"composer" |
"julia" |
"zig" |
"fortran-fpm" |
"gleam" |
"racket-info" |
"java-maven" |
"js-deno" |
"cpp-vcpkg" |
"python-conda" |
"ada-alire" |
"java-gradle-kts" |
"cpp-cmake" |
"cpp-meson" |
"cpp-conan" |
"haskell-cabal" |
"nim-nimble" |
"lua-rockspec" |
"r-description" |
"python-pipenv" |
"github-action"
)
}
fn has_url_discover(keyword: &str) -> bool {
matches!(keyword,
"rust-single-crate" | "rust-workspace" | "js-pnpm" | "js-deno" |
"npm" | "python-pipenv" | "python-conda" | "python-pdm" | "python" |
"helm" | "github-action" | "go" | "zig" | "fortran-fpm" |
"java-gradle-kts" | "java-maven" | "scala-sbt" | "clojure-deps" |
"dotnet-csproj" | "swift-spm" | "ocaml-dune" | "haskell-cabal" |
"gleam" | "racket-info" | "elixir-mix" | "ruby-gem" | "lua-rockspec" |
"nim-nimble" | "crystal" | "dart" | "composer" | "julia" |
"r-description" | "ada-alire" | "cpp-conan" | "cpp-vcpkg" |
"cpp-meson" | "cpp-cmake"
)
}
pub fn to_json(caps: &[EcosystemCapabilities]) -> String {
use crate::json_ast::Value;
let items: Vec<Value> = caps.iter().map(|c| {
let mut o = Value::obj();
o.insert("keyword", Value::s(&c.keyword));
o.insert("forge", Value::b(c.forge_dispatch));
o.insert("reverse", Value::b(c.reverse_extractor));
o.insert("validate", Value::b(c.validator));
o.insert("green-ci", Value::b(c.green_ci_starter));
o.insert("url-discover", Value::b(c.url_discover));
o.insert("fully-capable", Value::b(c.fully_capable()));
o.insert("preserved-fields",
Value::Array(c.preserved_fields.iter().map(Value::s).collect()));
o.insert("preserved-field-count", Value::i(c.preserved_fields.len() as i64));
o
}).collect();
crate::json_ast::render(&Value::Array(items))
}
pub fn to_text_table(caps: &[EcosystemCapabilities]) -> String {
let mut out = String::new();
out.push_str("ecosystem forge rev val gci url fully fields\n");
out.push_str("─────────────────────── ───── ─── ─── ─── ─── ──────── ──────\n");
for c in caps {
let line = format!(
"{:23} {:5} {:3} {:3} {:3} {:3} {:8} {}\n",
truncate(&c.keyword, 23),
yn(c.forge_dispatch),
yn(c.reverse_extractor),
yn(c.validator),
yn(c.green_ci_starter),
yn(c.url_discover),
if c.fully_capable() { "✓ full" } else { "" },
c.preserved_fields.len(),
);
out.push_str(&line);
}
let totals = capability_totals(caps);
out.push_str(&format!(
"\n{} ecosystems total · {} fully-capable · forge={} reverse={} \
validate={} green-ci={} url-disc={}\n",
caps.len(),
caps.iter().filter(|c| c.fully_capable()).count(),
totals.get("forge").copied().unwrap_or(0),
totals.get("reverse").copied().unwrap_or(0),
totals.get("validate").copied().unwrap_or(0),
totals.get("green-ci").copied().unwrap_or(0),
totals.get("url-disc").copied().unwrap_or(0),
));
out
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max { return s.to_string(); }
s.chars().take(max).collect()
}
fn yn(b: bool) -> &'static str { if b { "yes" } else { "no" } }
fn capability_totals(caps: &[EcosystemCapabilities]) -> BTreeMap<&'static str, usize> {
let mut m = BTreeMap::new();
m.insert("forge", caps.iter().filter(|c| c.forge_dispatch).count());
m.insert("reverse", caps.iter().filter(|c| c.reverse_extractor).count());
m.insert("validate", caps.iter().filter(|c| c.validator).count());
m.insert("green-ci", caps.iter().filter(|c| c.green_ci_starter).count());
m.insert("url-disc", caps.iter().filter(|c| c.url_discover).count());
m
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_known_ecosystem_returns_typed_capabilities() {
let c = query("rust-single-crate");
assert!(c.forge_dispatch);
assert!(c.reverse_extractor);
assert!(c.validator);
assert!(c.green_ci_starter);
assert!(c.url_discover);
assert!(c.fully_capable(), "rust-single-crate should be fully-capable");
}
#[test]
fn query_unknown_ecosystem_returns_all_false() {
let c = query("some-future-ecosystem-not-yet-supported");
assert!(!c.forge_dispatch);
assert!(!c.reverse_extractor);
assert!(!c.validator);
assert!(!c.green_ci_starter);
assert!(!c.url_discover);
assert!(!c.fully_capable());
}
#[test]
fn query_all_returns_full_ecosystem_set() {
let all = query_all();
assert!(all.len() >= 36, "expected ≥ 36 ecosystems, got {}", all.len());
assert_eq!(all[0].keyword, ALL_ECOSYSTEMS[0]);
}
#[test]
fn fully_capable_set_includes_the_4_green_ci_ecosystems() {
let all = query_all();
let fully: Vec<&str> = all.iter()
.filter(|c| c.fully_capable())
.map(|c| c.keyword.as_str())
.collect();
for required in &["rust-single-crate", "rust-workspace", "npm",
"js-pnpm", "python", "python-pdm"] {
assert!(fully.contains(required),
"expected {required} to be fully-capable; got fully={fully:?}");
}
}
#[test]
fn to_text_table_renders_with_headers_and_totals() {
let caps = vec![query("rust-single-crate"), query("go")];
let t = to_text_table(&caps);
assert!(t.contains("ecosystem"));
assert!(t.contains("rust-single-crate"));
assert!(t.contains("go"));
assert!(t.contains("2 ecosystems total"));
assert!(t.contains("fully-capable"));
}
#[test]
fn to_json_emits_typed_capability_records() {
let caps = vec![query("rust-single-crate")];
let j = to_json(&caps);
assert!(j.contains("\"keyword\": \"rust-single-crate\""));
assert!(j.contains("\"forge\": true"));
assert!(j.contains("\"fully-capable\": true"));
}
#[test]
fn all_ecosystems_are_recognised_by_at_least_one_surface() {
for eco in ALL_ECOSYSTEMS {
let c = query(eco);
assert!(
c.forge_dispatch || c.reverse_extractor || c.validator ||
c.green_ci_starter || c.url_discover,
"{eco} is in ALL_ECOSYSTEMS but no substrate surface recognises it"
);
}
}
}