///|
pub(all) enum DiscoveryRole {
Directory
Data
Sidecar
Auxiliary
Excluded
} derive(Eq)
///|
pub extend DiscoveryRole with Eq::{equal, not_equal}
///|
/// Classify an inventory entry before reading it. Unsupported candidates fail;
/// exclusions are explicit profile boundaries, never implicit .bidsignore rules.
pub fn classify_discovery_entry(
path : String,
directory~ : Bool,
) -> DiscoveryRole raise TraceError {
let components = parts(path)
if ["derivatives", "sourcedata", "code", "stimuli"].contains(components[0]) ||
components.any(c => c.has_prefix(".")) {
return Excluded
}
if directory {
return Directory
}
let name = components[components.length() - 1]
if name.has_suffix(".nii") || name.has_suffix(".nii.gz") {
selector(path, sidecar=false) |> ignore
return Data
}
let root_auxiliary = components.length() == 1 &&
[
"dataset_description.json", "participants.json", "participants.tsv", "README",
"README.md", "README.txt", "LICENSE", "LICENSE.txt", "CHANGES", "CHANGES.md",
"CHANGES.txt",
].contains(name)
let table_auxiliary = name.has_suffix(".tsv") ||
name.has_suffix(".tsv.gz") ||
name.has_suffix(".bval") ||
name.has_suffix(".bvec")
let json_auxiliary = ["events", "scans", "sessions", "participants"].any(suffix => {
name == suffix + ".json" || name.has_suffix("_" + suffix + ".json")
})
if root_auxiliary || table_auxiliary || json_auxiliary {
return Auxiliary
}
if name.has_suffix(".json") {
selector(path, sidecar=true) |> ignore
return Sidecar
}
reject(
"unsupported_inventory_file", path, "File is outside the documented discovery profile; no complete snapshot was returned. Select a supported dataset tree or extend the profile explicitly.",
)
Auxiliary
}
///|
pub struct DiscoveryReport {
priv index : DatasetIndex
priv auxiliary : Array[String]
priv excluded : Array[String]
}
///|
/// The caller supplies a full inventory of regular files and explicit excluded
/// subtree paths, plus text for every classified MRI sidecar. Image bytes unused.
pub fn DiscoveryReport::from_inventory(
paths : Array[String],
sidecars : Array[SidecarInput],
) -> DiscoveryReport raise TraceError {
if paths.length() > 20000 {
reject(
"inventory_limit", "", "Inventory exceeds 20,000 entries; select a smaller supported tree.",
)
}
let seen : Map[String, String] = Map([])
let files : Array[String] = []
let expected : Map[String, Bool] = Map([])
let auxiliary : Array[String] = []
let excluded : Array[String] = []
for path in paths {
if seen.get(path.to_lower()) is Some(existing) {
reject(
"path_collision",
path,
"Inventory contains a repeated or case-colliding entry.",
related=[existing],
)
}
seen[path.to_lower()] = path
match classify_discovery_entry(path, directory=false) {
Data => files.push(path)
Sidecar => expected[path] = true
Auxiliary => auxiliary.push(path)
Excluded => excluded.push(path)
Directory => abort("file classifier invariant")
}
}
let provided : Map[String, Bool] = Map([])
for input in sidecars {
if !expected.contains(input.path) {
reject(
"unexpected_sidecar_text",
input.path,
"Source text must correspond to a classified MRI sidecar in the inventory.",
)
}
if provided.contains(input.path) {
reject(
"path_collision",
input.path,
"Sidecar source text was supplied more than once.",
)
}
provided[input.path] = true
}
for path in sorted_keys(expected) {
if !provided.contains(path) {
reject(
"missing_sidecar_text", path, "Read this inventoried sidecar before constructing the snapshot; no partial result was returned.",
)
}
}
let index = DatasetIndex::from_manifest(files, sidecars)
auxiliary.sort_by(lexical_compare)
excluded.sort_by(lexical_compare)
{ index, auxiliary, excluded, }
}
///|
pub fn DiscoveryReport::index(self : DiscoveryReport) -> DatasetIndex {
self.index
}
///|
pub fn DiscoveryReport::auxiliary_paths(
self : DiscoveryReport,
) -> Array[String] {
self.auxiliary.copy()
}
///|
pub fn DiscoveryReport::excluded_paths(self : DiscoveryReport) -> Array[String] {
self.excluded.copy()
}
///|
pub fn DiscoveryReport::to_json(self : DiscoveryReport) -> String {
canonical_json(
Json::object({
"profile": Json::string("raw_mri_v1"),
"status": Json::string("complete_supported_inventory"),
"manifest": self.index.manifest_value(),
"auxiliary_paths": Json::array(self.auxiliary.map(Json::string)),
"excluded_paths": Json::array(self.excluded.map(Json::string)),
}),
)
}