/source/thepeoples-io/blob/scripts/build-apex-catalog.sh/

build-apex-catalog.sh

path scripts/build-apex-catalog.shlanguage bashbytes 64369source copy public-copy-2026-05-24

#!/usr/bin/env bash
# Validate catalog inputs, generate Zola content, and build the apex site.

set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/.." && pwd)"
zola_pin_file="${repo_root}/tools/zola-version.sh"

# shellcheck disable=SC1090
source "$zola_pin_file"

site_root="${repo_root}/sites/apex-src"
content_root="${site_root}/content"
static_root="${site_root}/static"
projects_content_dir="${content_root}/projects"
catalog_root="${repo_root}/sites/catalog"
fixture_root="${CATALOG_FIXTURE_ROOT:-${catalog_root}/fixtures/projects}"
generated_root="${CATALOG_GENERATED_ROOT:-${catalog_root}/generated}"
design_content_dir="${content_root}/designs"
source_content_dir="${content_root}/source"
verify_content_dir="${content_root}/verify"
source_snapshot_root="${CATALOG_SOURCE_SNAPSHOT_ROOT:-${catalog_root}/source-snapshots}"
registry_projects="${PROJECT_REGISTRY_FILE:-${repo_root}/sites/registry/projects.json}"
registry_sites="${SITE_REGISTRY_FILE:-${repo_root}/sites/registry/static-sites.json}"
production="${PRODUCTION_BUILD:-true}"
use_container="${USE_ZOLA_CONTAINER:-true}"
max_source_file_bytes="${MAX_SOURCE_FILE_BYTES:-200000}"
zola_binary=""

fail() {
    printf '[ERROR] %s\n' "$*" >&2
    exit 1
}

log() {
    printf '[INFO] %s\n' "$*"
}

require_command() {
    command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}

json_string() {
    local file="$1"
    local expr="$2"
    jq -er "$expr | strings" "$file"
}

html_escape() {
    jq -Rr @html
}

path_depth() {
    local path="$1"
    if [[ -z "$path" ]]; then
        printf '0'
    else
        awk -F/ '{print NF - 1}' <<< "$path"
    fi
}

parent_dir_path() {
    local path="$1"
    if [[ "$path" == */* ]]; then
        printf '%s' "${path%/*}"
    else
        printf ''
    fi
}

plural() {
    local count="$1"
    local singular="$2"
    if [[ "$count" == "1" ]]; then
        printf '1 %s' "$singular"
    else
        printf '%s %ss' "$count" "$singular"
    fi
}

dir_summary() {
    local dir="$1"
    local dir_count file_count
    dir_count="$(find "$dir" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
    file_count="$(find "$dir" -mindepth 1 -maxdepth 1 -type f | wc -l | tr -d ' ')"
    if [[ "$dir_count" == "0" && "$file_count" == "0" ]]; then
        printf 'empty'
    elif [[ "$dir_count" == "0" ]]; then
        plural "$file_count" "file"
    elif [[ "$file_count" == "0" ]]; then
        plural "$dir_count" "dir"
    else
        printf '%s / %s' "$(plural "$dir_count" "dir")" "$(plural "$file_count" "file")"
    fi
}

source_breadcrumb_html() {
    local slug="$1"
    local kind="$2"
    local rel_path="$3"
    local accum part i
    local -a parts

    printf '<nav class="source-breadcrumb" aria-label="Source path">\n'
    printf '<a href="/source/%s/">source</a><span>/</span><a href="/source/%s/">%s</a>\n' "$slug" "$slug" "$slug"
    [[ -z "$rel_path" ]] && {
        printf '</nav>\n'
        return
    }

    IFS='/' read -r -a parts <<< "$rel_path"
    accum=""
    for i in "${!parts[@]}"; do
        part="${parts[$i]}"
        [[ -n "$part" ]] || continue
        if [[ -n "$accum" ]]; then
            accum="${accum}/${part}"
        else
            accum="$part"
        fi
        printf '<span>/</span>'
        if [[ "$i" == "$((${#/*parts[@]} - 1))" ]]; then
            if [[ "$kind" == "tree" ]]; then
                printf '<span>%s/</span>\n' "$(printf '%s' "$part" | html_escape)"
            else
                printf '<span>%s</span>\n' "$(printf '%s' "$part" | html_escape)"
            fi
        else
            printf '<a href="/source/%s/tree/%s/">%s</a>\n' "$slug" "$accum" "$(printf '%s' "$part" | html_escape)"
        fi
    done
    printf '</nav>\n'
}

escape_zola_shortcode_markers() {
    sed \
        -e 's/{{ /{{/*\/\*/g' \
        -e 's/ }}/\*\/*/}}/g' \
        -e 's/{% /{%/*\/\*/g' \
        -e 's/ %}/\*\/*/%}/g' \
        -e 's/{# /{#/*\/\*/g' \
        -e 's/ #}/\*\/*/#}/g'
}

detect_language() {
    case "$1" in
        *.bash|*.sh) printf 'bash' ;;
        *.html|*.htm) printf 'html' ;;
        *.md|*.markdown) printf 'md' ;;
        *.json) printf 'json' ;;
        *.toml) printf 'toml' ;;
        *.tf) printf 'hcl' ;;
        *.yml|*.yaml) printf 'yaml' ;;
        *) printf 'text' ;;
    esac
}

validate_no_htmlish_text() {
    local file="$1"
    if jq -er '.. | strings | select(test("<[A-Za-z!/][^>]*>"))' "$file" >/dev/null; then
        fail "HTML-like content is not allowed in manifest strings: ${file}"
    fi
}

validate_registry_sites() {
    jq -e '
      (keys - ["schema_version", "sites"] | length == 0)
      and .schema_version == 1
      and (.sites | type == "array")
      and (.sites | length > 0)
      and all(.sites[]; (
        (keys - [
          "site_id",
          "aliases",
          "bucket",
          "cloudfront_distribution_id",
          "openbao_role",
          "allowed_source_repository",
          "default_build_dir"
        ] | length == 0)
        and (.site_id | test("^[a-z0-9][a-z0-9-]*$"))
        and (.aliases | type == "array" and length > 0)
        and (.bucket | type == "string" and length > 0)
        and (.cloudfront_distribution_id | type == "string" and length > 0)
        and (.openbao_role | type == "string" and length > 0)
        and (.allowed_source_repository | type == "string" and length > 0)
        and (.default_build_dir | type == "string" and length > 0)
      ))
    ' "$registry_sites" >/dev/null || fail "invalid static site registry: ${registry_sites}"
}

validate_registry_projects() {
    jq -e '
      (keys - ["schema_version", "projects"] | length == 0)
      and .schema_version == 1
      and (.projects | type == "array")
      and (.projects | length > 0)
      and all(.projects[]; . as $project | (
        (keys - [
          "project_id",
          "slug",
          "allowed_source_repository",
          "artifact_prefix",
          "openbao_role",
          "mirror_urls",
          "visibility"
        ] | length == 0)
        and (.project_id | test("^[a-z0-9][a-z0-9-]*$"))
        and (.slug | test("^[a-z0-9][a-z0-9-]*$"))
        and (.allowed_source_repository | type == "string" and length > 0)
        and (.artifact_prefix | startswith("projects/" + $project.project_id + "/"))
        and (.openbao_role | type == "string" and length > 0)
        and (.mirror_urls | type == "array")
        and (.visibility == "public" or .visibility == "docs-only" or .visibility == "private" or .visibility == "planned")
      ))
    ' "$registry_projects" >/dev/null || fail "invalid project registry: ${registry_projects}"
}

registry_project() {
    local project_id="$1"
    jq -er --arg project_id "$project_id" '.projects[] | select(.project_id == $project_id)' "$registry_projects"
}

validate_project_manifest() {
    local file="$1"
    validate_no_htmlish_text "$file"
    jq -e '
      (keys - [
        "schema_version",
        "project_id",
        "slug",
        "name",
        "summary",
        "summary_long",
        "status",
        "license",
        "visibility",
        "owner",
        "contact",
        "mirror_urls",
        "docs_entrypoints",
        "logo",
        "accent",
        "screenshots",
        "featured",
        "sort_weight",
        "external_links",
        "public_source_snapshot",
        "public_docs_snapshot",
        "topics"
      ] | length == 0)
      and .schema_version == 1
      and (.project_id | test("^[a-z0-9][a-z0-9-]*$"))
      and (.slug | test("^[a-z0-9][a-z0-9-]*$"))
      and (.name | type == "string" and length > 0)
      and (.summary | type == "string" and length > 0)
      and (.status == "active" or .status == "experimental" or .status == "planned" or .status == "archived" or .status == "infrastructure")
      and ((has("license") | not) or (.license | type == "string" and length > 0))
      and (.visibility == "public" or .visibility == "docs-only" or .visibility == "private" or .visibility == "planned")
      and (.topics | type == "array" and length > 0)
      and all(.topics[]; test("^[a-z0-9][a-z0-9-]*$"))
      and ((.visibility != "docs-only") or (has("public_source_snapshot") | not))
      and ((.visibility != "docs-only") or has("public_docs_snapshot"))
      and ((has("public_docs_snapshot") | not) or (
        (.public_docs_snapshot | keys - ["snapshot_id", "manifest_path", "display"] | length == 0)
        and (.public_docs_snapshot.snapshot_id | type == "string" and length > 0)
        and (.public_docs_snapshot.manifest_path | type == "string" and length > 0)
        and .public_docs_snapshot.display == "project-docs"
      ))
    ' "$file" >/dev/null || fail "invalid project manifest: ${file}"

    local project_id
    project_id="$(json_string "$file" '.project_id')"
    registry_project "$project_id" >/dev/null || fail "project not allowlisted in registry: ${project_id}"
}

validate_release_manifest() {
    local file="$1"
    local project_id
    local version
    local artifact_prefix
    local release_prefix

    validate_no_htmlish_text "$file"
    jq -e '
      (keys - [
        "schema_version",
        "project_id",
        "version",
        "commit",
        "built_at",
        "changelog",
        "artifacts",
        "sbom",
        "signatures",
        "attestations",
        "provenance"
      ] | length == 0)
      and .schema_version == 1
      and (.project_id | test("^[a-z0-9][a-z0-9-]*$"))
      and (.version | test("^v?[0-9][A-Za-z0-9._-]*$"))
      and (.commit | test("^[0-9a-f]{40}$"))
      and (.built_at | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"))
      and (.changelog | type == "string")
      and (.artifacts | type == "array")
      and (.signatures | type == "array")
      and (.attestations | type == "array")
      and ((has("provenance") | not) or (.provenance | type == "array"))
      and all((.artifacts + .signatures + .attestations + (.provenance // []))[]?; (
        (keys - ["name", "path", "sha256", "content_type"] | length == 0)
        and (.name | type == "string" and length > 0)
        and (.path | type == "string" and length > 0)
        and (.sha256 | test("^[0-9a-f]{64}$"))
        and (.content_type | type == "string" and length > 0)
      ))
      and ((has("sbom") | not) or (
        (.sbom | keys - ["format", "path", "sha256"] | length == 0)
        and (.sbom.format == "spdx-json" or .sbom.format == "cyclonedx-json")
        and (.sbom.path | type == "string" and length > 0)
        and (.sbom.sha256 | test("^[0-9a-f]{64}$"))
      ))
    ' "$file" >/dev/null || fail "invalid release manifest: ${file}"

    project_id="$(json_string "$file" '.project_id')"
    version="$(json_string "$file" '.version')"
    artifact_prefix="$(registry_project "$project_id" | jq -er '.artifact_prefix')"
    release_prefix="${artifact_prefix}releases/${version}/"

    jq -e --arg prefix "$release_prefix" '
      all((.artifacts + .signatures + .attestations + (.provenance // []))[]?; .path | startswith($prefix))
      and ((has("sbom") | not) or (.sbom.path | startswith($prefix)))
    ' "$file" >/dev/null || fail "release references artifact outside immutable release prefix: ${project_id} ${version}"
}

path_matches_pattern() {
    local rel="$1"
    local pattern="$2"

    case "$pattern" in
        */)
            [[ "$rel" == "${pattern}"* ]]
            ;;
        */*)
            [[ "$rel" == $pattern ]]
            ;;
        *)
            [[ "$rel" == $pattern || "$rel" == */"$pattern" ]]
            ;;
    esac
}

path_is_listed() {
    local rel="$1"
    local list_file="$2"
    local pattern

    while IFS= read -r pattern; do
        [[ -n "$pattern" ]] || continue
        if path_matches_pattern "$rel" "$pattern"; then
            return 0
        fi
    done < "$list_file"

    return 1
}

verify_release_digests() {
    local file="$1"
    local release_dir
    release_dir="$(dirname "$file")"

    jq -r '(.artifacts + .signatures + .attestations + (.provenance // []))[]? | [.path, .sha256] | @tsv' "$file" |
        while IFS=$'\t' read -r path expected; do
            local artifact="${release_dir}/$(basename "$path")"
            [[ -f "$artifact" ]] || fail "declared artifact missing: ${artifact}"
            actual="$(sha256sum "$artifact" | awk '{print $1}')"
            [[ "$actual" == "$expected" ]] || fail "digest mismatch for ${artifact}"
        done

    if jq -e 'has("sbom")' "$file" >/dev/null; then
        local sbom_path
        local expected
        local actual
        sbom_path="$(jq -er '.sbom.path' "$file")"
        expected="$(jq -er '.sbom.sha256' "$file")"
        artifact="${release_dir}/$(basename "$sbom_path")"
        [[ -f "$artifact" ]] || fail "declared SBOM missing: ${artifact}"
        actual="$(sha256sum "$artifact" | awk '{print $1}')"
        [[ "$actual" == "$expected" ]] || fail "digest mismatch for ${artifact}"
    fi
}

reset_generated_content() {
    mkdir -p "$projects_content_dir" "$design_content_dir" "$source_content_dir" "$generated_root" "$static_root"
    find "$projects_content_dir" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
    find "$source_content_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
    find "$generated_root" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
    find "$design_content_dir" -mindepth 1 -maxdepth 1 -type f -name '*.md' ! -name '_index.md' -delete
    rm -f "${content_root}/search.md" "${static_root}/search-index.json"
}

append_search_entry() {
    local output="$1"
    local type="$2"
    local title="$3"
    local url="$4"
    local summary="$5"
    local tags="${6:-}"

    jq -cn \
        --arg type "$type" \
        --arg title "$title" \
        --arg url "$url" \
        --arg summary "$summary" \
        --arg tags "$tags" \
        '{type: $type, title: $title, url: $url, summary: $summary, tags: ($tags | split(" ") | map(select(length > 0)))}' \
        >> "$output"
}

project_release_count() {
    local project_id="$1"
    if [[ -d "${fixture_root}/${project_id}/releases" ]]; then
        find "${fixture_root}/${project_id}/releases" -name release.json -print | wc -l | tr -d ' '
    else
        printf '0'
    fi
}

project_is_rendered() {
    local project_file="$1"
    local visibility
    visibility="$(json_string "$project_file" '.visibility')"
    [[ "$production" != "true" || "$visibility" == "public" || "$visibility" == "docs-only" ]]
}

project_source_root() {
    local project_file="$1"
    local manifest root_rel root_abs snapshot_abs

    jq -e 'has("public_source_snapshot")' "$project_file" >/dev/null || return 1
    manifest="${repo_root}/$(json_string "$project_file" '.public_source_snapshot.manifest_path')"
    [[ -f "$manifest" ]] || return 1
    root_rel="$(json_string "$manifest" '.root')"
    root_abs="$(realpath -m "${repo_root}/${root_rel}")"
    snapshot_abs="$(realpath -m "$source_snapshot_root")"
    [[ "$root_abs" == "$snapshot_abs"/* ]] || return 1
    printf '%s\n' "$root_abs"
}

project_docs_root() {
    local project_file="$1"
    local manifest root_rel root_abs snapshot_abs

    jq -e 'has("public_docs_snapshot")' "$project_file" >/dev/null || return 1
    manifest="${repo_root}/$(json_string "$project_file" '.public_docs_snapshot.manifest_path')"
    [[ -f "$manifest" ]] || return 1
    root_rel="$(json_string "$manifest" '.root')"
    root_abs="$(realpath -m "${repo_root}/${root_rel}")"
    snapshot_abs="$(realpath -m "$source_snapshot_root")"
    [[ "$root_abs" == "$snapshot_abs"/* ]] || return 1
    printf '%s\n' "$root_abs"
}

detect_license_id() {
    local license_file="$1"

    if grep -Eiq 'GNU AFFERO GENERAL PUBLIC LICENSE|Affero General Public License' "$license_file"; then
        printf 'AGPL-3.0-only'
    elif grep -Eiq 'GNU GENERAL PUBLIC LICENSE' "$license_file"; then
        printf 'GPL-3.0-only'
    elif grep -Eiq 'Mozilla Public License Version 2\.0|MPL-2\.0' "$license_file"; then
        printf 'MPL-2.0'
    elif grep -Eiq 'Apache License.*Version 2\.0|Apache-2\.0' "$license_file"; then
        printf 'Apache-2.0'
    elif grep -Eiq 'MIT License|Permission is hereby granted, free of charge' "$license_file"; then
        printf 'MIT'
    else
        printf 'LICENSE.txt'
    fi
}

project_license() {
    local project_file="$1"
    local root_abs license_file

    if root_abs="$(project_source_root "$project_file")"; then
        license_file="${root_abs}/LICENSE.txt"
        [[ -f "$license_file" ]] && detect_license_id "$license_file" && return 0
    fi

    printf 'AGPL-3.0-only'
}

write_project_pages() {
    local project_file="$1"
    local project_id slug name summary summary_long status license visibility snapshot_id docs_snapshot_id release_count
    project_id="$(json_string "$project_file" '.project_id')"
    slug="$(json_string "$project_file" '.slug')"
    name="$(json_string "$project_file" '.name')"
    summary="$(json_string "$project_file" '.summary')"
    summary_long="$(jq -er '.summary_long // .summary' "$project_file")"
    status="$(json_string "$project_file" '.status')"
    license="$(project_license "$project_file")"
    visibility="$(json_string "$project_file" '.visibility')"
    snapshot_id="$(jq -er '.public_source_snapshot.snapshot_id // "none"' "$project_file")"
    docs_snapshot_id="$(jq -er '.public_docs_snapshot.snapshot_id // "none"' "$project_file")"
    release_count="$(project_release_count "$project_id")"

    if ! project_is_rendered "$project_file"; then
        log "Skipping non-public project in production build: ${project_id}"
        return
    fi

    local project_dir="${projects_content_dir}/${slug}"
    mkdir -p "${project_dir}/releases" "${project_dir}/docs"

    {
        printf '+++\n'
        printf 'title = %s\n' "$(jq -n --arg v "$name" '$v')"
        printf 'description = %s\n' "$(jq -n --arg v "$summary" '$v')"
        printf '[extra]\nkind = "project"\n'
        printf '+++\n\n'
        printf '<p class="lede">%s</p>\n\n' "$summary"
        printf '<div class="project-console">\n'
        printf '<section class="project-console__main">\n'
        printf '<p>%s</p>\n\n' "$summary_long"
        printf '<nav class="action-row" aria-label="Project actions">\n'
        if [[ "$visibility" != "docs-only" ]] && jq -e 'has("public_source_snapshot")' "$project_file" >/dev/null; then
            printf '<a href="/source/%s/">open source browser</a>\n' "$slug"
        fi
        if jq -e 'has("public_docs_snapshot")' "$project_file" >/dev/null; then
            printf '<a href="/projects/%s/docs/">open documentation</a>\n' "$slug"
        fi
        if [[ "$release_count" == "0" ]]; then
            printf '<a href="/projects/%s/releases/">release status</a>\n' "$slug"
        else
            printf '<a href="/projects/%s/releases/">view releases</a>\n' "$slug"
        fi
        printf '<a href="/verify/provenance/">check build inputs</a>\n'
        printf '<a href="/search/">search catalog</a>\n'
        printf '</nav>\n'
        printf '</section>\n'
        printf '<aside class="fact-block project-console__facts">\n'
        printf '<h2>Project</h2>\n'
        printf '<dl class="meta-list">\n'
        printf '<dt>project</dt><dd><code>%s</code></dd>\n' "$project_id"
        printf '<dt>status</dt><dd><code>%s</code></dd>\n' "$status"
        printf '<dt>license</dt><dd><code>%s</code></dd>\n' "$license"
        printf '<dt>visibility</dt><dd><code>%s</code></dd>\n' "$visibility"
        if [[ "$snapshot_id" != "none" ]]; then
            printf '<dt>source copy</dt><dd><code>%s</code></dd>\n' "$snapshot_id"
        fi
        if [[ "$docs_snapshot_id" != "none" ]]; then
            printf '<dt>docs copy</dt><dd><code>%s</code></dd>\n' "$docs_snapshot_id"
        fi
        if [[ "$release_count" == "0" ]]; then
            printf '<dt>releases</dt><dd><code>none</code></dd>\n'
        else
            printf '<dt>releases</dt><dd><code>%s</code></dd>\n' "$release_count"
        fi
        printf '</dl>\n'
        printf '</aside>\n'
        printf '</div>\n\n'
        if jq -e 'has("public_docs_snapshot")' "$project_file" >/dev/null; then
            write_project_docs_readme "$project_file"
            write_project_docs_listing "$project_file"
        else
            write_project_readme "$project_file"
        fi
        printf '## Design notes\n\n'
        printf '<ul class="link-list compact">\n'
        jq -r '.docs_entrypoints[]? | "<li><a href=\"" + .url + "\">" + .title + "</a></li>"' "$project_file"
        printf '</ul>\n'
    } > "${project_dir}/_index.md"

    {
        printf '+++\n'
        printf 'title = "%s Releases"\n' "$name"
        printf 'sort_by = "title"\n'
        printf '[extra]\nkind = "releases"\n'
        if [[ "$release_count" == "0" ]]; then
            printf '+++\n\n'
            printf 'No public releases have been published yet.\n'
        else
            printf '+++\n'
        fi
    } > "${project_dir}/releases/_index.md"

    if [[ -d "${fixture_root}/${project_id}/releases" ]]; then
        find "${fixture_root}/${project_id}/releases" -name release.json -print | sort |
            while IFS= read -r release_file; do
                validate_release_manifest "$release_file"
                verify_release_digests "$release_file"
                write_release_page "$project_file" "$release_file" "${project_dir}/releases"
            done
    fi

    write_project_docs_pages "$project_file" "${project_dir}/docs"
}

write_project_readme() {
    local project_file="$1"
    local manifest root_abs readme_file

    jq -e 'has("public_source_snapshot")' "$project_file" >/dev/null || return 0
    manifest="${repo_root}/$(json_string "$project_file" '.public_source_snapshot.manifest_path')"
    [[ -f "$manifest" ]] || return 0
    root_abs="${repo_root}/$(json_string "$manifest" '.root')"
    readme_file="${root_abs}/README.md"
    write_readme_section "$readme_file" "project-readme"
}

write_project_docs_readme() {
    local project_file="$1"
    local root_abs

    root_abs="$(project_docs_root "$project_file")" || return 0
    write_readme_section "${root_abs}/README.md" "project-readme"
}

write_project_docs_listing() {
    local project_file="$1"
    local root_abs docs_dir slug

    root_abs="$(project_docs_root "$project_file")" || return 0
    docs_dir="${root_abs}/docs"
    [[ -d "$docs_dir" ]] || return 0
    slug="$(json_string "$project_file" '.slug')"

    printf '## Documentation\n\n'
    printf '<ul class="link-list compact">\n'
    find "$docs_dir" -type f \( -name '*.md' -o -name '*.markdown' \) -print | sort |
        while IFS= read -r doc_file; do
            local rel doc_slug title
            rel="${doc_file#${docs_dir}/}"
            doc_slug="${rel%.*}"
            title="$(sed -n '1s/^# //p' "$doc_file")"
            [[ -n "$title" ]] || title="$rel"
            printf '<li><a href="/projects/%s/docs/%s/">%s</a></li>\n' "$slug" "$doc_slug" "$(printf '%s' "$title" | html_escape)"
        done
    printf '</ul>\n\n'
}

write_project_docs_pages() {
    local project_file="$1"
    local docs_content_dir="$2"
    local root_abs docs_dir name slug manifest project_id

    jq -e 'has("public_docs_snapshot")' "$project_file" >/dev/null || return 0
    project_id="$(json_string "$project_file" '.project_id')"
    manifest="${repo_root}/$(json_string "$project_file" '.public_docs_snapshot.manifest_path')"
    [[ -f "$manifest" ]] || fail "public docs snapshot manifest missing: ${manifest}"
    validate_docs_snapshot "$manifest"
    [[ "$(json_string "$manifest" '.project_id')" == "$project_id" ]] ||
        fail "public docs snapshot project_id does not match project manifest: ${project_id}"
    [[ "$(json_string "$manifest" '.snapshot_id')" == "$(json_string "$project_file" '.public_docs_snapshot.snapshot_id')" ]] ||
        fail "public docs snapshot id does not match project manifest: ${project_id}"
    root_abs="$(project_docs_root "$project_file")" || fail "public docs snapshot root missing"
    docs_dir="${root_abs}/docs"
    name="$(json_string "$project_file" '.name')"
    slug="$(json_string "$project_file" '.slug')"
    mkdir -p "$docs_content_dir"

    {
        printf '+++\n'
        printf 'title = "%s Documentation"\n' "$name"
        printf 'sort_by = "title"\n'
        printf '[extra]\nkind = "docs"\n'
        printf '+++\n\n'
        write_project_docs_listing "$project_file"
    } > "${docs_content_dir}/_index.md"

    [[ -d "$docs_dir" ]] || return 0
    find "$docs_dir" -type f \( -name '*.md' -o -name '*.markdown' \) -print | sort |
        while IFS= read -r doc_file; do
            local rel doc_slug page_dir title
            rel="${doc_file#${docs_dir}/}"
            doc_slug="${rel%.*}"
            page_dir="${docs_content_dir}/${doc_slug}"
            title="$(sed -n '1s/^# //p' "$doc_file")"
            [[ -n "$title" ]] || title="$rel"
            mkdir -p "$page_dir"
            {
                printf '+++\n'
                printf 'title = %s\n' "$(jq -n --arg v "$title" '$v')"
                printf 'path = "projects/%s/docs/%s"\n' "$slug" "$doc_slug"
                printf '[extra]\nkind = "docs"\n'
                printf '+++\n\n'
                escape_zola_shortcode_markers < "$doc_file"
            } > "${page_dir}/index.md"
        done
}

write_readme_section() {
    local readme_file="$1"
    local section_class="$2"

    [[ -f "$readme_file" ]] || return 0

    printf '<section class="%s">\n\n' "$section_class"
    printf '## README.md\n\n'
    escape_zola_shortcode_markers < "$readme_file"
    printf '\n</section>\n\n'
}

write_release_page() {
    local project_file="$1"
    local release_file="$2"
    local releases_dir="$3"
    local name project_id version commit built_at changelog release_dir
    name="$(json_string "$project_file" '.name')"
    project_id="$(json_string "$release_file" '.project_id')"
    version="$(json_string "$release_file" '.version')"
    commit="$(json_string "$release_file" '.commit')"
    built_at="$(json_string "$release_file" '.built_at')"
    changelog="$(json_string "$release_file" '.changelog')"
    release_dir="${releases_dir}/${version}"
    mkdir -p "$release_dir"

    {
        printf '+++\n'
        printf 'title = "%s %s"\n' "$name" "$version"
        printf 'path = "projects/%s/releases/%s"\n' "$(json_string "$project_file" '.slug')" "$version"
        printf '[extra]\nkind = "release"\n'
        printf '+++\n\n'
        printf '%s\n\n' "$changelog"
        printf '<div class="fact-block">\n'
        printf '<h2>Release</h2>\n'
        printf '<dl class="meta-list">\n'
        printf '<dt>project</dt><dd><code>%s</code></dd>\n' "$project_id"
        printf '<dt>version</dt><dd><code>%s</code></dd>\n' "$version"
        if [[ "$commit" != "0000000000000000000000000000000000000000" ]]; then
            printf '<dt>commit</dt><dd><code>%s</code></dd>\n' "$commit"
        fi
        printf '<dt>built_at</dt><dd><code>%s</code></dd>\n' "$built_at"
        printf '</dl>\n'
        printf '</div>\n\n'
        if jq -e '(.artifacts | length) > 0 or has("sbom")' "$release_file" >/dev/null; then
            printf '## Files\n\n'
            if jq -e '(.artifacts | length) > 0' "$release_file" >/dev/null; then
                printf '| name | sha256 | content type |\n'
                printf '| --- | --- | --- |\n'
                jq -r '.artifacts[] | "| `" + .name + "` | `" + .sha256 + "` | `" + .content_type + "` |"' "$release_file"
            fi
            if jq -e 'has("sbom")' "$release_file" >/dev/null; then
                printf -- '- [SBOM](/projects/%s/releases/%s/sbom/)\n' "$(json_string "$project_file" '.slug')" "$version"
            fi
            printf '\n'
        fi
        if jq -e '((.provenance // []) | length) > 0' "$release_file" >/dev/null; then
            printf '## Provenance\n\n'
            printf '| name | sha256 | content type |\n'
            printf '| --- | --- | --- |\n'
            jq -r '.provenance[] | "| `" + .name + "` | `" + .sha256 + "` | `" + .content_type + "` |"' "$release_file"
            printf '\n'
        fi
    } > "${release_dir}/index.md"

    if jq -e 'has("sbom")' "$release_file" >/dev/null; then
        mkdir -p "${release_dir}/sbom"
        {
            printf '+++\n'
            printf 'title = "%s %s SBOM"\n' "$name" "$version"
            printf 'path = "projects/%s/releases/%s/sbom"\n' "$(json_string "$project_file" '.slug')" "$version"
            printf '[extra]\nkind = "sbom"\n'
            printf '+++\n\n'
            printf '<div class="fact-block">\n'
            printf '<h2>SBOM</h2>\n'
            printf '<dl class="meta-list">\n'
            jq -r '.sbom | "<dt>format</dt><dd><code>" + .format + "</code></dd>\n<dt>path</dt><dd><code>" + .path + "</code></dd>\n<dt>sha256</dt><dd><code>" + .sha256 + "</code></dd>"' "$release_file"
            printf '</dl>\n'
            printf '</div>\n'
        } > "${release_dir}/sbom/index.md"
    fi
}

write_projects_index() {
    {
        printf '+++\n'
        printf 'title = "Projects"\n'
        printf 'sort_by = "title"\n'
        printf '+++\n'
    } > "${projects_content_dir}/_index.md"
}

validate_source_snapshot() {
    local manifest="$1"
    local project_id root_abs root_real snapshot_root_real root_rel source_kind include_file exclude_file

    validate_no_htmlish_text "$manifest"
    jq -e '
      (keys - [
        "schema_version",
        "project_id",
        "snapshot_id",
        "generated_at",
        "source_kind",
        "root",
        "include",
        "exclude"
      ] | length == 0)
      and
      .schema_version == 1
      and (.project_id | test("^[a-z0-9][a-z0-9-]*$"))
      and (.snapshot_id | type == "string" and length > 0)
      and (.generated_at | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"))
      and .source_kind == "approved-public-copy"
      and (.root | type == "string" and length > 0)
      and (.include | type == "array" and length > 0)
      and (.exclude | type == "array")
    ' "$manifest" >/dev/null || fail "invalid source snapshot manifest: ${manifest}"

    project_id="$(json_string "$manifest" '.project_id')"
    registry_project "$project_id" >/dev/null || fail "source snapshot project not allowlisted: ${project_id}"
    root_rel="$(json_string "$manifest" '.root')"
    case "$root_rel" in
        /*|../*|*/../*|*'/..'|'.'|'')
            fail "source snapshot root must stay inside the catalog snapshot tree: ${root_rel}"
            ;;
    esac
    root_abs="${repo_root}/${root_rel}"
    source_kind="$(json_string "$manifest" '.source_kind')"
    [[ "$source_kind" == "approved-public-copy" ]] || fail "unsupported source snapshot kind: ${source_kind}"
    [[ -d "$root_abs" ]] || fail "source snapshot root does not exist: ${root_rel}"

    root_real="$(realpath "$root_abs")"
    snapshot_root_real="$(realpath "$source_snapshot_root")"
    case "$root_real" in
        "${snapshot_root_real}/${project_id}/files"| "${snapshot_root_real}/${project_id}/files/"*)
            ;;
        *)
            fail "source snapshot root escapes project snapshot files directory: ${root_rel}"
            ;;
    esac

    if find "$root_abs" -type l -print -quit | grep -q .; then
        fail "source snapshot contains symlinks: ${root_rel}"
    fi

    include_file="$(mktemp)"
    exclude_file="$(mktemp)"
    jq -r '.include[]' "$manifest" > "$include_file"
    jq -r '.exclude[]' "$manifest" > "$exclude_file"
    if grep -E '(^/|(^|/)\.\.(/|$))' "$include_file" "$exclude_file" >/dev/null; then
        fail "source snapshot include/exclude patterns must be relative and cannot contain .."
    fi

    while IFS= read -r snapshot_file; do
        local rel size
        rel="${snapshot_file#${root_abs}/}"
        case "$rel" in
            /*|../*|*/../*|.git/*|.terraform/*|*.state-file|*.plan-file|.env|.env.*|*/.env|*/.env.*)
                fail "source snapshot contains denied path: ${rel}"
                ;;
        esac
        path_is_listed "$rel" "$include_file" || fail "source snapshot file is not included by manifest allowlist: ${rel}"
        if path_is_listed "$rel" "$exclude_file"; then
            fail "source snapshot file is excluded by manifest denylist: ${rel}"
        fi
        size="$(wc -c < "$snapshot_file")"
        [[ "$size" -le "$max_source_file_bytes" ]] || fail "source file exceeds ${max_source_file_bytes} bytes: ${rel}"
        if grep -En 'private source host|private source host|source url field|cloud secret key|cloud access key|vault token|private key|state file' "$snapshot_file" >/dev/null; then
            fail "source snapshot file contains denied operational/private text: ${rel}"
        fi
        if grep -En '~~~' "$snapshot_file" >/dev/null; then
            fail "source snapshot file contains unsupported fenced-code delimiter: ${rel}"
        fi
    done < <(find "$root_abs" -type f | sort)

    rm -f "$include_file" "$exclude_file"
}

validate_docs_snapshot() {
    local manifest="$1"
    local project_id root_rel root_abs rel

    validate_source_snapshot "$manifest"
    project_id="$(json_string "$manifest" '.project_id')"
    root_rel="$(json_string "$manifest" '.root')"
    root_abs="${repo_root}/${root_rel}"

    while IFS= read -r snapshot_file; do
        rel="${snapshot_file#${root_abs}/}"
        case "$rel" in
            README.md|docs/*.md|docs/*.markdown)
                ;;
            *)
                fail "docs-only snapshot contains non-documentation path: ${project_id} ${rel}"
                ;;
        esac
    done < <(find "$root_abs" -type f | sort)
}

source_tree_html() {
    local root_abs="$1"
    local slug="$2"
    local current_kind="$3"
    local current_path="$4"

    printf '<nav class="source-tree" aria-label="Source tree">\n'
    printf '<div class="source-tree__label">files</div>\n'
    printf '<ul>\n'
    if [[ "$current_kind" == "tree" && -z "$current_path" ]]; then
        printf '<li class="source-tree__item source-tree__item--dir" style="--depth:0"><a aria-current="page" href="/source/%s/" title="./"><span class="source-tree__name">./</span><span class="source-tree__count">%s</span></a></li>\n' "$slug" "$(dir_summary "$root_abs" | html_escape)"
    else
        printf '<li class="source-tree__item source-tree__item--dir" style="--depth:0"><a href="/source/%s/" title="./"><span class="source-tree__name">./</span><span class="source-tree__count">%s</span></a></li>\n' "$slug" "$(dir_summary "$root_abs" | html_escape)"
    fi

    while IFS= read -r dir; do
        local rel label title current_attr depth summary
        rel="${dir#${root_abs}/}"
        [[ "$rel" == "$dir" ]] && continue
        label="$(basename "$dir" | html_escape)/"
        title="$(printf '%s/' "$rel" | html_escape)"
        depth="$(path_depth "$rel")"
        summary="$(dir_summary "$dir" | html_escape)"
        current_attr=""
        [[ "$current_kind" == "tree" && "$current_path" == "$rel" ]] && current_attr=' aria-current="page"'
        printf '<li class="source-tree__item source-tree__item--dir" style="--depth:%s"><a%s href="/source/%s/tree/%s/" title="%s"><span class="source-tree__name">%s</span><span class="source-tree__count">%s</span></a></li>\n' "$depth" "$current_attr" "$slug" "$rel" "$title" "$label" "$summary"
    done < <(find "$root_abs" -mindepth 1 -type d | sort)

    while IFS= read -r nav_file; do
        local rel label title current_attr depth
        rel="${nav_file#${root_abs}/}"
        label="$(basename "$nav_file" | html_escape)"
        title="$(printf '%s' "$rel" | html_escape)"
        depth="$(path_depth "$rel")"
        current_attr=""
        [[ "$current_kind" == "blob" && "$current_path" == "$rel" ]] && current_attr=' aria-current="page"'
        printf '<li class="source-tree__item source-tree__item--file" style="--depth:%s"><a%s href="/source/%s/blob/%s/" title="%s"><span class="source-tree__name">%s</span></a></li>\n' "$depth" "$current_attr" "$slug" "$rel" "$title" "$label"
    done < <(find "$root_abs" -type f | sort)
    printf '</ul>\n'
    printf '</nav>\n'
}

write_source_index() {
    {
        printf '+++\n'
        printf 'title = "Source"\n'
        printf 'sort_by = "title"\n'
        printf '+++\n\n'
        printf 'Browse source files generated for public viewing. These pages are copies, not a live repository.\n'
    } > "${source_content_dir}/_index.md"
}

write_search_page() {
    {
        printf '+++\n'
        printf 'title = "Search"\n'
        printf 'description = "Client-side search over public catalog pages."\n'
        printf '[extra]\nkind = "search"\n'
        printf '+++\n\n'
        printf 'Search public pages by project, source path, release, or build note.\n\n'
        printf '<form class="search-form" data-search-form>\n'
        printf '<label for="site-search">query</label>\n'
        printf '<input id="site-search" name="q" type="search" autocomplete="off" data-search-input placeholder="project, source path, release, verify">\n'
        printf '<button type="submit">search</button>\n'
        printf '</form>\n'
        printf '<noscript><p>Search needs JavaScript. Primary project, source, release, and verify pages remain linked from the navigation.</p></noscript>\n'
        printf '<section class="search-output" data-search-results aria-live="polite">Type to search.</section>\n'
    } > "${content_root}/search.md"
}

write_source_browser() {
    local project_file="$1"
    local project_id slug name visibility manifest root_rel root_abs snapshot_id generated_at source_kind project_source_dir
    project_id="$(json_string "$project_file" '.project_id')"
    slug="$(json_string "$project_file" '.slug')"
    name="$(json_string "$project_file" '.name')"
    visibility="$(json_string "$project_file" '.visibility')"

    if [[ "$visibility" == "docs-only" ]]; then
        jq -e 'has("public_source_snapshot")' "$project_file" >/dev/null &&
            fail "docs-only projects cannot publish source browser snapshots: ${project_id}"
        return 0
    fi

    jq -e 'has("public_source_snapshot")' "$project_file" >/dev/null || return 0
    manifest="${repo_root}/$(json_string "$project_file" '.public_source_snapshot.manifest_path')"
    [[ -f "$manifest" ]] || fail "public source snapshot manifest missing: ${manifest}"
    validate_source_snapshot "$manifest"
    [[ "$(json_string "$manifest" '.project_id')" == "$project_id" ]] ||
        fail "public source snapshot project_id does not match project manifest: ${project_id}"
    [[ "$(json_string "$manifest" '.snapshot_id')" == "$(json_string "$project_file" '.public_source_snapshot.snapshot_id')" ]] ||
        fail "public source snapshot id does not match project manifest: ${project_id}"

    snapshot_id="$(json_string "$manifest" '.snapshot_id')"
    generated_at="$(json_string "$manifest" '.generated_at')"
    source_kind="$(json_string "$manifest" '.source_kind')"
    root_rel="$(json_string "$manifest" '.root')"
    root_abs="${repo_root}/${root_rel}"
    project_source_dir="${source_content_dir}/${slug}"
    mkdir -p "${project_source_dir}/tree" "${project_source_dir}/blob"

    {
        printf '+++\n'
        printf 'title = "%s Source"\n' "$name"
        printf 'description = "Public source copy for %s."\n' "$name"
        printf '[extra]\nkind = "source"\nhide_children = true\n'
        printf '+++\n\n'
        printf 'This is a generated public source copy, not a live repository view.\n\n'
        printf '<div class="source-browser">\n'
        source_tree_html "$root_abs" "$slug" "tree" ""
        printf '<section class="source-pane">\n'
        source_breadcrumb_html "$slug" "tree" ""
        printf '<p class="source-meta"><span>path <code>./</code></span><span>source copy <code>%s</code></span><span>generated <code>%s</code></span><span>kind <code>%s</code></span></p>\n' "$snapshot_id" "$generated_at" "$source_kind"
        source_directory_listing "$root_abs" "$slug" "$root_abs"
        write_readme_section "${root_abs}/README.md" "source-readme"
        printf '</section>\n</div>\n'
    } > "${project_source_dir}/_index.md"

    write_source_tree_page "$root_abs" "$slug" "$name" "$snapshot_id" ""
    while IFS= read -r dir; do
        local rel
        rel="${dir#${root_abs}/}"
        [[ "$rel" == "$dir" ]] && continue
        write_source_tree_page "$root_abs" "$slug" "$name" "$snapshot_id" "$rel"
    done < <(find "$root_abs" -mindepth 1 -type d | sort)

    while IFS= read -r source_file; do
        write_source_blob_page "$root_abs" "$slug" "$name" "$snapshot_id" "$source_file"
    done < <(find "$root_abs" -type f | sort)
}

source_directory_listing() {
    local root_abs="$1"
    local slug="$2"
    local dir_abs="$3"

    printf '<div class="directory-list">\n'
    printf '<h2>Contents</h2>\n'
    printf '<table class="directory-table">\n'
    printf '<thead><tr><th>name</th><th>detail</th></tr></thead>\n'
    printf '<tbody>\n'
    while IFS= read -r child; do
        local child_rel label summary
        child_rel="${child#${root_abs}/}"
        label="$(basename "$child" | html_escape)"
        summary="$(dir_summary "$child" | html_escape)"
        printf '<tr><td><span class="filetype-icon" aria-label="directory">d</span><a href="/source/%s/tree/%s/">%s/</a></td><td>%s</td></tr>\n' "$slug" "$child_rel" "$label" "$summary"
    done < <(find "$dir_abs" -mindepth 1 -maxdepth 1 -type d | sort)
    while IFS= read -r child; do
        local child_rel label child_size child_lang
        child_rel="${child#${root_abs}/}"
        label="$(basename "$child" | html_escape)"
        child_size="$(wc -c < "$child")"
        child_lang="$(detect_language "$child_rel")"
        printf '<tr><td><span class="filetype-icon" aria-label="file">f</span><a href="/source/%s/blob/%s/">%s</a></td><td><code>%s</code> bytes, <code>%s</code></td></tr>\n' "$slug" "$child_rel" "$label" "$child_size" "$child_lang"
    done < <(find "$dir_abs" -mindepth 1 -maxdepth 1 -type f | sort)
    printf '</tbody></table>\n</div>\n'
}

write_source_tree_page() {
    local root_abs="$1"
    local slug="$2"
    local name="$3"
    local snapshot_id="$4"
    local rel_dir="$5"
    local dir_abs page_dir page_path display_path

    if [[ -z "$rel_dir" ]]; then
        dir_abs="$root_abs"
        page_dir="${source_content_dir}/${slug}/tree"
        page_path="source/${slug}/tree"
        display_path="./"
    else
        dir_abs="${root_abs}/${rel_dir}"
        page_dir="${source_content_dir}/${slug}/tree/${rel_dir}"
        page_path="source/${slug}/tree/${rel_dir}"
        display_path="${rel_dir}/"
    fi
    mkdir -p "$page_dir"

    {
        printf '+++\n'
        printf 'title = "Source tree: %s"\n' "$display_path"
        printf 'path = "%s"\n' "$page_path"
        printf '[extra]\nkind = "source-tree"\n'
        printf '+++\n\n'
        printf '<div class="source-browser">\n'
        source_tree_html "$root_abs" "$slug" "tree" "$rel_dir"
        printf '<section class="source-pane">\n'
        printf '<nav class="source-toolbar" aria-label="Source navigation">\n'
        printf '<a href="/source/%s/">root</a>\n' "$slug"
        if [[ -n "$rel_dir" ]]; then
            local parent_dir
            parent_dir="$(parent_dir_path "$rel_dir")"
            if [[ -n "$parent_dir" ]]; then
                printf '<a href="/source/%s/tree/%s/">parent</a>\n' "$slug" "$parent_dir"
            else
                printf '<a href="/source/%s/tree/">parent</a>\n' "$slug"
            fi
        fi
        printf '</nav>\n'
        source_breadcrumb_html "$slug" "tree" "$rel_dir"
        printf '<p class="source-meta"><span>path <code>%s</code></span><span>source copy <code>%s</code></span></p>\n' "$(printf '%s' "$display_path" | html_escape)" "$snapshot_id"
        source_directory_listing "$root_abs" "$slug" "$dir_abs"
        printf '</section>\n</div>\n'
    } > "${page_dir}/index.md"
}

write_source_blob_page() {
    local root_abs="$1"
    local slug="$2"
    local name="$3"
    local snapshot_id="$4"
    local source_file="$5"
    local rel page_dir lang size file_name parent_dir
    rel="${source_file#${root_abs}/}"
    file_name="$(basename "$rel")"
    parent_dir="$(parent_dir_path "$rel")"
    page_dir="${source_content_dir}/${slug}/blob/${rel}"
    lang="$(detect_language "$rel")"
    size="$(wc -c < "$source_file")"
    mkdir -p "$page_dir"

    {
        printf '+++\n'
        printf 'title = %s\n' "$(jq -n --arg v "$file_name" '$v')"
        printf 'path = "source/%s/blob/%s"\n' "$slug" "$rel"
        printf '[extra]\nkind = "source-blob"\n'
        printf '+++\n\n'
        printf '<div class="source-browser">\n'
        source_tree_html "$root_abs" "$slug" "blob" "$rel"
        printf '<section class="source-pane">\n'
        printf '<nav class="source-toolbar" aria-label="Source navigation">\n'
        printf '<a href="/source/%s/">root</a>\n' "$slug"
        if [[ -n "$parent_dir" ]]; then
            printf '<a href="/source/%s/tree/%s/">parent</a>\n' "$slug" "$parent_dir"
        else
            printf '<a href="/source/%s/tree/">parent</a>\n' "$slug"
        fi
        printf '</nav>\n'
        source_breadcrumb_html "$slug" "blob" "$rel"
        printf '<p class="source-meta"><span>path <code>%s</code></span><span>language <code>%s</code></span><span>bytes <code>%s</code></span><span>source copy <code>%s</code></span></p>\n' "$(printf '%s' "$rel" | html_escape)" "$lang" "$size" "$snapshot_id"
        printf '<div class="source-code">\n'
        printf '\n~~~%s\n' "$lang"
        escape_zola_shortcode_markers < "$source_file"
        printf '\n~~~\n'
        printf '</div>\n'
        printf '</section>\n</div>\n'
    } > "${page_dir}/index.md"
}

write_design_pages() {
    local src title slug dest summary body
    for src in "${repo_root}/docs/design/"*.md; do
        [[ -f "$src" ]] || continue
        title="$(sed -n '1s/^# //p' "$src")"
        slug="$(basename "$src" .md)"
        dest="${design_content_dir}/${slug}.md"
        case "$slug" in
            0002-aws-static-hosting-security)
                summary="Public summary of the static hosting security model."
                body="Design 0002 defines the private S3 plus CloudFront hosting setup for this site: HTTPS, origin access control, private buckets, DNS, budgets, and short-lived publish credentials. It omits private repository and runner details."
                ;;
            0003-static-site-generation-and-open-source-portfolio)
                summary="Public summary of the static site generation and catalog model."
                body="Design 0003 defines the generated project index: project pages, release files, SBOM links, source-copy pages, and design notes. It shows approved source copies, not a public repository."
                ;;
            *)
                summary="Public design summary."
                body="This public design note keeps the implementation summary visible while leaving private automation and repository details out."
                ;;
        esac
        {
            printf '+++\n'
            printf 'title = %s\n' "$(jq -n --arg v "$title" '$v')"
            printf 'description = %s\n' "$(jq -n --arg v "$summary" '$v')"
            printf '[extra]\nkind = "design"\n'
            printf '+++\n\n'
            printf '%s\n' "$body"
        } > "$dest"
    done
}

write_provenance_page() {
    local output="${verify_content_dir}/provenance.md"
    local project_count release_count source_count
    mkdir -p "$verify_content_dir"
    project_count="$(find "$fixture_root" -mindepth 2 -maxdepth 2 -name project.json -print | wc -l | tr -d ' ')"
    release_count="$(find "$fixture_root" -name release.json -print | wc -l | tr -d ' ')"
    source_count="$(find "$source_snapshot_root" -mindepth 2 -maxdepth 2 -name manifest.json -print | wc -l | tr -d ' ')"

    {
        printf '+++\n'
        printf 'title = "Catalog Provenance"\n'
        printf 'description = "Generated catalog input and build provenance for the public site."\n'
        printf '[extra]\nkind = "provenance"\n'
        printf '+++\n\n'
        printf '<div class="fact-block">\n'
        printf '<h2>Build inputs</h2>\n'
        printf '<dl class="meta-list">\n'
        printf '<dt>generator</dt><dd><code>scripts/build-apex-catalog.sh</code></dd>\n'
        printf '<dt>zola</dt><dd><code>%s</code></dd>\n' "$ZOLA_VERSION"
        printf '<dt>site registry</dt><dd><code>sites/registry/static-sites.json</code></dd>\n'
        printf '<dt>project registry</dt><dd><code>sites/registry/projects.json</code></dd>\n'
        printf '<dt>project manifests</dt><dd><code>%s</code></dd>\n' "$project_count"
        printf '<dt>release manifests</dt><dd><code>%s</code></dd>\n' "$release_count"
        printf '<dt>source copies</dt><dd><code>%s</code></dd>\n' "$source_count"
        printf '</dl>\n'
        printf '</div>\n\n'

        printf '## Projects\n\n'
        printf '| project | slug | visibility | release count | source copy |\n'
        printf '| --- | --- | --- | --- | --- |\n'
        find "$fixture_root" -mindepth 2 -maxdepth 2 -name project.json -print | sort |
            while IFS= read -r project_file; do
                local project_id slug visibility releases snapshot
                project_id="$(json_string "$project_file" '.project_id')"
                slug="$(json_string "$project_file" '.slug')"
                visibility="$(json_string "$project_file" '.visibility')"
                if [[ -d "${fixture_root}/${project_id}/releases" ]]; then
                    releases="$(find "${fixture_root}/${project_id}/releases" -name release.json -print | wc -l | tr -d ' ')"
                else
                    releases=0
                fi
                snapshot="$(jq -er '.public_source_snapshot.snapshot_id // "none"' "$project_file")"
                printf '| `%s` | `%s` | `%s` | `%s` | `%s` |\n' "$project_id" "$slug" "$visibility" "$releases" "$snapshot"
            done
        printf '\n'

        printf '## Release Manifests\n\n'
        printf '| project | version | commit | built_at | sbom |\n'
        printf '| --- | --- | --- | --- | --- |\n'
        find "$fixture_root" -name release.json -print | sort |
            while IFS= read -r release_file; do
                local project_id version commit built_at sbom_path
                project_id="$(json_string "$release_file" '.project_id')"
                version="$(json_string "$release_file" '.version')"
                commit="$(json_string "$release_file" '.commit')"
                built_at="$(json_string "$release_file" '.built_at')"
                sbom_path="$(jq -er '.sbom.path // "none"' "$release_file")"
                [[ "$commit" == "0000000000000000000000000000000000000000" ]] && commit="not recorded"
                printf '| `%s` | `%s` | `%s` | `%s` | `%s` |\n' "$project_id" "$version" "$commit" "$built_at" "$sbom_path"
            done
        printf '\n'

        printf '## Source Copies\n\n'
        printf '| project | snapshot | generated | kind | root |\n'
        printf '| --- | --- | --- | --- | --- |\n'
        find "$source_snapshot_root" -mindepth 2 -maxdepth 2 -name manifest.json -print | sort |
            while IFS= read -r manifest; do
                local project_id snapshot generated source_kind root_rel
                project_id="$(json_string "$manifest" '.project_id')"
                snapshot="$(json_string "$manifest" '.snapshot_id')"
                generated="$(json_string "$manifest" '.generated_at')"
                source_kind="$(json_string "$manifest" '.source_kind')"
                root_rel="$(json_string "$manifest" '.root')"
                printf '| `%s` | `%s` | `%s` | `%s` | `%s` |\n' "$project_id" "$snapshot" "$generated" "$source_kind" "$root_rel"
            done
    } > "$output"
}

write_rollback_page() {
    local output="${verify_content_dir}/rollback.md"
    mkdir -p "$verify_content_dir"

    {
        printf '+++\n'
        printf 'title = "Rollback Procedure"\n'
        printf 'description = "Static-site rollback paths for the generated site."\n'
        printf '[extra]\nkind = "rollback"\n'
        printf '+++\n\n'
        printf 'The site can be republished from checked-in catalog, source-copy, template, and script inputs. Rollback must use one of these repeatable paths:\n\n'
        printf '1. Rebuild and republish a known-good commit through `scripts/validate-apex-catalog.sh` and `scripts/publish-apex-site.sh`.\n'
        printf '2. Restore prior S3 object versions from the versioned private site bucket, then invalidate the CloudFront distribution.\n\n'
        printf '<div class="fact-block">\n'
        printf '<h2>Rollback requirements</h2>\n'
        printf '<dl class="meta-list">\n'
        printf '<dt>bucket versioning</dt><dd><code>modules/static-site/main.tf aws_s3_bucket_versioning.origin Enabled</code></dd>\n'
        printf '<dt>publish script</dt><dd><code>scripts/publish-apex-site.sh</code></dd>\n'
        printf '<dt>validation gate</dt><dd><code>scripts/validate-apex-catalog.sh</code></dd>\n'
        printf '<dt>cloudfront invalidation</dt><dd><code>aws cloudfront create-invalidation --paths "/*"</code></dd>\n'
        printf '</dl>\n'
        printf '</div>\n\n'
        printf 'Project release files are excluded from site cleanup, so a site rollback does not remove published release objects.\n'
    } > "$output"
}

write_search_index() {
    local entries_file output_file project_file release_file manifest source_file rel slug project_id name summary version
    entries_file="$(mktemp)"
    output_file="${static_root}/search-index.json"

    find "$fixture_root" -mindepth 2 -maxdepth 2 -name project.json -print | sort |
        while IFS= read -r project_file; do
            local visibility topics snapshot_id
            visibility="$(json_string "$project_file" '.visibility')"
            project_is_rendered "$project_file" || continue
            project_id="$(json_string "$project_file" '.project_id')"
            slug="$(json_string "$project_file" '.slug')"
            name="$(json_string "$project_file" '.name')"
            summary="$(json_string "$project_file" '.summary')"
            topics="$(jq -r '.topics | join(" ")' "$project_file")"
            append_search_entry "$entries_file" "Project" "$name" "/projects/${slug}/" "$summary" "$topics"
            append_search_entry "$entries_file" "Release" "${name} releases" "/projects/${slug}/releases/" "Immutable public release metadata for ${name}." "$topics releases"
            if jq -e 'has("public_docs_snapshot")' "$project_file" >/dev/null; then
                append_search_entry "$entries_file" "Docs" "${name} documentation" "/projects/${slug}/docs/" "README and docs directory content for ${name}." "$topics docs"
            fi

            if [[ "$visibility" != "docs-only" ]] && jq -e 'has("public_source_snapshot")' "$project_file" >/dev/null; then
                snapshot_id="$(json_string "$project_file" '.public_source_snapshot.snapshot_id')"
                append_search_entry "$entries_file" "Source" "${name} source" "/source/${slug}/" "Generated public source copy ${snapshot_id}." "$topics source ${snapshot_id}"
            fi
        done

    find "$fixture_root" -name release.json -print | sort |
        while IFS= read -r release_file; do
            project_id="$(json_string "$release_file" '.project_id')"
            project_file="${fixture_root}/${project_id}/project.json"
            [[ -f "$project_file" ]] || continue
            project_is_rendered "$project_file" || continue
            slug="$(json_string "$project_file" '.slug')"
            name="$(json_string "$project_file" '.name')"
            version="$(json_string "$release_file" '.version')"
            summary="$(json_string "$release_file" '.changelog')"
            append_search_entry "$entries_file" "Release" "${name} ${version}" "/projects/${slug}/releases/${version}/" "$summary" "release ${version}"
            if jq -e 'has("sbom")' "$release_file" >/dev/null; then
                append_search_entry "$entries_file" "Release" "${name} ${version} SBOM" "/projects/${slug}/releases/${version}/sbom/" "Software bill of materials metadata for ${name} ${version}." "sbom ${version}"
            fi
        done

    find "$source_snapshot_root" -mindepth 2 -maxdepth 2 -name manifest.json -print | sort |
        while IFS= read -r manifest; do
            project_id="$(json_string "$manifest" '.project_id')"
            project_file="${fixture_root}/${project_id}/project.json"
            [[ -f "$project_file" ]] || continue
            project_is_rendered "$project_file" || continue
            [[ "$(jq -er '.public_source_snapshot.manifest_path // ""' "$project_file")" == "${manifest#${repo_root}/}" ]] || continue
            slug="$(json_string "$project_file" '.slug')"
            root_abs="${repo_root}/$(json_string "$manifest" '.root')"
            while IFS= read -r source_file; do
                rel="${source_file#${root_abs}/}"
                append_search_entry "$entries_file" "Source" "$rel" "/source/${slug}/blob/${rel}/" "Source file in the public copy." "source ${slug} ${rel}"
            done < <(find "$root_abs" -type f | sort)
        done

    find "$source_snapshot_root" -mindepth 2 -maxdepth 2 -name manifest.json -print | sort |
        while IFS= read -r manifest; do
            project_id="$(json_string "$manifest" '.project_id')"
            project_file="${fixture_root}/${project_id}/project.json"
            [[ -f "$project_file" ]] || continue
            project_is_rendered "$project_file" || continue
            [[ "$(jq -er '.public_docs_snapshot.manifest_path // ""' "$project_file")" == "${manifest#${repo_root}/}" ]] || continue
            slug="$(json_string "$project_file" '.slug')"
            root_abs="${repo_root}/$(json_string "$manifest" '.root')"
            docs_dir="${root_abs}/docs"
            [[ -d "$docs_dir" ]] || continue
            while IFS= read -r source_file; do
                rel="${source_file#${docs_dir}/}"
                append_search_entry "$entries_file" "Docs" "$rel" "/projects/${slug}/docs/${rel%.*}/" "Documentation page for ${slug}." "docs ${slug} ${rel}"
            done < <(find "$docs_dir" -type f \( -name '*.md' -o -name '*.markdown' \) | sort)
        done

    append_search_entry "$entries_file" "Verify" "Build notes" "/verify/" "How the site is built and rolled back." "verify provenance rollback"
    append_search_entry "$entries_file" "Verify" "Build inputs" "/verify/provenance/" "Generated catalog inputs and build details." "verify provenance"
    append_search_entry "$entries_file" "Verify" "Rollback procedure" "/verify/rollback/" "Static-site rollback paths for the generated site." "verify rollback"
    append_search_entry "$entries_file" "Design" "0002 - AWS Static Hosting Security" "/designs/0002-aws-static-hosting-security/" "Public summary of the static hosting security model." "design aws static hosting"
    append_search_entry "$entries_file" "Design" "0003 - Static Site Generation and Open Source Portfolio" "/designs/0003-static-site-generation-and-open-source-portfolio/" "Public summary of the static site generation and catalog model." "design zola catalog source"

    jq -s '{schema_version: 1, generated_by: "scripts/build-apex-catalog.sh", entries: sort_by(.type, .title, .url)}' "$entries_file" > "$output_file"
    rm -f "$entries_file"
}

write_generated_catalog() {
    local entries_file project_file project_id license releases

    entries_file="$(mktemp)"
    find "$fixture_root" -mindepth 2 -maxdepth 2 -name project.json -print | sort |
        while IFS= read -r project_file; do
            project_id="$(json_string "$project_file" '.project_id')"
            license="$(project_license "$project_file")"
            releases="$(project_release_count "$project_id")"
            jq \
                --arg license "$license" \
                --argjson release_count "$releases" \
                '. + {
                    license: $license,
                    release_count: $release_count,
                    has_releases: ($release_count > 0)
                }' "$project_file" >> "$entries_file"
        done

    jq -s '{schema_version: 1, projects: .}' "$entries_file" > "${generated_root}/projects.json"
    rm -f "$entries_file"
}

ensure_zola_binary() {
    local archive tmp_dir url checksum actual
    tmp_dir="${TMPDIR:-/tmp}/thepeoples-io-zola-${ZOLA_VERSION}"
    zola_binary="${tmp_dir}/zola"

    if [[ -x "$zola_binary" ]]; then
        actual="$("$zola_binary" --version | awk '{print $2}')"
        [[ "$actual" == "$ZOLA_VERSION" ]] || fail "cached zola ${ZOLA_VERSION} required, found ${actual}"
        log "Using cached zola ${actual}"
        return
    fi

    require_command curl
    require_command tar
    mkdir -p "$tmp_dir"
    archive="${tmp_dir}/zola.tar.gz"
    url="https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
    checksum="$ZOLA_LINUX_X86_64_GNU_SHA256"

    log "Fetching pinned zola ${ZOLA_VERSION}"
    curl -fsSL "$url" -o "$archive"
    printf '%s  %s\n' "$checksum" "$archive" | sha256sum -c -
    tar -xzf "$archive" -C "$tmp_dir" zola
    actual="$("$zola_binary" --version | awk '{print $2}')"
    [[ "$actual" == "$ZOLA_VERSION" ]] || fail "downloaded zola ${ZOLA_VERSION} required, found ${actual}"
}

verify_zola_pin() {
    if command -v zola >/dev/null 2>&1; then
        actual="$(zola --version | awk '{print $2}')"
        [[ "$actual" == "$ZOLA_VERSION" ]] || fail "zola ${ZOLA_VERSION} required, found ${actual}"
        log "Using local zola ${actual}"
        return
    fi

    if [[ "$use_container" == "true" ]] && command -v podman >/dev/null 2>&1; then
        log "Using pinned Zola container ghcr.io/getzola/zola:v${ZOLA_VERSION}"
        return
    fi

    if [[ "$use_container" == "true" ]] && command -v docker >/dev/null 2>&1; then
        log "Using pinned Zola container ghcr.io/getzola/zola:v${ZOLA_VERSION}"
        return
    fi

    ensure_zola_binary
}

run_zola_build() {
    if command -v zola >/dev/null 2>&1; then
        zola --root "$site_root" build --output-dir "${repo_root}/sites/apex" --force
        return
    fi

    if [[ -n "$zola_binary" ]]; then
        "$zola_binary" --root "$site_root" build --output-dir "${repo_root}/sites/apex" --force
        return
    fi

    if [[ "$use_container" == "true" ]] && command -v podman >/dev/null 2>&1; then
        podman run --rm -v "${repo_root}:/site:Z" -w /site "ghcr.io/getzola/zola:v${ZOLA_VERSION}" --root sites/apex-src build --output-dir sites/apex --force
        return
    fi

    [[ "$use_container" == "true" ]] || fail "zola ${ZOLA_VERSION} is required and pinned binary setup failed"
    docker run --rm -v "${repo_root}:/site" -w /site "ghcr.io/getzola/zola:v${ZOLA_VERSION}" --root sites/apex-src build --output-dir sites/apex --force
}

restore_source_shortcode_markers() {
    [[ -d "${repo_root}/sites/apex/source" ]] || return
    find "${repo_root}/sites/apex/source" -type f -name index.html -print0 |
        xargs -0 perl -0pi \
            -e 's/\{\{\/\*\s*(.*?)\s*\*\/\}\}/{{ $1 }}/gs;' \
            -e 's/\{% \/\*\s*(.*?)\s*\*\/%\}/{%/* $1 %}/gs;' \
            -e 's/\{# \/\*\s*(.*?)\s*\*\/#\}/{#/* $1 #}/gs;'
}

main() {
    require_command jq
    require_command perl
    require_command realpath
    require_command sha256sum
    validate_registry_sites
    validate_registry_projects
    verify_zola_pin
    reset_generated_content
    write_projects_index
    write_source_index
    write_search_page
    write_design_pages
    write_provenance_page
    write_rollback_page

    find "$fixture_root" -mindepth 2 -maxdepth 2 -name project.json -print | sort |
        while IFS= read -r project_file; do
            validate_project_manifest "$project_file"
            write_project_pages "$project_file"
            write_source_browser "$project_file"
        done

    write_generated_catalog
    write_search_index
    run_zola_build
    restore_source_shortcode_markers
    log "Built apex site into sites/apex"
}

main "$@"