Validation Package ceplas-01-investigation

Language: F#

ceplas; investigation; quality-arc

v1.0.0 released on 08/20/2026

by Dominik Brilhaus; Heinrich Lukas Weil

320 Downloads

Validates whether the ARC contains the minimal metadata to meet the CEPLAS quality criteria only on investigation level.


Install with arc-validate

 arc-validate package install ceplas-01-investigation --version 1.0.0

Include in a PLANTDataHUB CQC pipeline

validation_packages:
  - name: ceplas-01-investigation
    version: 1.0.0

Description

## Critical quality criteria

- ARC contains README

- ARC contains any LICENSE file

- Investigation contains title

- Investigation contains description

- Investigation contains contact

- All investigation contacts contain first name and last name

- At least two investigation contacts contain an affiliation and valid email

## Non-critical quality criteria

- ARC contains README in recommended file format: README.md

- ARC contains LICENSE file in recommended file format: LICENSE

- Every investigation contact should have a valid email

- Every investigation contact should have an affiliation

- Every investigation contact should have an ORCID

- At least one investigation contact should have role 'researcher'

- At least one investigation contact should have role 'principal investigator'


Release notes

Release leveled validation packages.


Browse code for v1.0.0 (fsharp)
let [<Literal>]PACKAGE_METADATA = """(*
---
Name: ceplas-01-investigation
Summary: Validates whether the ARC contains the minimal metadata to meet the CEPLAS quality criteria only on investigation level.
Description: |
    ## Critical quality criteria
    - ARC contains README
    - ARC contains any LICENSE file
    - Investigation contains title
    - Investigation contains description
    - Investigation contains contact
    - All investigation contacts contain first name and last name
    - At least two investigation contacts contain an affiliation and valid email

    ## Non-critical quality criteria
    - ARC contains README in recommended file format: README.md
    - ARC contains LICENSE file in recommended file format: LICENSE
    - Every investigation contact should have a valid email
    - Every investigation contact should have an affiliation
    - Every investigation contact should have an ORCID
    - At least one investigation contact should have role 'researcher'
    - At least one investigation contact should have role 'principal investigator'
MajorVersion: 1
MinorVersion: 0
PatchVersion: 0
Publish: true
Authors:
  - FullName: Dominik Brilhaus
    Email: brilhaus@hhu.de
    Affiliation: CEPLAS
    AffiliationLink: https://ceplas.eu
  - FullName: Heinrich Lukas Weil
    Email: weil@nfdi4plants.org
    Affiliation: RPTU Kaiserslautern
    AffiliationLink: http://rptu.de/startseite
Tags:
  - Name: ceplas
  - Name: investigation
  - Name: quality-arc
ReleaseNotes: |
    Release leveled validation packages.
---
*)"""

#r "nuget: ARCExpect.Core, 7.0.0-alpha"
#r "nuget: ARCtrl.QueryModel, 3.0.0-alpha.4"
#r "nuget: Fable.SimpleHttp"

open ARCtrl
open ARCtrl.QueryModel
open Expecto
open ARCExpect
open System.IO

let emailIsValid (email: string) =
    let pattern = @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"
    System.Text.RegularExpressions.Regex.IsMatch(email, pattern)

// Input:

let arcDir = Directory.GetCurrentDirectory()


////////////////////////

let arc =
    try ARC.load arcDir with
    | _ -> ARC(identifier = "placeholder")

arc.MakeDataFilesAbsolute()
arc.DataContextMapping()



let readmeNames =
    set [
        "README"
        "README.md"
        "README.txt"
        "README.rst"
        "README.adoc"
        "README.asciidoc"
        "README.markdown"
        "README.mdown"
        "README.mkd"
        "README.org"
    ]

let readmeNamesLow = readmeNames |> Seq.map (fun n -> n.ToLowerInvariant()) |> set

let containsReadme =
    Directory.EnumerateFiles arcDir 
    |> Seq.map Path.GetFileName
    |> Seq.map (fun n -> n.ToLowerInvariant())
    |> Seq.exists readmeNamesLow.Contains

let readmeNamesOptions =                
    Set.union readmeNames readmeNamesLow
    |> String.concat ", "


let licenseNames =
    set [
        "LICENSE"
        "LICENSE.md"
        "LICENSE.txt"
        "LICENCE"
        "LICENCE.md"
        "LICENCE.txt"
        ]

let licenseNamesLow = licenseNames |> Seq.map (fun n -> n.ToLowerInvariant()) |> set

let containsLicense =
    Directory.EnumerateFiles(arcDir)
    |> Seq.map Path.GetFileName
    |> Seq.map (fun n -> n.ToLowerInvariant())
    |> Seq.exists licenseNamesLow.Contains

let licenseNamesOptions =                
    Set.union licenseNames licenseNamesLow
    |> String.concat ", " 


// Validations

let criticalCases =     
    testList "criticalCases" [

    ////////////////////////////////////
    ////// ARC root
    ////////////////////////////////////

    // TestCase Critical: ARC contains README

    testCase "ARC contains README" <| fun _ ->
        
        Expect.isTrue containsReadme
            $"""ARC does not contain a README. README.md is recommended. Expected one of: {readmeNamesOptions}"""

    // TestCase Critical: ARC contains any LICENSE file

    testCase "ARC contains LICENSE file" <| fun _ ->

        Expect.isTrue containsLicense
            $"""ARC does not contain a LICENSE file. Expected one of: {licenseNamesOptions}"""

    ////////////////////////////////////
    ////// ARC Investigation
    ////////////////////////////////////        

    // TestCase Critical: Investigation contains title

    testCase $"Investigation '{arc.Identifier}' contains title" <| fun _ ->

        // Investigation title exists        
        Expect.isSome arc.Title
            $"Investigation '{arc.Identifier}' contains no title"

        // Investigation title is longer than 3 characters
        Expect.isGreaterThan arc.Title.Value.Length 3 
            $"Investigation '{arc.Identifier}' contains no meaningful title (i.e. longer than 3 characters):\"{arc.Title.Value}\""       

    // TestCase Critical: Investigation contains description

    testCase $"Investigation '{arc.Identifier}' contains description" <| fun _ ->
        // Investigation description exists
        Expect.isSome arc.Description
            $"Investigation '{arc.Identifier}' contains no description"
        // Investigation description is longer than 30 characters
        Expect.isGreaterThan arc.Description.Value.Length 30
            $"Investigation '{arc.Identifier}' contains no meaningful description (i.e. longer than 30 characters):\"{arc.Description.Value}\""

    // TestCase Critical: Investigation contains contact

    testCase $"Investigation '{arc.Identifier}' contains contact" <| fun _ ->        
        
        Expect.notEqual arc.Contacts.Count 0
            $"Investigation '{arc.Identifier}' contains no contact"
    
    // TestCase Critical: All investigation contacts contain first name and last name

    for c in arc.Contacts |> Seq.distinctBy (fun c -> (c.FirstName, c.LastName)) do

        let fname = Option.defaultValue "" c.FirstName
        let lname = Option.defaultValue "" c.LastName

        let fullName = $"'{fname} {lname}'"

        testCase $"Contact {fullName} contains first name" <| fun _ ->
            Expect.isSome c.FirstName
                $"Contact {fullName} contains no first name"

        testCase $"Contact {fullName} contains last name" <| fun _ ->
            Expect.isSome c.LastName
                $"Contact {fullName} contains no last name"

    // TestCase Critical: At least two investigation contacts contain an affiliation and valid email
    
    testCase "At least two investigation contacts contain an affiliation and valid email" <| fun _ ->

        let validContacts =
            arc.Contacts
            |> Seq.filter (fun c ->
                c.EMail.IsSome
                && emailIsValid c.EMail.Value
                && c.Affiliation.IsSome
            )
            |> Seq.length

        Expect.isGreaterThanOrEqual validContacts 2
            $"Expected at least two contacts with a valid email and affiliation, but found {validContacts}."

    ]
    

let nonCriticalCases =
    testList "nonCriticalCases" [
    
    /////////////////////////////////////////////////////////////////
    ////// ARC Root
    /////////////////////////////////////////////////////////////////

    // TestCase Non-critical: ARC contains README in recommended file format: README.md

    if containsReadme then
        testCase "ARC contains README in recommended file format: README.md" <| fun _ ->

            let containsReadmeMd =
                Directory.EnumerateFiles(arcDir)
                |> Seq.map Path.GetFileName
                |> Seq.contains "README.md"

            Expect.isTrue containsReadmeMd
                "ARC contains README file in recommended file format: README.md"

    // TestCase Non-critical: ARC contains LICENSE file in recommended file format: LICENSE

    if containsLicense then

        testCase "ARC contains LICENSE file in recommended file format: LICENSE" <| fun _ ->
            let containsLICENSE =
                Directory.EnumerateFiles(arcDir)
                |> Seq.map Path.GetFileName
                |> Seq.contains "LICENSE"

            Expect.isTrue containsLICENSE
                $"ARC contains LICENSE file in recommended file format: LICENSE"

    /////////////////////////////////////////////////////////////////
    ////// ARC Investigation metadata
    /////////////////////////////////////////////////////////////////

    for c in arc.Contacts |> Seq.distinctBy (fun c -> (c.FirstName, c.LastName)) do
        let fname = Option.defaultValue "" c.FirstName
        let lname = Option.defaultValue "" c.LastName

        let fullName = $"'{fname} {lname}'"
    
    // TestCase Non-critical: Every investigation contact should have a valid email

        testCase $"Contact {fullName} contains email" <| fun _ ->
            match c.EMail with
            | None ->
                failtest $"Contact {fullName} contains no email"
            | Some email ->
                Expect.isTrue (emailIsValid email) $"'{email}' is not a valid email"
        
    // TestCase Non-critical: Every investigation contact should have an affiliation
        
        testCase $"Contact {fullName} contains affiliation" <| fun _ ->
            Expect.isSome c.Affiliation
                $"Contact {fullName} contains no affiliation"
    
    // TestCase Non-critical: Every investigation contact should have an ORCID
    
        testCase $"Contact {fullName} contains ORCID" <| fun _ ->
            Expect.isSome c.ORCID
                $"Contact {fullName} contains no ORCID"    
    
    // TestCase Non-critical: At least one investigation contact should have role 'researcher'

    let containsResearcher = 
        arc.Contacts
        |> Seq.exists (fun c -> c.Roles |> Seq.exists (fun oa -> 
                oa.NameText = "researcher")
                )
    
    testCase $"At least one investigation contact should have role 'researcher'" <| fun _ ->
        Expect.isTrue containsResearcher
            $"No investigation contact has role 'researcher'"

    // TestCase Non-critical: At least one investigation contact should have role 'principal investigator'
    
    let containsPI = 
        arc.Contacts
        |> Seq.exists (fun c -> c.Roles |> Seq.exists (fun oa -> 
                oa.NameText = "principal investigator")
                )
    
    testCase $"At least one investigation contact should have role 'principal investigator'" <| fun _ ->
        Expect.isTrue containsPI
            $"No investigation contact has role 'principal investigator'"
    
    ]

// Execution:
Setup.ValidationPackage(
    metadata = Setup.Metadata(
        PACKAGE_METADATA,
        AVPRIndex.Frontmatter.FrontmatterLanguage.FSharpFrontmatter
        ),
    CriticalValidationCases = [criticalCases],
    NonCriticalValidationCases = [nonCriticalCases]
)
|> Execute.ValidationPipeline(
    basePath = arcDir
)

Available versions
Version Released on
1.0.0 08/20/2026