check_flashos_operation_map.fsh

#!/usr/bin/env fsh
# Taplo decodes the platform documents and jq exposes field names and bounded
# cross-document projections. ripgrep exposes tracked markers and the live Rust
# enum. Flash owns ABI seam policy, operation ordering, routing, closure, and
# diagnostics.

import { require_jq, require_rg, toml_to_json } from './lib/tools.fsh'
import { repository_root } from './lib/repository.fsh'

def operation_error(message) {
    ^printf 'FlashOS operation map: %s\n' $message 1>&2
    exit 1
}

def nonempty(value, label) {
    if $value == null || $value == '' {
        throw "$label must be a non-empty string"
    }
    return $value
}

def validate_strings(values, label, nonempty_required) {
    if $values == null {
        throw "$label must be a list of non-empty strings"
    }
    if $nonempty_required && $values == [] {
        throw "$label must not be empty"
    }
    for selected in $values {
        if $selected == null || $selected == '' {
            throw "$label must be a list of non-empty strings"
        }
        mut occurrences = 0
        for candidate in $values {
            if $candidate == $selected {
                $occurrences = $occurrences + 1
            }
        }
        if $occurrences != 1 {
            throw "$label contains duplicates"
        }
    }
    return $values
}

def same_set(observed, expected) -> Bool {
    for value in $observed {
        if !($value in $expected) {
            return false
        }
    }
    for value in $expected {
        if !($value in $observed) {
            return false
        }
    }
    return true
}

def validate(bundle) {
    let expected_fields = [
    'abi_seam',
    'architecture',
    'capability_evidence',
    'classification',
    'compiler_source',
    'contract_source',
    'libc_source',
    'operation',
    'platform',
    'platform_baseline',
    'schema_version',
    'selected_adapter',
    'target',
    ]
    if $bundle.fields != $expected_fields {
        throw 'document fields are unexpected'
    }
    let document = $bundle.document
    let expected = {
        schema_version: 2,
        platform: 'flashos',
        architecture: 'x86_64',
        target: 'x86_64-unknown-redox',
        platform_baseline: 'flashos-x86_64.toml',
        capability_evidence: 'flashos-x86_64-capability-evidence.toml',
        contract_source: 'components/flash/crates/flash-platform/src/lib.rs',
        selected_adapter: 'flash-platform-flashos::FlashOsPlatform',
        classification: 'deferred',
    }
    for field in [
    'schema_version',
    'platform',
    'architecture',
    'target',
    'platform_baseline',
    'capability_evidence',
    'contract_source',
    'selected_adapter',
    'classification',
    ] {
        if $document[$field] != $expected[$field] {
            throw "$field does not preserve the operation-map contract"
        }
    }
    if $bundle.baseline.architecture != $document.architecture {
        throw 'architecture does not match the platform baseline'
    }
    if $bundle.baseline.target.triple != $document.target {
        throw 'target does not match the platform baseline'
    }
    if $bundle.compiler_fields != [
    'mapping_scope',
    'repository',
    'revision',
    'selector',
    'selector_kind',
    ] {
        throw 'compiler_source fields are unexpected'
    }
    let expected_compiler = {
        repository: $bundle.baseline.compiler.source,
        selector_kind: $bundle.baseline.compiler.source_selector_kind,
        selector: $bundle.baseline.compiler.source_selector,
        revision: $bundle.baseline.compiler.commit,
        mapping_scope: 'public-std-api',
    }
    if $document.compiler_source != $expected_compiler {
        throw 'compiler_source does not preserve the platform baseline identity'
    }
    if $bundle.libc_fields != ['mapping_revision', 'mapping_scope', 'repository'] {
        throw 'libc_source fields are unexpected'
    }
    let expected_libc = {
        repository: $bundle.baseline.libc.source,
        mapping_revision: $bundle.baseline.libc.configured_revision,
        mapping_scope: 'configured-source',
    }
    if $document.libc_source != $expected_libc {
        throw 'libc_source does not preserve the configured source identity'
    }
    if $bundle.evidence.classification != 'deferred' {
        throw 'capability evidence classification must remain deferred'
    }
    if $bundle.evidence_variants != $bundle.contract_variants {
        throw 'capability evidence no longer matches the live contract enum'
    }
    if $bundle.expected_operations == [] {
        throw 'capability evidence has no capability array'
    }

    if $document.abi_seam == null || $document.abi_seam == [] {
        throw 'abi_seam must be a non-empty array of tables'
    }
    let seam_fields = [
    'id',
    'interfaces',
    'observation',
    'paths',
    'provider',
    'revision',
    'symbols',
    'tracked_markers',
    'tracked_path',
    ]
    mut seam_index = 0
    for seam in $document.abi_seam {
        let label = "abi_seam[$seam_index]"
        if $bundle.seam_fields[$seam_index] != $seam_fields {
            throw "$label fields are unexpected"
        }
        let identifier = nonempty($seam.id, "$label.id")
        mut occurrences = 0
        for candidate in $bundle.seam_ids {
            if $candidate == $identifier {
                $occurrences = $occurrences + 1
            }
        }
        if $occurrences != 1 {
            throw "duplicate abi_seam id '$identifier'"
        }
        let provider = nonempty($seam.provider, "$label.provider")
        let revision = nonempty($seam.revision, "$label.revision")
        nonempty($seam.tracked_path, "$label.tracked_path")
        validate_strings($seam.tracked_markers, "$label.tracked_markers", true)
        let paths = validate_strings($seam.paths, "$label.paths", false)
        validate_strings($seam.interfaces, "$label.interfaces", true)
        let symbols = validate_strings($seam.symbols, "$label.symbols", false)
        nonempty($seam.observation, "$label.observation")
        if $provider == 'rust-std' {
            if $revision != 'unknown' || $paths != [] || $symbols != [] {
                throw "$label must preserve the unknown Rust source boundary without source paths or inferred libc symbols"
            }
        } else if $provider == 'relibc' {
            if $revision != $document.libc_source.mapping_revision || $paths == [] || $symbols == [] {
                throw "$label must use the configured relibc revision with non-empty source paths and ABI symbols"
            }
        } else {
            throw "$label.provider must be 'rust-std' or 'relibc'"
        }
        $seam_index = $seam_index + 1
    }

    if $document.operation == null || $document.operation == [] {
        throw 'operation must be a non-empty array of tables'
    }
    let operation_fields = [
    'abi_seams',
    'boundary',
    'capability',
    'classification',
    'id',
    'mapping_observation',
    'requirement',
    'source_evidence',
    ]
    mut operation_index = 0
    for operation in $document.operation {
        let label = "operation[$operation_index]"
        if $bundle.operation_fields[$operation_index] != $operation_fields {
            throw "$label fields are unexpected"
        }
        let identifier = nonempty($operation.id, "$label.id")
        mut occurrences = 0
        for candidate in $bundle.operation_ids {
            if $candidate == $identifier {
                $occurrences = $occurrences + 1
            }
        }
        if $occurrences != 1 {
            throw "duplicate operation id '$identifier'"
        }
        nonempty($operation.capability, "$label.capability")
        nonempty($operation.requirement, "$label.requirement")
        let evidence_ids = validate_strings(
        $operation.source_evidence,
        "$label.source_evidence",
        true,
        )
        for evidence_id in $evidence_ids {
            if !($evidence_id in $bundle.source_ids) {
                throw "$label references unknown source evidence '$evidence_id'"
            }
        }
        let seam_ids = validate_strings($operation.abi_seams, "$label.abi_seams", false)
        for seam_id in $seam_ids {
            if !($seam_id in $bundle.seam_ids) {
                throw "$label references unknown ABI seam '$seam_id'"
            }
        }
        let boundary = nonempty($operation.boundary, "$label.boundary")
        let providers = $bundle.operation_seam_providers[$operation_index]
        if $boundary in ['flash-internal', 'unrouted'] {
            if $seam_ids != [] {
                throw "$label boundary '$boundary' must not name an ABI seam"
            }
        } else if $boundary == 'rust-std' {
            if $seam_ids == [] || !('rust-std' in $providers) {
                throw "$label rust-std boundary must name a Rust standard-library seam"
            }
        } else if $boundary == 'libc-abi' {
            if $seam_ids == [] || !('relibc' in $providers) {
                throw "$label libc-abi boundary must name a relibc seam"
            }
        } else {
            throw "$label.boundary is not a mapping boundary"
        }
        nonempty($operation.mapping_observation, "$label.mapping_observation")
        if $operation.classification != 'deferred' {
            throw "$label.classification must remain 'deferred'"
        }
        $operation_index = $operation_index + 1
    }
    if $bundle.actual_operations != $bundle.expected_operations {
        throw 'operation sequence does not exactly cover capability requirements'
    }
    if !same_set($bundle.used_seams, $bundle.seam_ids) {
        throw 'unreferenced ABI seam ids'
    }
    return true
}

def validate_seam_files(bundle, root, rg, jq) {
    mut index = 0
    mut seams_remaining = true
    while $seams_remaining {
        let exists = "$(^env $jq --raw-output --argjson index $index '.document.abi_seam | has($index)' $bundle)"
        if $exists != 'true' {
            $seams_remaining = false
            continue
        }
        let label = "abi_seam[$index]"
        let relative = "$(^env $jq --raw-output --argjson index $index '.document.abi_seam[$index].tracked_path' $bundle)"
        if ^printf '%s' $relative | ^env $rg --quiet '(^/|(^|/)\.\.(/|$))' {
            throw "$label.tracked_path must stay inside the repository"
        }
        let path = "$root/$relative"
        if ^test -f $path {
        } else {
            throw "cannot read $relative"
        }
        mut marker_index = 0
        mut markers_remaining = true
        while $markers_remaining {
            let marker_exists = "$(^env $jq --raw-output --argjson index $index --argjson marker $marker_index '.document.abi_seam[$index].tracked_markers | has($marker)' $bundle)"
            if $marker_exists != 'true' {
                $markers_remaining = false
                continue
            }
            let marker = "$(^env $jq --raw-output --argjson index $index --argjson marker $marker_index '.document.abi_seam[$index].tracked_markers[$marker]' $bundle)"
            if ^env $rg --fixed-strings --quiet -- $marker $path {
            } else {
                throw "$label marker is absent from $relative: '$marker'"
            }
            $marker_index = $marker_index + 1
        }
        let provider = "$(^env $jq --raw-output --argjson index $index '.document.abi_seam[$index].provider' $bundle)"
        if $provider == 'relibc' {
            mut path_index = 0
            mut paths_remaining = true
            while $paths_remaining {
                let path_exists = "$(^env $jq --raw-output --argjson index $index --argjson path $path_index '.document.abi_seam[$index].paths | has($path)' $bundle)"
                if $path_exists != 'true' {
                    $paths_remaining = false
                    continue
                }
                let relibc_path = "$(^env $jq --raw-output --argjson index $index --argjson path $path_index '.document.abi_seam[$index].paths[$path]' $bundle)"
                if ^printf '%s' $relibc_path | ^env $rg --quiet '(^/|(^|/)\.\.(/|$))' {
                    throw "$label.paths must stay inside the relibc repository"
                }
                $path_index = $path_index + 1
            }
        }
        $index = $index + 1
    }
}

let root = repository_root('versions.env')
let rg = require_rg()
let jq = require_jq()
mut temporary_parent = env('TMPDIR')
if $temporary_parent == null {
    $temporary_parent = '/tmp'
}
let temporary = "$(^mktemp -d "$temporary_parent/flashos-operation-map.XXXXXX")"
if !$status.ok {
    operation_error('cannot create temporary directory')
}
let errors = "$temporary/errors"
let map = "$temporary/map.json"
let baseline = "$temporary/baseline.json"
let evidence = "$temporary/evidence.json"
let variants = "$temporary/variants.json"
let bundle = "$temporary/bundle.json"
let map_document = toml_to_json(
"$root/components/flash/platforms/flashos-x86_64-operation-map.toml",
$errors,
)
^printf '%s\n' $map_document > $map
let baseline_document = toml_to_json(
"$root/components/flash/platforms/flashos-x86_64.toml",
$errors,
)
^printf '%s\n' $baseline_document > $baseline
let evidence_document = toml_to_json(
"$root/components/flash/platforms/flashos-x86_64-capability-evidence.toml",
$errors,
)
^printf '%s\n' $evidence_document > $evidence
^sed -n '/^pub enum Capability {$/,/^}$/p' "$root/components/flash/crates/flash-platform/src/lib.rs" \
| ^env $rg --only-matching --replace '$1' '^    ([A-Z][A-Za-z0-9]+),$' \
| ^env $jq --raw-input --slurp 'split("\n") | map(select(length > 0))' > $variants
if !$status.ok {
    operation_error('cannot locate the Capability enum')
}
^env $jq \
--slurpfile baseline $baseline \
--slurpfile evidence $evidence \
--slurpfile variants $variants \
'. as $document | {document:$document, fields:(keys|sort), baseline:$baseline[0], evidence:$evidence[0], contract_variants:$variants[0], compiler_fields:(try (.compiler_source|keys|sort) catch []), libc_fields:(try (.libc_source|keys|sort) catch []), evidence_variants:(try [$evidence[0].capability[].rust_variant] catch []), source_ids:(try [$evidence[0].source_evidence[].id] catch []), expected_operations:(try [$evidence[0].capability[] | .name as $capability | .requirements[] | {capability:$capability, requirement:.}] catch []), seam_fields:(try [.abi_seam[]|(keys|sort)] catch []), seam_ids:(try [.abi_seam[].id] catch []), operation_fields:(try [.operation[]|(keys|sort)] catch []), operation_ids:(try [.operation[].id] catch []), actual_operations:(try [.operation[]|{capability,requirement}] catch []), used_seams:(try [.operation[].abi_seams[]] catch []), operation_seam_providers:(try [.operation[] | [.abi_seams[] as $id | $document.abi_seam[] | select(.id == $id) | .provider]] catch [])}' \
$map > $bundle 2> $errors
if !$status.ok {
    operation_error('cannot project the operation-map contract')
}
try {
    open $bundle | from json | each {|document| validate($document)} | to json > /dev/null
} catch error {
    let message = $error.message
    ^rm -rf $temporary
    operation_error($message)
}
try {
    validate_seam_files($bundle, $root, $rg, $jq)
} catch error {
    let message = $error.message
    ^rm -rf $temporary
    operation_error($message)
}
if ^env $rg --fixed-strings --quiet -- 'build/flash-bootstrap/134635a5e1282b5d8455a4b2aeb754be5a3a77c1/fsh ci/check_flashos_operation_map.fsh' "$root/.github/workflows/ci.yml" {
} else {
    ^rm -rf $temporary
    operation_error('standard CI does not validate the FlashOS operation map')
}
^rm -rf $temporary
^printf 'FlashOS operation map: mapping contract passed for x86_64-unknown-redox\n'

Return to documentation →