From 87f235544d90a9f9bd4220a86eef6bc0f713e0b2 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 24 Jul 2026 19:27:01 +0000 Subject: [PATCH] Establish BlackSTAR as an independent successor --- .github/CODEOWNERS | 8 + .github/ISSUE_TEMPLATE/bug-report.yml | 71 ++++ .../ISSUE_TEMPLATE/compatibility-report.yml | 48 +++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature-request.yml | 40 ++ .../ISSUE_TEMPLATE/performance-regression.yml | 62 +++ .github/PULL_REQUEST_TEMPLATE.md | 31 ++ .github/dependabot.yml | 16 + .github/repository-settings.json | 92 ++++ .github/workflows/blackstar-ci.yml | 54 ++- .github/workflows/codeql.yml | 54 +++ .github/workflows/release.yml | 106 +++++ ATTRIBUTION.md | 39 ++ CHANGELOG.md | 36 ++ CODE_OF_CONDUCT.md | 88 ++-- CONTRIBUTING.md | 145 ++++--- GOVERNANCE.md | 68 +++ README.md | 187 ++++----- SECURITY.md | 40 ++ SUPPORT.md | 44 ++ docs/BLACKSTAR_RELEASE.md | 21 +- docs/COMPATIBILITY.md | 107 +++++ docs/INDEPENDENCE_TRANSITION.md | 60 +++ docs/MIGRATING_FROM_STAR.md | 74 ++++ docs/PERFORMANCE.md | 93 +++++ docs/RELEASE_POLICY.md | 74 ++++ docs/VERSIONING.md | 53 +++ docs/architecture/README.md | 3 + docs/architecture/claims.tsv | 8 + .../pdf/F24-independent-successor.pdf | Bin 0 -> 27488 bytes .../pdf/F25-version-compatibility.pdf | Bin 0 -> 26243 bytes .../diagrams/pdf/F26-governance-release.pdf | Bin 0 -> 29017 bytes .../src/F24-independent-successor.mmd | 28 ++ .../src/F25-version-compatibility.mmd | 31 ++ .../diagrams/src/F26-governance-release.mmd | 36 ++ .../svg/F24-independent-successor.svg | 2 + .../svg/F25-version-compatibility.svg | 2 + .../diagrams/svg/F26-governance-release.svg | 2 + .../direct-upstream-comparison-20260724.tsv | 9 + docs/architecture/figures.json | 36 ++ .../README.md | 26 ++ .../alignment-scaling.tsv | 9 + .../delta-cold-pairs.tsv | 4 + .../index-pairs.tsv | 4 + .../summary.json | 62 +++ docs/decisions/0001-independent-successor.md | 56 +++ docs/releases/1.0.0-release-notes.md | 61 +++ extras/maintenance/README.md | 78 ++++ .../maintenance/check_detachment_readiness.py | 204 +++++++++ extras/maintenance/export_github_state.py | 374 +++++++++++++++++ extras/maintenance/finalize_successor_repo.py | 395 ++++++++++++++++++ .../validate_successor_metadata.py | 201 +++++++++ .../maintenance/verify_transition_backup.py | 129 ++++++ extras/scripts/buildBlackSTARRelease.sh | 38 +- extras/scripts/generateBlackSTARSbom.py | 154 +++++++ extras/tests/scripts/testBlackstarVersion.sh | 42 ++ source/Parameters.cpp | 11 + source/VERSION | 5 +- 58 files changed, 3490 insertions(+), 239 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/compatibility-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/performance-regression.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/repository-settings.json create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/release.yml create mode 100644 ATTRIBUTION.md create mode 100644 CHANGELOG.md create mode 100644 GOVERNANCE.md create mode 100644 SECURITY.md create mode 100644 SUPPORT.md create mode 100644 docs/COMPATIBILITY.md create mode 100644 docs/INDEPENDENCE_TRANSITION.md create mode 100644 docs/MIGRATING_FROM_STAR.md create mode 100644 docs/PERFORMANCE.md create mode 100644 docs/RELEASE_POLICY.md create mode 100644 docs/VERSIONING.md create mode 100644 docs/architecture/diagrams/pdf/F24-independent-successor.pdf create mode 100644 docs/architecture/diagrams/pdf/F25-version-compatibility.pdf create mode 100644 docs/architecture/diagrams/pdf/F26-governance-release.pdf create mode 100644 docs/architecture/diagrams/src/F24-independent-successor.mmd create mode 100644 docs/architecture/diagrams/src/F25-version-compatibility.mmd create mode 100644 docs/architecture/diagrams/src/F26-governance-release.mmd create mode 100644 docs/architecture/diagrams/svg/F24-independent-successor.svg create mode 100644 docs/architecture/diagrams/svg/F25-version-compatibility.svg create mode 100644 docs/architecture/diagrams/svg/F26-governance-release.svg create mode 100644 docs/architecture/evidence/direct-upstream-comparison-20260724.tsv create mode 100644 docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/README.md create mode 100644 docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/alignment-scaling.tsv create mode 100644 docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/delta-cold-pairs.tsv create mode 100644 docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/index-pairs.tsv create mode 100644 docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json create mode 100644 docs/decisions/0001-independent-successor.md create mode 100644 docs/releases/1.0.0-release-notes.md create mode 100644 extras/maintenance/README.md create mode 100755 extras/maintenance/check_detachment_readiness.py create mode 100755 extras/maintenance/export_github_state.py create mode 100755 extras/maintenance/finalize_successor_repo.py create mode 100755 extras/maintenance/validate_successor_metadata.py create mode 100755 extras/maintenance/verify_transition_backup.py create mode 100755 extras/scripts/generateBlackSTARSbom.py create mode 100755 extras/tests/scripts/testBlackstarVersion.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..b1a0595c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +* @justinblethrow-cloud + +/source/ @justinblethrow-cloud +/extras/tests/ @justinblethrow-cloud +/extras/scripts/buildBlackSTARRelease.sh @justinblethrow-cloud +/.github/ @justinblethrow-cloud +/SECURITY.md @justinblethrow-cloud +/GOVERNANCE.md @justinblethrow-cloud diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..57e80bc5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,71 @@ +name: Bug report +description: Report reproducible incorrect behavior or a crash +title: "[Bug]: " +labels: + - "type: bug" + - "needs: triage" +body: + - type: markdown + attributes: + value: | + Do not include private sequence data, customer identifiers, credentials, or presigned URLs. + - type: input + id: version + attributes: + label: BlackSTAR version + description: Paste `STAR --version` and the executable SHA-256. + validations: + required: true + - type: textarea + id: command + attributes: + label: Command and environment + description: Include the complete command and relevant environment variables. + render: shell + validations: + required: true + - type: textarea + id: behavior + attributes: + label: Observed and expected behavior + validations: + required: true + - type: textarea + id: reproducer + attributes: + label: Minimal reproducer + description: Provide synthetic or public inputs and exact reproduction steps. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: Attach or paste relevant sections of `Log.out` and `Log.final.out`. + validations: + required: true + - type: textarea + id: system + attributes: + label: System information + description: OS, CPU, RAM, filesystem, storage placement, compiler or package source. + validations: + required: true + - type: dropdown + id: upstream + attributes: + label: Official STAR 2.7.11b comparison + options: + - "Official STAR does not reproduce the behavior" + - "Official STAR reproduces the behavior" + - "Not yet tested" + - "Not applicable to a BlackSTAR-only feature" + validations: + required: true + - type: checkboxes + id: confirmation + attributes: + label: Data safety + options: + - label: I removed private data, credentials, and customer information. + required: true diff --git a/.github/ISSUE_TEMPLATE/compatibility-report.yml b/.github/ISSUE_TEMPLATE/compatibility-report.yml new file mode 100644 index 00000000..65852e1b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility-report.yml @@ -0,0 +1,48 @@ +name: Compatibility report +description: Report a STAR interface, index, output, or wrapper incompatibility +title: "[Compatibility]: " +labels: + - "type: compatibility" + - "needs: triage" +body: + - type: dropdown + id: surface + attributes: + label: Compatibility surface + options: + - Command-line interface + - Conventional genome index + - Alignment output + - Shared-memory lifecycle + - Wrapper or workflow integration + - Overlay or Delta package + - Other + validations: + required: true + - type: textarea + id: versions + attributes: + label: Versions and identities + description: BlackSTAR, official STAR, wrapper, index, and executable identities. + validations: + required: true + - type: textarea + id: reproducer + attributes: + label: Minimal reproducer and observed difference + validations: + required: true + - type: textarea + id: contract + attributes: + label: Expected compatibility + description: Link the relevant compatibility promise or explain the expected inherited behavior. + validations: + required: true + - type: checkboxes + id: confirmation + attributes: + label: Data safety + options: + - label: I removed private data, credentials, and customer information. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..65267c07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: BlackSTAR questions and design discussion + url: https://github.com/justinblethrow-cloud/blackSTAR/discussions + about: Use Discussions for questions that are not reproducible defects. + - name: Private security report + url: https://github.com/justinblethrow-cloud/blackSTAR/security/advisories/new + about: Report suspected vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..50ba0f0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,40 @@ +name: Feature request +description: Propose a broadly useful BlackSTAR capability +title: "[Feature]: " +labels: + - "type: enhancement" + - "needs: triage" +body: + - type: textarea + id: problem + attributes: + label: Problem + description: Describe the user or operational problem independently of a proposed implementation. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior + validations: + required: true + - type: textarea + id: compatibility + attributes: + label: Compatibility and correctness impact + description: Explain affected CLI, index, output, resource, or lifecycle contracts. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + validations: + required: true + - type: textarea + id: validation + attributes: + label: Proposed validation + description: State how biological correctness, determinism, and practical value would be established. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/performance-regression.yml b/.github/ISSUE_TEMPLATE/performance-regression.yml new file mode 100644 index 00000000..c0480032 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance-regression.yml @@ -0,0 +1,62 @@ +name: Performance regression +description: Report a reproducible slowdown or resource regression +title: "[Performance]: " +labels: + - "type: performance" + - "needs: triage" +body: + - type: markdown + attributes: + value: | + Performance reports require a matched control and correctness checks. Absolute timings from unrelated systems are not sufficient. + - type: input + id: candidate + attributes: + label: Candidate identity + description: BlackSTAR version, commit, build flags, and executable SHA-256. + validations: + required: true + - type: input + id: control + attributes: + label: Control identity + description: Pinned BlackSTAR or official STAR version and executable SHA-256. + validations: + required: true + - type: textarea + id: workload + attributes: + label: Workload + description: Inputs, index, read count and length, output mode, thread count, and commands. + validations: + required: true + - type: textarea + id: system + attributes: + label: System and cache state + description: CPU topology, RAM, NUMA policy, filesystem, storage device, competing load, and cold/warm definition. + validations: + required: true + - type: textarea + id: measurements + attributes: + label: Measurements + description: Order-balanced replicates, wall time, CPU use, peak RSS, and variability. + validations: + required: true + - type: textarea + id: correctness + attributes: + label: Correctness comparison + description: Metrics, junctions, counts, BAM canonicalization, index identity, or other applicable gates. + validations: + required: true + - type: checkboxes + id: confirmation + attributes: + label: Confirmation + options: + - label: Candidate and control used identical inputs and resource allocation. + required: true + - label: I removed private data, credentials, and customer information. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..7809256d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## Purpose + +Describe the user-visible change and why it belongs in BlackSTAR. + +## Origin Classification + +- [ ] Inherited upstream behavior or fix +- [ ] BlackSTAR regression fix +- [ ] BlackSTAR-specific feature +- [ ] Documentation, test, or maintenance only + +## Compatibility and Risk + +Describe CLI, index, output, resource, concurrency, and lifecycle effects. + +## Validation + +List exact commands, tests, sanitizers, fixtures, and output comparisons. + +## Performance Evidence + +For a performance claim, identify the pinned control and candidate, matched +workload, order-balanced replicates, system/cache state, wall time, CPU, peak +RSS, variability, and correctness gates. Write `Not applicable` otherwise. + +## Documentation + +- [ ] Tests would fail without the change +- [ ] User-facing behavior and changelog are updated +- [ ] Generated architecture artifacts are synchronized +- [ ] No private data, credentials, or customer information is included diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3052ae63 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + labels: + - "type: dependencies" + - package-ecosystem: npm + directory: /extras/docs + schedule: + interval: monthly + open-pull-requests-limit: 5 + labels: + - "type: dependencies" diff --git a/.github/repository-settings.json b/.github/repository-settings.json new file mode 100644 index 00000000..32cdc10d --- /dev/null +++ b/.github/repository-settings.json @@ -0,0 +1,92 @@ +{ + "schema_version": 1, + "repository": { + "default_branch": "main", + "description": "Independently maintained, performance-oriented successor to the STAR RNA-seq aligner", + "homepage": "https://justinblethrow-cloud.github.io/blackSTAR/", + "features": { + "discussions": true, + "issues": true, + "wiki": false + }, + "private_vulnerability_reporting": true, + "topics": [ + "bioinformatics", + "genomics", + "high-performance-computing", + "openmp", + "rna-seq", + "star-aligner", + "transcriptomics" + ] + }, + "pages": { + "branch": "gh-pages", + "path": "/" + }, + "branch_protection": { + "allow_deletions": false, + "allow_force_pushes": false, + "allow_fork_syncing": false, + "block_creations": false, + "enforce_admins": true, + "lock_branch": false, + "required_conversation_resolution": true, + "required_linear_history": true, + "required_pull_request_reviews": { + "dismiss_stale_reviews": false, + "require_code_owner_reviews": false, + "require_last_push_approval": false, + "required_approving_review_count": 0 + }, + "required_status_checks": { + "contexts": [ + "build-and-test" + ], + "strict": true + }, + "restrictions": null + }, + "labels": { + "needs: reproducer": { + "color": "fbca04", + "description": "Requires a minimal public reproducer" + }, + "needs: triage": { + "color": "d4c5f9", + "description": "Requires initial maintainer classification" + }, + "origin: blackstar": { + "color": "1d76db", + "description": "Behavior introduced by BlackSTAR" + }, + "origin: upstream": { + "color": "c5def5", + "description": "Behavior inherited from official STAR" + }, + "security": { + "color": "b60205", + "description": "Security-related work; no private details" + }, + "type: bug": { + "color": "d73a4a", + "description": "Reproducible incorrect behavior" + }, + "type: compatibility": { + "color": "5319e7", + "description": "Compatibility contract or integration" + }, + "type: dependencies": { + "color": "0366d6", + "description": "Dependency maintenance" + }, + "type: enhancement": { + "color": "a2eeef", + "description": "Proposed feature or improvement" + }, + "type: performance": { + "color": "f9d0c4", + "description": "Performance or resource behavior" + } + } +} diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index c4a18ace..7c1624b3 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install -y g++ make zlib1g-dev + sudo apt-get install -y g++ jq make python3-yaml zlib1g-dev - name: Install architecture renderer run: npm ci --prefix extras/docs @@ -49,6 +49,17 @@ jobs: extras/benchmarks/runAlignmentPairs.py extras/tests/scripts/testBenchmarkHarness.sh + - name: Validate successor metadata + run: | + python3 -m py_compile \ + extras/maintenance/export_github_state.py \ + extras/maintenance/verify_transition_backup.py \ + extras/maintenance/check_detachment_readiness.py \ + extras/maintenance/finalize_successor_repo.py \ + extras/maintenance/validate_successor_metadata.py \ + extras/scripts/generateBlackSTARSbom.py + python3 extras/maintenance/validate_successor_metadata.py + - name: Build BlackSTAR env: JOBS: "2" @@ -56,8 +67,16 @@ jobs: - name: Verify release identity and OpenMP linkage run: | - test "$(source/STAR --version)" = "2.7.11b-blackstar.2" + test "$(source/STAR --version)" = "2.7.11b-blackstar.3" + extras/tests/scripts/testBlackstarVersion.sh ldd source/STAR | grep -Eq 'libgomp|libomp' + test -f dist/blackstar-1.0.0-linux-x86_64.spdx.json + test -f dist/blackstar-1.0.0-linux-x86_64/LICENSE + test -f dist/blackstar-1.0.0-linux-x86_64/ATTRIBUTION.md + jq -e ' + .spdxVersion == "SPDX-2.3" and + (.packages[] | select(.name == "BlackSTAR").versionInfo) == "1.0.0" + ' dist/blackstar-1.0.0-linux-x86_64.spdx.json - name: Build upstream 2.7.11b compatibility oracle run: | @@ -109,3 +128,34 @@ jobs: dist ${{ runner.temp }}/genome-insert-hardening ${{ runner.temp }}/saindex-strategies + + compiler-build: + name: compiler-${{ matrix.name }} + runs-on: ubuntu-24.04 + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + include: + - name: gcc + cxx: g++ + packages: g++ + - name: clang + cxx: clang++ + packages: clang libomp-dev + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install compiler + run: | + sudo apt-get update + sudo apt-get install -y ${{ matrix.packages }} jq make zlib1g-dev + + - name: Build and inspect + env: + CXX: ${{ matrix.cxx }} + run: | + make -C source -j2 STAR CXX="${CXX}" + test "$(source/STAR --version)" = "2.7.11b-blackstar.3" + source/STAR --version-json | jq -e '.blackstar_version == "1.0.0"' + ldd source/STAR | grep -Eq 'libgomp|libomp' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..be5112d1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,54 @@ +name: CodeQL + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + schedule: + - cron: "17 9 * * 3" + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: codeql-${{ matrix.language }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - language: c-cpp + build_mode: manual + - language: python + build_mode: none + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Initialize CodeQL + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build_mode }} + + - name: Install C++ build dependencies + if: matrix.language == 'c-cpp' + run: | + sudo apt-get update + sudo apt-get install -y g++ make zlib1g-dev + + - name: Build C++ database + if: matrix.language == 'c-cpp' + run: make -C source -j2 STAR + + - name: Analyze + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c90655e1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,106 @@ +name: Publish BlackSTAR Release + +on: + workflow_dispatch: + inputs: + tag: + description: Existing annotated vMAJOR.MINOR.PATCH tag + required: true + type: string + +permissions: + attestations: write + contents: write + id-token: write + +jobs: + publish: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + TAG: ${{ inputs.tag }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + ref: ${{ inputs.tag }} + + - name: Validate release identity + run: | + [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" + test "${TAG}" = "v${version}" + git cat-file -e "${TAG}^{tag}" + git fetch origin main + git merge-base --is-ancestor HEAD origin/main + test -f "docs/releases/${version}-release-notes.md" + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y g++ make zlib1g-dev + + - name: Build first release package + env: + DIST_DIR: ${{ runner.temp }}/build-1 + JOBS: "2" + run: extras/scripts/buildBlackSTARRelease.sh + + - name: Build independent reproduction + env: + DIST_DIR: ${{ runner.temp }}/build-2 + JOBS: "2" + run: | + git worktree add --detach "${RUNNER_TEMP}/source-2" HEAD + cd "${RUNNER_TEMP}/source-2" + extras/scripts/buildBlackSTARRelease.sh + + - name: Compare release products + run: | + version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" + package="blackstar-${version}-linux-x86_64" + cmp "${RUNNER_TEMP}/build-1/${package}.tar.gz" "${RUNNER_TEMP}/build-2/${package}.tar.gz" + cmp "${RUNNER_TEMP}/build-1/${package}.tar.gz.sha256" "${RUNNER_TEMP}/build-2/${package}.tar.gz.sha256" + cmp "${RUNNER_TEMP}/build-1/${package}.spdx.json" "${RUNNER_TEMP}/build-2/${package}.spdx.json" + cmp "${RUNNER_TEMP}/build-1/${package}/STAR" "${RUNNER_TEMP}/build-2/${package}/STAR" + cmp "${RUNNER_TEMP}/build-1/${package}/build-info.tsv" "${RUNNER_TEMP}/build-2/${package}/build-info.tsv" + cmp "${RUNNER_TEMP}/build-1/${package}/ldd.txt" "${RUNNER_TEMP}/build-2/${package}/ldd.txt" + cmp "${RUNNER_TEMP}/build-1/${package}/LICENSE" "${RUNNER_TEMP}/build-2/${package}/LICENSE" + cmp "${RUNNER_TEMP}/build-1/${package}/ATTRIBUTION.md" "${RUNNER_TEMP}/build-2/${package}/ATTRIBUTION.md" + + - name: Attest release products + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3 + with: + subject-path: | + ${{ runner.temp }}/build-1/*.tar.gz + ${{ runner.temp }}/build-1/*.spdx.json + ${{ runner.temp }}/build-1/*/STAR + + - name: Publish immutable release + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${TAG#v}" + package="blackstar-${version}-linux-x86_64" + gh release create "${TAG}" \ + --verify-tag \ + --title "BlackSTAR ${version}" \ + --notes-file "docs/releases/${version}-release-notes.md" \ + "${RUNNER_TEMP}/build-1/${package}/STAR#BlackSTAR Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${package}.tar.gz#BlackSTAR Linux x86-64 archive" \ + "${RUNNER_TEMP}/build-1/${package}.tar.gz.sha256#Archive SHA-256 sidecar" \ + "${RUNNER_TEMP}/build-1/${package}.spdx.json#SPDX 2.3 SBOM" \ + "${RUNNER_TEMP}/build-1/${package}/build-info.tsv#Reproducible build metadata" \ + "${RUNNER_TEMP}/build-1/${package}/ldd.txt#Runtime linkage metadata" \ + "LICENSE#MIT license" \ + "ATTRIBUTION.md#Upstream lineage and citation" + + - name: Retain release evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blackstar-release-${{ inputs.tag }} + retention-days: 30 + path: | + ${{ runner.temp }}/build-1 + ${{ runner.temp }}/build-2 diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md new file mode 100644 index 00000000..5e80acb1 --- /dev/null +++ b/ATTRIBUTION.md @@ -0,0 +1,39 @@ +# Attribution and Lineage + +BlackSTAR is derived from the STAR RNA-seq aligner. + +## Upstream Baseline + +- Project: STAR +- Upstream repository: https://github.com/alexdobin/STAR +- Baseline release: `2.7.11b` +- Baseline commit: `b1edc1208d91a53bf40ebae8669f71d50b994851` +- Original author: Alexander Dobin +- License: MIT + +The original copyright and MIT license are retained in [LICENSE](LICENSE). +BlackSTAR's independent maintenance does not imply endorsement by Alexander +Dobin, Cold Spring Harbor Laboratory, or another upstream contributor or +institution. + +## Scientific Citation + +Research using the STAR alignment method should cite: + +Dobin A, Davis CA, Schlesinger F, Drenkow J, Zaleski C, Jha S, Batut P, +Chaisson M, Gingeras TR. STAR: ultrafast universal RNA-seq aligner. +Bioinformatics. 2013;29(1):15-21. doi:10.1093/bioinformatics/bts635. + +BlackSTAR users should additionally record the exact BlackSTAR release, commit +or package checksum, index identity, and command line. + +## BlackSTAR Scope + +BlackSTAR maintains the inherited alignment core while adding independently +qualified indexing, named-sequence insertion, high-thread runtime, correctness, +packaging, and verification work. The precise boundary is documented in +[docs/BLACKSTAR_RELEASE.md](docs/BLACKSTAR_RELEASE.md) and +[docs/COMPATIBILITY.md](docs/COMPATIBILITY.md). + +Upstream-origin and BlackSTAR-origin behavior are tracked separately in issue +triage and release records. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d0962064 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# BlackSTAR Changelog + +This changelog records BlackSTAR project releases. The inherited STAR history +remains available in `CHANGES.md` and `RELEASEnotes.md`. + +## Unreleased + +- Transition project identity from a GitHub fork to an independently maintained + successor while preserving upstream attribution. +- Establish governance, security, support, compatibility, versioning, and + release policies. +- Add BlackSTAR-owned issue intake, pull-request requirements, and migration + documentation. +- Add a recoverable GitHub fork-detachment procedure and evidence archive. + +No alignment, indexing, or output behavior is changed by the project-identity +work alone. + +## 2.7.11b-blackstar.2 - 2026-07-24 + +- Added the qualified high-thread alignment stack. +- Preserved all accepted full-index and named-sequence insertion behavior from + `blackstar.1`. +- Published deterministic Linux x86-64 artifacts and checksums. + +See [the release notes](docs/releases/2.7.11b-blackstar.2-release-notes.md). + +## 2.7.11b-blackstar.1 - 2026-07-16 + +- Added deterministic parallel full-index construction. +- Added persistent Full, Overlay, and Delta named-sequence insertion. +- Added insert-only GTF support, strict package identity, and atomic + publication. +- Added inherited upstream correctness fixes and deployment rollback tooling. + +See [the acceptance record](docs/releases/2.7.11b-blackstar.1-acceptance.md). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 295de515..8383b5c0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,76 +1,40 @@ -# Contributor Covenant Code of Conduct +# BlackSTAR Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal +Contributors and maintainers pledge to provide a harassment-free, +professional, and welcoming project environment regardless of age, body size, +disability, ethnicity, sex characteristics, gender identity and expression, +experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. -## Our Standards +## Expected Behavior -Examples of behavior that contributes to creating a positive environment -include: +- Use respectful and inclusive language. +- Focus technical disagreement on evidence and project outcomes. +- Accept constructive review and correct mistakes transparently. +- Protect private information and confidential data. +- Respect the project's scientific, security, and compatibility requirements. -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +Harassment, sexualized conduct, threats, personal attacks, deliberate +misrepresentation, disclosure of private information, and other unprofessional +conduct are not acceptable. -Examples of unacceptable behavior by participants include: +## Scope and Enforcement -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +This policy applies in repository issues, pull requests, discussions, release +channels, and other spaces where someone represents BlackSTAR. -## Our Responsibilities +Maintainers may edit or remove content, reject contributions, restrict +participation, or report conduct to GitHub when necessary. Do not publish +sensitive conduct reports in a public issue. Use the private contact methods +listed on the repository owner's GitHub profile or GitHub's abuse-reporting +mechanism. Security vulnerabilities follow [SECURITY.md](SECURITY.md). -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at dobin@cshl.edu. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +Reports will be handled as confidentially as the available communication +channel permits. BlackSTAR cannot guarantee a response time. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +This policy is adapted from the Contributor Covenant, version 1.4: +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08211e89..6a22bf04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,94 +1,105 @@ -# Contributing to STAR +# Contributing to BlackSTAR -The following is a set of guidelines for contributing to STAR hosted in the GitHub: https://github.com/alexdobin/STAR/. -These are mostly guidelines, not rules. -Use your best judgment, and feel free to propose changes to this document in a pull request. +BlackSTAR accepts focused bug fixes, compatibility improvements, +documentation, tests, and performance work. Contributions must preserve +scientific correctness, deterministic output contracts, and the documented +compatibility boundary. -#### Table Of Contents +Participation is governed by [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). -[Code of Conduct](#code-of-conduct) +## Before Opening an Issue -[How Can I Contribute?](#how-can-i-contribute) - * [Ask a question](#ask-a-question) - * [Reporting Bugs](#reporting-bugs) - * [Suggesting Enhancements](#suggesting-enhancements) - * [Pull Requests](#pull-requests) +Use the latest qualified BlackSTAR release and retain: -## Code of Conduct +- the exact `STAR --version` output and executable checksum; +- the complete STAR command line; +- `Log.out` and `Log.final.out`; +- operating system, compiler or package source, CPU, RAM, and storage details; +- input and index identities that can be shared safely; and +- whether official STAR 2.7.11b reproduces the behavior. -This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). -By participating, you are expected to uphold this code. +Do not upload private sequencing data, credentials, or customer information. +Build the smallest public or synthetic reproducer that retains the failure. -## How Can I Contribute? +## Classifying Behavior -### Ask a question +Reports and pull requests must distinguish: -Please do not file an issue to ask a question. -The GitHub issue tracker is intended for bug reports and feature requests. -We have an official discussion forum where the community chimes in with helpful advice if you have questions. +- **Inherited upstream behavior**: official STAR 2.7.11b reproduces it. +- **BlackSTAR regression**: official STAR does not reproduce it and BlackSTAR + violates the compatibility contract. +- **BlackSTAR feature behavior**: the report concerns an explicitly + BlackSTAR-only interface such as Overlay or Delta. +- **Unresolved provenance**: a reproducer is not yet sufficient to classify the + origin. -* [STAR discussion forum](https://groups.google.com/forum/#!forum/rna-star) +This classification prevents inherited STAR debt from being presented as a +BlackSTAR regression while still allowing BlackSTAR to fix upstream defects. -### Reporting Bugs +## Development Setup -This section guides you through submitting a bug report for STAR. -Following these guidelines helps maintainers and the community understand your report, -reproduce the behavior, and find related reports. -If you find a **Closed** issue that seems like it is the same thing that you're experiencing, -open a new issue and include a link to the original issue in the body of your new one. +Build the default binary: -#### Before Submitting A Bug Report +```bash +make -C source -j"$(nproc)" STAR +source/STAR --version +``` -* You might be able to find the cause of the problem and fix things yourself. -Most importantly, check if you can reproduce the problem in the latest version of STAR -and if the problem happens when you run with mostly default parameters. -* Check the Log.out file for ERROR/WARNING/SOLUTION messages. -* Perform a through STAR GitHub issues to see if the problem has already been reported. -If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. +Build the deterministic release package from a clean commit: -#### How Do I Submit A (Good) Bug Report? +```bash +JOBS=16 extras/scripts/buildBlackSTARRelease.sh +``` -Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/) on https://github.com/alexdobin/STAR/issues. -Explain the problem and include additional details to help maintainers reproduce the problem: +Run the focused release checks relevant to a change. Common entry points +include: -* **Use a clear and descriptive title** for the issue to identify the problem. -* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you run STAR, e.g. which command exactly you used in the terminal. When listing steps, **don't just say what you did, but explain how you did it. -* **Log.out**. Attach the Log.out file generated in the failed run. This file contains a lot of useful debugging information and is a starting point -* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). -* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. -* **Explain which behavior you expected to see instead and why.** -* **System information**. In many cases, the problems are associated with the hardware configurations. Provide a brief description of the CPU(s), RAM, storage. -* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. +```bash +extras/tests/scripts/testPackedArray.sh +extras/tests/scripts/testSuffixComparator.sh +extras/tests/scripts/testTranscriptInitialization.sh +extras/tests/scripts/testJunctionAlignment.sh +extras/tests/scripts/testReadChunkConfig.sh +extras/tests/scripts/testGenomeInsertHardening.sh +extras/tests/scripts/testSAindexParallel.sh +extras/tests/scripts/testBenchmarkHarness.sh +``` -Include details about your configuration and environment: +Run architecture validation after changing diagrams or claims: -### Suggesting Enhancements +```bash +python3 extras/docs/render_architecture.py --check --svg-only +python3 extras/docs/validate_architecture.py +``` -This section guides you through submitting an enhancement suggestion for Atom, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions. +## Pull Requests -#### Before Submitting An Enhancement Suggestion +Every pull request must: -* **Check the STAR manual and Release Notes** — you might discover that the enhancement is already available. Most importantly, check if you're using the latest version of STAR. +1. State the user-visible behavior and its origin classification. +2. Describe correctness and compatibility risks. +3. Add or update tests that would fail without the change. +4. Record exact commands used for validation. +5. Avoid unrelated refactoring and generated-file churn. +6. Update documentation and the changelog when behavior changes. +7. Include matched, order-balanced evidence for performance claims. -* **Perform a cursory search** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. +Performance work must compare the candidate with a pinned control binary using +identical inputs, indexes, output modes, storage placement, and thread +allocation. Report wall time, CPU utilization, peak RSS, cache state, and +output-equivalence gates. A faster result with unexplained output differences +is a failed experiment. -#### How Do I Submit A (Good) Enhancement Suggestion? +Generated architecture SVG and PDF artifacts must be regenerated from their +Mermaid sources. Large raw benchmark outputs should remain outside Git; commit +bounded receipts and the scripts needed to reproduce them. -Feature requests and enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/) on https://github.com/alexdobin/STAR/issues. +## Review and Licensing -* **Use a clear and descriptive title** for the issue to identify the suggestion. -* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. -* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). -* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. -* **Explain why this enhancement would be useful** to many STAR users. -* **List some other tools where this enhancement exists.** +Maintainers may request a smaller reproducer, additional platform evidence, or +an upstream comparison before review. Acceptance requires passing CI and the +release gates appropriate to the change. -### Pull Requests - -Please read the guides on creating good pull requests (PR) and follow these guidelines: -* **Use a clear and descriptive title** for the pull request. -* **State the purpose of the PR**: is it a bug-fix, new feature implementation, documentation improvement, or cosmetic change? Why is it important? -* **Explain the expected changes in STAR behavior**. Make sure that the default STAR behavior does not change. -* **Provide detailed code documentation and commit messages**. - -Adopted from https://github.com/atom/atom/blob/master/CONTRIBUTING.md +By submitting a contribution, you certify that you have the right to provide +it under the repository's MIT License. No contributor license agreement is +currently required. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 00000000..8e4b6bfc --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,68 @@ +# BlackSTAR Governance + +## Project Status + +BlackSTAR is an independently maintained open-source successor derived from +STAR 2.7.11b. It is not the official STAR project and does not represent the +original STAR authors or institutions. + +The project currently uses a maintainer-led model. The repository owner is the +initial maintainer and has final responsibility for releases, security +decisions, compatibility policy, and repository administration. This model can +be revised when additional maintainers have demonstrated sustained, +high-quality participation. + +## Decision Process + +Material changes are proposed through GitHub pull requests. Decisions are based +on: + +1. biological and output correctness; +2. compatibility with documented STAR behavior; +3. deterministic and reproducible evidence; +4. operational safety and resource semantics; +5. maintainability; and +6. measured benefit relative to complexity. + +Performance evidence does not override correctness. A proposal can be rejected +when its effect is too narrow, its lifecycle is unsafe, its outputs differ +without explanation, or its maintenance burden exceeds the demonstrated +benefit. + +Substantial interface, index-format, governance, or support-policy changes +require an architecture decision record under `docs/decisions/`. Accepted +decisions identify alternatives, evidence, compatibility impact, and reversal +strategy. + +## Maintainer Responsibilities + +Maintainers: + +- apply the Code of Conduct consistently; +- classify inherited upstream issues separately from BlackSTAR regressions; +- require tests and evidence proportional to the change; +- preserve attribution and licensing; +- publish exact release commits, checksums, provenance, and limitations; +- protect the default branch and release credentials; and +- disclose conflicts of interest relevant to project decisions. + +Maintainers do not promise support response times or acceptance of proposed +features. + +## Releases + +Only commits that pass [docs/RELEASE_POLICY.md](docs/RELEASE_POLICY.md) may be +published as stable releases. The protected default branch is the source of +release tags. Rewriting published history, moving release tags, and replacing +assets without a new release are prohibited. + +Security fixes may use an abbreviated private process, but the final release +must still publish provenance, checksums, compatibility impact, and an +appropriate disclosure. + +## Changing Governance + +Governance changes use the normal pull-request process and require an explicit +maintainer decision. A future multi-maintainer model should define nomination, +review authority, inactivity, removal, and tie-breaking before granting release +or administrative access. diff --git a/README.md b/README.md index 7e9a2d4b..737e2301 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,120 @@ -BlackSTAR 2.7.11b-blackstar.1 -============================ -Spliced Transcripts Alignment to a Reference -© Alexander Dobin, 2009-2024 -https://www.ncbi.nlm.nih.gov/pubmed/23104886 +# BlackSTAR -BlackSTAR is a focused performance and genome-insert fork of STAR 2.7.11b. See [the BlackSTAR release boundary](docs/BLACKSTAR_RELEASE.md) for supported changes and explicit exclusions, [the acceptance record](docs/BLACKSTAR_ACCEPTANCE.md) for current verification status, [the promotion runbook](docs/BLACKSTAR_PROMOTION.md) for canary and rollback, and [the genome-insert guide](docs/STARgenomeInsert.md) for Full, Overlay, and Delta usage. +BlackSTAR is an independently maintained, performance-oriented successor to +the STAR RNA-seq aligner. It is derived from STAR 2.7.11b and retains the +`STAR` executable name and familiar command-line interface for operational +compatibility. -AUTHOR/SUPPORT -============== -Alex Dobin, dobin@cshl.edu
-https://github.com/alexdobin/STAR/issues
-https://groups.google.com/d/forum/rna-star +BlackSTAR is not the official STAR project and is not affiliated with or +endorsed by the original STAR authors. The upstream lineage, license, and +scientific citation are preserved in [ATTRIBUTION.md](ATTRIBUTION.md). -HARDWARE/SOFTWARE REQUIREMENTS -============================== - * x86-64 compatible processors - * 64-bit Linux for the tested BlackSTAR release target +## Why BlackSTAR -The inherited macOS source path remains available but has not been recertified for BlackSTAR-specific features or release artifacts. +The current qualified release adds: -MANUAL -====== -https://github.com/alexdobin/STAR/blob/master/doc/STARmanual.pdf +- deterministic, memory-adaptive parallel full-index construction; +- persistent Full, Overlay, and cached Delta named-sequence insertion; +- insert-only GTF support for added references; +- improved high-thread alignment scheduling, affinity recovery, and NUMA + placement; +- correctness fixes separated from inherited upstream behavior; and +- reproducible release packages, checksums, acceptance evidence, and rollback + tooling. -[RELEASEnotes](https://github.com/alexdobin/STAR/blob/master/RELEASEnotes.md) contains detailed information about the latest major release -[CHANGES](https://github.com/alexdobin/STAR/blob/master/CHANGES.md) contains detailed information about all the changes in all releases +Direct qualification against official STAR 2.7.11b measured: -DIRECTORY CONTENTS -================== - * source: all source files required for compilation - * bin: pre-compiled executables for Linux and Mac OS X - * doc: documentation - * extras: miscellaneous files and scripts +| Workload | Official STAR | BlackSTAR | Result | +| --- | ---: | ---: | ---: | +| Full CHM13 index, 96 threads | 1,256.03 s | 607.18 s | 51.4% less wall time | +| Uncompressed alignment, 96 threads | 82.49 s | 55.20 s | 33.1% less wall time | +| Cold GFP/GST named-sequence addition | 1,225.62 s | 39.04 s | 31.5x speedup | -COMPILING FROM SOURCE -===================== +The full-index improvement used more memory: median peak RSS increased from +52.78 GiB to 77.15 GiB. Alignment gains were workload- and thread-dependent; +the 32-thread result was close to upstream while 64- and 96-thread runs showed +the largest gains. See [docs/PERFORMANCE.md](docs/PERFORMANCE.md) for the +complete claims boundary and [docs/BLACKSTAR_ACCEPTANCE.md](docs/BLACKSTAR_ACCEPTANCE.md) +for release gates. -Download the latest [release from](https://github.com/alexdobin/STAR/releases) and uncompress it --------------------------------------------------------- +## Compatibility -```bash -# Get latest STAR source from releases -wget https://github.com/alexdobin/STAR/archive/2.7.11b.tar.gz -tar -xzf 2.7.11b.tar.gz -cd STAR-2.7.11b +Conventional BlackSTAR indexes and alignment outputs retain the established +STAR interfaces. Full indexes have been validated with official STAR 2.7.11b. +Overlay and Delta packages are BlackSTAR-specific and require +`--genomeLoad NoSharedMemory`. -# Alternatively, get STAR source using git -git clone https://github.com/alexdobin/STAR.git -``` +Compatibility is a tested contract, not an assumption. See +[docs/COMPATIBILITY.md](docs/COMPATIBILITY.md) before adopting a +BlackSTAR-specific feature. + +## Installation -Compile under Linux -------------------- +The latest qualified release is available from the +[BlackSTAR releases page](https://github.com/justinblethrow-cloud/blackSTAR/releases). +Release assets include the `STAR` executable, a deterministic Linux x86-64 +archive, checksums, linkage metadata, build provenance, license, and upstream +attribution. + +Build from source on Linux: ```bash -# Compile -cd STAR/source -make STAR -``` -For processors that do not support AVX extensions, specify the target SIMD architecture, e.g. -``` -make STAR CXXFLAGS_SIMD=sse +git clone https://github.com/justinblethrow-cloud/blackSTAR.git +cd blackSTAR/source +make -j"$(nproc)" STAR +./STAR --version ``` +The release-gated target is 64-bit x86 Linux with GCC and an OpenMP runtime. +The inherited macOS and non-x86 source paths are not currently release +qualified. -Upstream compile guidance for Mac OS X (not BlackSTAR release-gated) -------------------------------------------------------------------- +## Named-Sequence Addition -```bash -# 1. Install brew (http://brew.sh/) -# 2. Install gcc with brew: -$ brew install gcc -# 3. Build STAR: -# run 'make' in the source directory -# note that the path to c++ executable has to be adjusted to its current version -$cd source -$make STARforMacStatic CXX=/usr/local/Cellar/gcc/8.2.0/bin/g++-8 -# 4. Make it availible through the terminal -$cp STAR /usr/local/bin -``` +`--runMode genomeInsert` adds transgenes, controls, decoys, plasmids, or other +named FASTA records to an existing index. Full mode writes a conventional +index. Overlay and Delta write compact packages that refer to an unchanged base +index; Delta caches the insertion plan for reuse. -All platforms - non-standard gcc --------------------------------- +See [docs/STARgenomeInsert.md](docs/STARgenomeInsert.md) for commands, GTF +handling, validation, and package constraints. -If g++ compiler (true g++, not Clang sym-link) is not on the path, you will need to tell `make` where to find it: -```bash -cd source -make STARforMacStatic CXX=/path/to/gcc -``` +## Documentation -If employing STAR only on a single machine or a homogeneously setup cluster, you may aim at helping the compiler to optimize in way that is tailored to your platform. The flags LDFLAGSextra and CXXFLAGSextra are appended to the default optimizations specified in source/Makefile. -``` -# platform-specific optimization for gcc/g++ -make CXXFLAGSextra=-march=native -# together with link-time optimization -make LDFLAGSextra=-flto CXXFLAGSextra="-flto -march=native" -``` +- [Compatibility contract](docs/COMPATIBILITY.md) +- [Performance evidence and limitations](docs/PERFORMANCE.md) +- [Architecture Atlas](docs/architecture/README.md) +- [Release boundary](docs/BLACKSTAR_RELEASE.md) +- [Release acceptance](docs/BLACKSTAR_ACCEPTANCE.md) +- [Versioning policy](docs/VERSIONING.md) +- [Migration from STAR](docs/MIGRATING_FROM_STAR.md) +- [Release process](docs/RELEASE_POLICY.md) -FreeBSD ports -============= +The original STAR manual remains the authoritative reference for inherited +STAR behavior. BlackSTAR documentation takes precedence for BlackSTAR-specific +features and qualified differences. -STAR can be installed on FreeBSD via the FreeBSD ports system. -To install via the binary package, simply run: -``` -pkg install star -``` +## Project Participation + +Use [GitHub Issues](https://github.com/justinblethrow-cloud/blackSTAR/issues) +for reproducible bugs, compatibility reports, and performance regressions. +Use [GitHub Discussions](https://github.com/justinblethrow-cloud/blackSTAR/discussions) +for questions and design exploration. + +Read [CONTRIBUTING.md](CONTRIBUTING.md), [SUPPORT.md](SUPPORT.md), and +[SECURITY.md](SECURITY.md) before opening a report. The project is maintained +on a best-effort basis and does not promise support response times. + +## License and Citation + +BlackSTAR remains available under the MIT License. Retain the upstream +copyright notice when redistributing source or binaries. -LIMITATIONS -=========== -This release was tested with the default parameters for human and mouse genomes. -Mammal genomes require at least 16GB of RAM, ideally 32GB. -Please contact the author for a list of recommended parameters for much larger or much smaller genomes. +Scientific work using the STAR alignment method should continue to cite the +original STAR publication: +> Dobin A, et al. STAR: ultrafast universal RNA-seq aligner. +> Bioinformatics. 2013;29(1):15-21. doi:10.1093/bioinformatics/bts635. -FUNDING -======= -The development of STAR is supported by the National Human Genome Research Institute of -the National Institutes of Health under Award Number R01HG009318. -The content is solely the responsibility of the authors and does not necessarily represent the official views of the National Institutes of Health. +Also record the exact BlackSTAR release tag, executable checksum, genome index +identity, and command line used for reproducibility. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..d313f3ff --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +# BlackSTAR Security Policy + +## Supported Versions + +Security fixes target the latest stable BlackSTAR release and the current +default branch. Older releases are not guaranteed to receive backports. + +## Reporting a Vulnerability + +Use GitHub private vulnerability reporting for the BlackSTAR repository. Do +not open a public issue for suspected memory corruption, arbitrary file access, +unsafe archive behavior, command injection, credential exposure, or another +issue that could put users or data at risk. + +Include: + +- affected release or commit; +- platform and build details; +- minimal reproduction steps; +- expected security impact; +- whether official STAR reproduces the behavior; and +- any proposed embargo constraints. + +Do not include real customer data or credentials. Synthetic inputs are +preferred. + +The project is maintained on a best-effort basis and cannot promise an +acknowledgement or remediation deadline. Maintainers will avoid unnecessary +public disclosure while validating a credible report and will coordinate a +release and advisory when warranted. + +## Release Integrity + +Stable releases publish checksums and build provenance. Consumers should pin a +release tag and verify the downloaded artifact checksum. A GitHub release, +container tag, or package name alone is not sufficient provenance. + +Security reports about bundled third-party code will be classified separately +from BlackSTAR-authored code and coordinated with the relevant upstream when +practical. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..cd54bebd --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,44 @@ +# BlackSTAR Support + +BlackSTAR is maintained on a best-effort basis. There is no guaranteed response +time, service-level agreement, or commitment to support every STAR platform, +parameter combination, or downstream wrapper. + +## Where to Ask + +- Use GitHub Issues for reproducible bugs, compatibility failures, and + performance regressions. +- Use GitHub Discussions for questions, design exploration, and operational + experience that is not yet a reproducible defect. +- Use GitHub private vulnerability reporting for security issues. + +Do not send BlackSTAR reports to the original STAR authors unless the behavior +has independently been reproduced with official STAR and is relevant to that +project. + +## Supported Release Boundary + +The latest stable BlackSTAR release is supported on the platform stated in its +release notes. At present, the release-gated target is x86-64 Linux with an +OpenMP runtime. Older BlackSTAR releases receive documentation support only +unless a maintainer explicitly backports a critical correction. + +Inherited STAR behavior outside BlackSTAR's qualified platform remains +best-effort. macOS, non-x86 architectures, long-read performance, network +storage, and every downstream workflow are not implied to be qualified merely +because the source compiles. + +## Required Diagnostic Information + +A useful report includes: + +- exact BlackSTAR version and executable SHA-256; +- command line and relevant environment variables; +- `Log.out` and `Log.final.out`; +- CPU, RAM, operating system, filesystem, and storage placement; +- index identity and generation command; +- a minimal public or synthetic reproducer; and +- the result of the same reproducer under official STAR 2.7.11b when feasible. + +Remove credentials, customer identifiers, private sequence data, and +presigned URLs before posting. diff --git a/docs/BLACKSTAR_RELEASE.md b/docs/BLACKSTAR_RELEASE.md index 72898cb6..ddaa601e 100644 --- a/docs/BLACKSTAR_RELEASE.md +++ b/docs/BLACKSTAR_RELEASE.md @@ -1,12 +1,13 @@ # BlackSTAR Release Boundary -BlackSTAR `2.7.11b-blackstar.2` is a maintained fork of upstream STAR -`2.7.11b`. It preserves the upstream genome compatibility version while -adding qualified genome-generation, persistent named-sequence insertion, and -high-thread alignment improvements. +BlackSTAR `1.0.0` is an independently maintained successor derived from +upstream STAR `2.7.11b`. It preserves the qualified STAR compatibility and +genome-format boundaries while adding genome-generation, persistent +named-sequence insertion, and high-thread alignment improvements. The +transitional executable lineage token is `2.7.11b-blackstar.3`. -The `blackstar.2` release target is x86-64 Linux. Upstream macOS source support -has not been recertified for the BlackSTAR-specific paths or release builder. +The `1.0.0` release target is x86-64 Linux. Inherited macOS source support has +not been recertified for the BlackSTAR-specific paths or release builder. See [the acceptance record](BLACKSTAR_ACCEPTANCE.md) for closed audit findings, segregated upstream debt, and measured verification. See @@ -57,7 +58,8 @@ changing measured biological outputs. A release candidate must pass: -1. A clean OpenMP build with a fork-specific `--version` string. +1. A clean OpenMP build with the declared legacy `--version` token and + structured `--version-json` identities. 2. Focused ASan/UBSan tests for packed arrays, suffix comparison, transcript initialization, junction records, and SHA-256 identities. 3. Genome-insert equivalence against a full rebuild, including alignments, junctions, and gene counts. 4. Adversarial rejection tests for stale bases, namespace collisions, malformed annotations, package corruption, extra/missing files, and nonempty destinations. @@ -88,7 +90,10 @@ From a clean tagged checkout, build a release package with: JOBS=16 extras/scripts/buildBlackSTARRelease.sh ``` -The builder derives `SOURCE_DATE_EPOCH` from the commit, fixes embedded build provenance, verifies the fork version and OpenMP linkage, and writes a binary, `build-info.tsv`, `ldd.txt`, a deterministic tarball, and SHA-256 checksums under `dist/`. +The builder derives `SOURCE_DATE_EPOCH` from the commit, fixes embedded build +provenance, verifies BlackSTAR identity and OpenMP linkage, and writes a +binary, `build-info.tsv`, `ldd.txt`, an SPDX SBOM, a deterministic tarball, and +SHA-256 checksums under `dist/`. Use `extras/scripts/selectBlackSTAR.sh` for deployment selection. It copies and revalidates a pinned standalone executable into a generation, then atomically diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md new file mode 100644 index 00000000..e9c9c183 --- /dev/null +++ b/docs/COMPATIBILITY.md @@ -0,0 +1,107 @@ +# BlackSTAR Compatibility Contract + +This document defines the compatibility promises made by stable BlackSTAR +releases. Anything not stated here remains best-effort. + +## Compatibility Identities + +BlackSTAR tracks three separate identities: + +| Identity | Meaning | +| --- | --- | +| BlackSTAR release | Independent project release and support boundary | +| STAR compatibility base | Upstream source release from which inherited behavior is evaluated | +| Genome format | `versionGenome` accepted when loading a conventional index | + +For BlackSTAR `1.0.0`, the executable lineage token is +`2.7.11b-blackstar.3`. Its compatibility base is official STAR `2.7.11b` at +commit `b1edc1208d91a53bf40ebae8669f71d50b994851`. The conventional genome format +remains `2.7.4a`. + +## Command-Line Interface + +The installed executable remains named `STAR`. Inherited STAR parameters and +defaults are intended to retain official STAR 2.7.11b behavior unless release +notes identify a qualified correction. + +BlackSTAR adds parameters for named-sequence insertion, input scheduling, and +NUMA policy. Official STAR is not expected to recognize those parameters. + +The project does not promise that log timestamps, absolute paths, performance +counters, thread scheduling, or command-line provenance comments are +byte-identical. + +## Conventional Genome Indexes + +BlackSTAR loads existing conventional STAR indexes whose `versionGenome` +matches the supported format. Conventional full indexes produced by BlackSTAR +use the established file set plus an optional `blackstar.complete.tsv` +integrity manifest. Official STAR ignores that additional file. + +The qualified full-index boundary includes: + +- `Genome`, `SA`, and `SAindex`; +- chromosome names, starts, and lengths; +- genome parameters; +- splice-junction metadata; and +- gene, transcript, and exon sidecars when annotations are supplied. + +Official STAR 2.7.11b successfully aligned against a full BlackSTAR index in +release qualification. BlackSTAR does not promise future official STAR +versions will retain that behavior. + +## Named-Sequence Packages + +`genomeInsert Full` writes a self-contained conventional index. + +Overlay and Delta are BlackSTAR-specific package formats. They: + +- retain their own inserted FASTA and optional GTF; +- refer to a content-identified base index; +- require `--genomeLoad NoSharedMemory`; +- validate strict manifests and payload identities; and +- reject unsupported development-format versions. + +Official STAR cannot load Overlay or Delta packages. Delta's virtual-SA +no-junction path does not support two-pass mapping; use Full mode when two-pass +mapping is required. + +## Alignment Outputs + +Compatibility tests compare applicable timing-independent metrics, splice +junctions, gene counts, and canonicalized BAM records. Byte identity is +required where file order and metadata are specified to be deterministic. + +BlackSTAR does not promise byte-identical BAM container bytes when compression, +headers, record ordering, timestamps, or absolute command paths can vary. It +does require equivalent canonical records for a test that claims BAM +compatibility. + +Changes caused by explicit BlackSTAR-only references are expected. In a +base-versus-Delta test, noninserted biological output must remain equivalent +outside mappings and counts attributable to requested added references. + +## Resource and Runtime Behavior + +Wall time, peak memory, CPU placement, NUMA allocation, I/O scheduling, and +thread utilization are not compatibility surfaces. They are measured +operational properties. + +Full-index acceleration may use materially more RAM than official STAR. +Consumers must apply a host-memory gate rather than infer capacity from +official STAR behavior. + +## Platforms + +The stable release target is x86-64 Linux with an OpenMP runtime. Source paths +for macOS and other architectures are inherited but not release-qualified. +Compilation alone does not establish support. + +## Regression Standard + +A BlackSTAR regression is established when a documented promise fails under a +reproducible test and the failure is not present in the pinned official STAR +control, or when a BlackSTAR-only feature violates its documented contract. + +An inherited upstream defect can still be fixed by BlackSTAR, but it is tracked +as upstream-origin behavior rather than presented as a BlackSTAR regression. diff --git a/docs/INDEPENDENCE_TRANSITION.md b/docs/INDEPENDENCE_TRANSITION.md new file mode 100644 index 00000000..7cffb1ec --- /dev/null +++ b/docs/INDEPENDENCE_TRANSITION.md @@ -0,0 +1,60 @@ +# BlackSTAR Independence Transition + +This runbook converts the GitHub fork into a standalone repository and changes +the protected default branch from `master` to `main`. + +Fork-network detachment is permanent. It is blocked until the repository owner +approves the exact transaction after the recovery receipt passes. + +## Preconditions + +- Released `master` is clean and pinned by commit and tag. +- A remote mirror and `git bundle --all` exist. +- Release assets, PR evidence, Actions evidence, Pages state, repository + settings, and branch protections are exported. +- Every exported file passes SHA-256 verification. +- Local restore drills reproduce both Git reference inventories exactly. +- The successor transition branch passes CI. +- The final recovery archive contains the transition branch and its passing + check evidence. +- No repository transfer, branch deletion, or production integration is + combined with the detachment transaction. + +## Transaction + +1. Freeze pushes and record UTC start time. +2. Requery repository size, child forks, default-branch SHA, releases, and + required checks. +3. Obtain explicit owner approval. +4. Use GitHub's native **Leave fork network** operation. +5. Wait for GitHub to report a standalone repository. +6. Verify `isFork=false`, no parent, exact branch/tag SHAs, and release state. +7. Recreate lost releases and assets from the checksummed archive. +8. Restore Pages, Actions permissions, private vulnerability reporting, and + repository metadata. +9. Rename `master` to `main`. +10. Reapply strict `main` protection and verify required checks. +11. Enable Issues and Discussions, install labels, and disable the wiki. +12. Push the transition branch, open its PR against `main`, and require CI. +13. Record UTC finish time and a machine-readable post-transition receipt. + +## Verification + +- Fresh clone selects `main`. +- `main` equals the expected pre-transition commit before successor changes. +- Historical tags resolve to the original commits. +- Historical release assets match archived SHA-256 values. +- GitHub Pages returns successfully. +- Issue and pull-request templates render. +- A test PR cannot merge without required checks. +- The release binary downloads and verifies. +- Old `master` URLs redirect to `main`. + +## Recovery Boundary + +Before detachment, aborting leaves the GitHub repository unchanged. After +detachment, Git code and assets can be restored from the archive, but the +repository cannot be rejoined automatically to the original fork network. + +Branch renaming and repository settings are reversible. Fork detachment is not. +The archive and transaction receipts must therefore be retained permanently. diff --git a/docs/MIGRATING_FROM_STAR.md b/docs/MIGRATING_FROM_STAR.md new file mode 100644 index 00000000..f874b2d4 --- /dev/null +++ b/docs/MIGRATING_FROM_STAR.md @@ -0,0 +1,74 @@ +# Migrating from Official STAR + +BlackSTAR retains the `STAR` executable and conventional command-line shape so +that evaluation can begin without redesigning an RNA-seq workflow. + +## Recommended Evaluation + +1. Pin official STAR and BlackSTAR by path, version, commit, and executable + SHA-256. +2. Reuse the same conventional genome index for the first alignment comparison. +3. Run one representative sample through both binaries with identical + parameters and resources. +4. Compare timing-independent metrics, junctions, gene counts, and canonical + BAM records. +5. Measure wall time, CPU, peak RSS, storage behavior, and thread placement. +6. Retain official STAR as an explicit fallback until downstream validation is + complete. + +Do not compare runs that use different reference files, annotations, output +modes, decompression commands, thread counts, storage tiers, or concurrent +system load. + +## Full Index Generation + +BlackSTAR accepts the established `--runMode genomeGenerate` interface. +Parallel index construction can use substantially more memory. The direct +CHM13 qualification measured 77.15 GiB median peak RSS for BlackSTAR versus +52.78 GiB for official STAR. + +Capacity-gate the host before choosing the accelerated path. BlackSTAR retains +a lower-memory strategy internally, but operators should still monitor RSS and +avoid memory overcommit. + +## Named-Sequence Addition + +Use `--runMode genomeInsert` when adding new named FASTA records such as +transgenes or controls to a prebuilt base. + +- Full mode produces a self-contained conventional index. +- Overlay mode stores inserted inputs and reconstructs insertions at startup. +- Delta mode additionally caches the insertion plan. + +Insert-only GTF annotations may describe exons on added sequences. Existing +base annotations are retained automatically. See +[STARgenomeInsert.md](STARgenomeInsert.md). + +## Operational Rollback + +Deploy a standalone executable selected by checksum rather than overwriting an +existing STAR binary in place. The included selector supports a pinned +candidate, pinned fallback, runtime canary, and atomic generation switch: + +```bash +extras/scripts/selectBlackSTAR.sh --help +``` + +Publication of a BlackSTAR release does not authorize modification of an +external pipeline. Each production environment requires its own approval, +shadow run, downstream comparison, and rollback owner. + +## Repository Migration + +The independently maintained repository uses `main` as its default branch. +Existing clones created while the repository used `master` can update with: + +```bash +git fetch origin +git branch -m master main +git branch --set-upstream-to=origin/main main +git remote set-head origin --auto +``` + +Clones that use another local working branch need only fetch and update the +remote HEAD. Published release tags remain unchanged. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md new file mode 100644 index 00000000..2467067d --- /dev/null +++ b/docs/PERFORMANCE.md @@ -0,0 +1,93 @@ +# BlackSTAR Performance + +Performance claims apply only to the stated workload, host, binaries, thread +count, storage placement, and cache definition. They are not universal +guarantees. + +## Direct Official STAR Comparison + +The cumulative qualification compared official STAR `2.7.11b` at +`b1edc1208d91a53bf40ebae8669f71d50b994851` with released BlackSTAR +`2.7.11b-blackstar.2` at +`d6fbf932ae2b155ce4f689bce106429ab2bc07f6`. + +The benchmark ran on dedicated Slurm node `ca2`, using Linux x86-64 GCC 13 +binaries, 96 logical CPUs for the primary high-thread measurements, and +node-local SSD. Every timing used seeded, order-balanced pairs. + +### Full CHM13 Index + +CHM13v2 plus ERCC and the production CHM13/Ensembl/HPRC/ERCC GTF used three +pairs: + +| Metric | Official STAR | BlackSTAR | +| --- | ---: | ---: | +| Median wall time | 1,256.03 s | 607.18 s | +| Paired reduction | | 51.44% | +| Speedup | | 2.06x | +| Median peak RSS | 52.78 GiB | 77.15 GiB | + +All 42 substantive index-file comparisons passed byte-for-byte. The speedup +therefore carries a material 46.18% memory cost that must remain visible in +capacity planning. + +### Alignment Scaling + +The public corpus contained 12,768,316 paired 76-base ENCODE reads and used a +common GRCh38/Ensembl 114 index with `GeneCounts`, `NoSharedMemory`, and no +alignment file. + +| Input | Threads | Official STAR | BlackSTAR | Reduction | +| --- | ---: | ---: | ---: | ---: | +| Uncompressed | 1 | 1,613.41 s | 1,524.61 s | 6.17% | +| Uncompressed | 32 | 101.50 s | 99.22 s | 2.38% | +| Uncompressed | 64 | 83.35 s | 61.07 s | 26.73% | +| Uncompressed | 96 | 82.49 s | 55.20 s | 33.11% | +| `zcat` | 1 | 1,633.61 s | 1,522.24 s | 6.68% | +| `zcat` | 32 | 101.14 s | 98.89 s | 2.20% | +| `zcat` | 64 | 84.96 s | 64.88 s | 23.30% | +| `zcat` | 96 | 90.22 s | 63.15 s | 30.75% | + +The 1-thread control shows a modest serial gain. The larger 64- and 96-thread +differences establish improved multicore efficiency. The approximately 2% +32-thread result prevents a general claim that BlackSTAR is 30% faster at all +thread counts. + +All 24 mapping-pair comparisons passed. A separate 2-million-pair unsorted-BAM +check passed canonical record identity. Median peak RSS at 96 threads was +approximately 5.7% lower for BlackSTAR. + +### Named-Sequence Addition + +The test added public GFP and glutathione S-transferase FASTA records plus +insert-only GTF annotations to GRCh38/Ensembl 114. + +| Input-file cache | Official full rebuild | BlackSTAR Delta | Speedup | +| --- | ---: | ---: | ---: | +| Warm | 1,172.64 s | 23.21 s | 50.52x | +| Verified cold | 1,225.62 s | 39.04 s | 31.50x | + +All six cold gates measured zero resident input-file pages before timing. This +controls file-page residency, not powered-off hardware; filesystem metadata, +libraries, and device caches can remain warm. + +All 57 cold-versus-warm artifact checks passed. Full and Delta alignment +matched timing-independent metrics, junctions, gene counts, and canonical BAM +records, with exactly 100 GFP and 100 GST fragments counted in each mode. + +## Evidence + +Bounded machine-readable receipts are committed under +`docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/`. Large raw outputs remain +outside Git. The receipts identify both binaries and preserve every published +aggregate needed to audit the claims. + +## Claim Rules + +- Do not add incremental experiment percentages to cumulative results. +- Do not generalize high-thread results to all thread counts. +- Report memory changes alongside wall-time changes. +- State local, network, cold, and warm storage conditions explicitly. +- Require output-equivalence gates before accepting a speed result. +- Requalify after performance-sensitive code, compiler, dependency, or host + topology changes. diff --git a/docs/RELEASE_POLICY.md b/docs/RELEASE_POLICY.md new file mode 100644 index 00000000..4dc5cf5c --- /dev/null +++ b/docs/RELEASE_POLICY.md @@ -0,0 +1,74 @@ +# BlackSTAR Release Policy + +Stable BlackSTAR releases are immutable, reproducible, evidence-backed +artifacts from the protected default branch. + +## Release Classes + +- Patch releases correct compatible behavior, security, packaging, + documentation, or measured regressions. +- Minor releases add backward-compatible features or material performance work. +- Major releases change a documented compatibility contract. +- Release candidates are explicitly marked prerelease and are not stable + deployment targets. + +## Required Source Gates + +Every stable release must: + +1. use a clean, reviewed commit on protected `main`; +2. pass required CI, architecture validation, and focused sanitizers; +3. pass genome-insert and SAindex strategy suites; +4. verify the declared CLI, index, and output compatibility surfaces; +5. classify inherited upstream defects separately from BlackSTAR regressions; +6. update the changelog, release boundary, acceptance record, and limitations; +7. record compiler, flags, OpenMP linkage, commit, and source-date epoch; and +8. reproduce the package and executable across two clean builds. + +Performance-affecting releases additionally require seeded, order-balanced +candidate/control pairs on a controlled host. Raw outputs remain outside Git; +bounded receipts, identities, methodology, and correctness results are +committed. + +## Published Artifacts + +A Linux x86-64 release publishes: + +- the `STAR` executable; +- a deterministic archive; +- archive and executable SHA-256 values; +- `build-info.tsv`; +- runtime linkage metadata; +- an SBOM; +- build provenance or attestation; +- license and upstream-attribution notices; +- release notes; and +- an acceptance record. + +The release tag points to the exact source commit. Assets are never replaced +silently; a correction requires a new release. + +## Branch and Review Policy + +Changes enter `main` through pull requests. Required checks use stable names so +branch protection cannot be bypassed by renaming a workflow. Linear history, +conversation resolution, no force pushes, and no branch deletion remain +enforced. + +While BlackSTAR has one maintainer, pull requests may be merged after all +required checks pass without an independent approval. A second active +maintainer triggers a governance review and a one-approval CODEOWNERS policy. + +## Security Releases + +Credible vulnerabilities use GitHub's private advisory workflow. The final +release discloses affected versions and mitigation without exposing private +reporter information. Third-party vulnerabilities are identified separately +from BlackSTAR-authored defects. + +## Rollback + +The prior stable release and checksums remain available. A release must not be +published until its fallback binary and runtime canary are identified. +Deployment rollback uses `extras/scripts/selectBlackSTAR.sh`; it is separate +from repository rollback. diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md new file mode 100644 index 00000000..269eb216 --- /dev/null +++ b/docs/VERSIONING.md @@ -0,0 +1,53 @@ +# BlackSTAR Versioning + +BlackSTAR versions the maintained project independently while preserving +machine-readable STAR ancestry and genome-format identities. + +## Version Surfaces + +| Surface | Example | Purpose | +| --- | --- | --- | +| BlackSTAR release | `1.0.0` | Independent API, support, and release boundary | +| STAR compatibility base | `2.7.11b` | Pinned inherited behavior oracle | +| BlackSTAR lineage identity | `2.7.11b-blackstar.3` | Transitional executable identity | +| Genome format | `2.7.4a` | Conventional index loading compatibility | + +These values must not be collapsed into one string. A project release can +change without changing the genome format, and a compatibility-base update can +require a new BlackSTAR major or minor release. + +## Semantic Versioning + +BlackSTAR release tags use `vMAJOR.MINOR.PATCH`. + +- **MAJOR**: a documented incompatible change to BlackSTAR's supported CLI, + package, index, or output contract. +- **MINOR**: a backward-compatible feature or material performance capability. +- **PATCH**: a compatible correctness, security, documentation, packaging, or + performance correction. + +Prereleases use tags such as `v1.1.0-rc.1`. Published tags are immutable. + +## Transition to 1.0 + +`2.7.11b-blackstar.1` and `.2` remain historical release identities. +`v1.0.0` establishes the independently maintained project contract. + +For the 1.x transition, the release package and manifest carry the independent +version while `STAR --version` retains a lineage-shaped value for wrappers that +expect a STAR-like token. A structured version command and `build-info.tsv` +must report both identities. Changing the legacy `--version` token requires a +separate compatibility survey and release decision. + +## Compatibility-Base Updates + +Adopting a later STAR-derived baseline, another fork, or a replacement +implementation requires: + +1. a pinned source and license audit; +2. a behavioral and index-format diff; +3. the full BlackSTAR correctness suite; +4. migration documentation; and +5. an explicit versioning decision. + +The status of upstream development does not remove these requirements. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index bf378b1b..ec5c2fc6 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -30,6 +30,9 @@ customer-derived evidence belong in an untracked internal derivative. 21. [A09 LTO and PGO evaluation](diagrams/svg/F21-toolchain.svg) 22. [Q01 cumulative alignment qualification](diagrams/svg/F22-cumulative-qualification.svg) 23. [BlackSTAR 2.7.11b-blackstar.2 promotion](diagrams/svg/F23-blackstar2-promotion.svg) +24. [Independent-successor transition](diagrams/svg/F24-independent-successor.svg) +25. [Version and compatibility identities](diagrams/svg/F25-version-compatibility.svg) +26. [Governance and release path](diagrams/svg/F26-governance-release.svg) The editable sources are under `diagrams/src/`. The generated SVG and PDF exports are presentation-ready but are never the source of truth. diff --git a/docs/architecture/claims.tsv b/docs/architecture/claims.tsv index 3dd8395d..b5475268 100644 --- a/docs/architecture/claims.tsv +++ b/docs/architecture/claims.tsv @@ -148,3 +148,11 @@ REL2-ALIGN-012 BlackSTAR.2 shared-genome canonical BAM digest e3f67bccb149277f6f REL2-ALIGN-013 BlackSTAR.2 bound-failure control wall times 226.13 / 37.69 seconds 2,000,000 public paired reads; 96 requested threads; OMP_PROC_BIND close; OMP_PLACES cores; one matched pair docs/architecture/evidence/release-2.7.11b-blackstar.2.tsv d21d4e731b3a1da67c86ee5176bc7e74a60dba9144e149a7a537f773bc3bf86f 9998c445c5b87adacd2a4663bd964ce744aea300 failure-mode positive control; not an ordinary-path speed claim REL2-ALIGN-014 BlackSTAR.2 bound-failure control improvement 83.3326 percent same one-pair failure-mode control docs/architecture/evidence/release-2.7.11b-blackstar.2.tsv d21d4e731b3a1da67c86ee5176bc7e74a60dba9144e149a7a537f773bc3bf86f 9998c445c5b87adacd2a4663bd964ce744aea300 descriptive 6.00-fold recovery with exact outputs REL2-ALIGN-015 BlackSTAR.2 release-style functional gates 8 / 24 / 4 sanitizer scripts / insertion checks / SAindex checks cumulative source plus inherited release surface docs/architecture/evidence/release-2.7.11b-blackstar.2.tsv d21d4e731b3a1da67c86ee5176bc7e74a60dba9144e149a7a537f773bc3bf86f 9998c445c5b87adacd2a4663bd964ce744aea300 all gates passed +DIRECT-INDEX-001 Released BlackSTAR reduced median full CHM13 index wall time versus official STAR 51.4419 percent CHM13v2 plus ERCC and production GTF; 96 threads; local SSD; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 direct cumulative official STAR comparison; 42 byte-equivalence checks passed +DIRECT-INDEX-002 Released BlackSTAR full CHM13 index speedup versus official STAR 2.0594 fold same three-pair direct comparison docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 median paired result +DIRECT-INDEX-003 Released BlackSTAR full CHM13 median peak-RSS increase 46.1786 percent same three-pair direct comparison; 52.78 GiB official STAR and 77.15 GiB BlackSTAR docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 memory cost must accompany the index speed claim +DIRECT-ALIGN-001 Released BlackSTAR uncompressed alignment median wall-time reduction at 96 threads 33.1057 percent 12,768,316 public paired 76-base reads; GRCh38; local SSD; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 direct official STAR comparison; mapping and canonical BAM gates passed +DIRECT-ALIGN-002 Released BlackSTAR zcat alignment median wall-time reduction at 96 threads 30.7477 percent same public corpus through zcat; local SSD; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 direct official STAR comparison; mapping gates passed +DIRECT-ALIGN-003 Released BlackSTAR uncompressed alignment reduction at 32 threads 2.3842 percent same public corpus and index; local SSD; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 threshold control; prevents generalizing high-thread gain to every thread count +DIRECT-DELTA-001 Released BlackSTAR verified-cold Delta insertion speedup versus an official STAR full rebuild 31.4988 fold GRCh38 plus GFP and GST FASTA and insert-only GTF; zero resident input pages; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 39.04 versus 1225.62 seconds; mapping equivalence passed +DIRECT-DELTA-002 Released BlackSTAR cold-to-warm Delta artifact comparisons 57/57 comparisons same named-sequence addition benchmark docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 all comparisons passed; GFP and GST each counted 100 fragments diff --git a/docs/architecture/diagrams/pdf/F24-independent-successor.pdf b/docs/architecture/diagrams/pdf/F24-independent-successor.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7eaa9609cfe3c132880718a8387f594b8cd6a351 GIT binary patch literal 27488 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 aPC6M7=qd=D-n>x;8DYt+U(dEQmia%uh;YmR literal 0 HcmV?d00001 diff --git a/docs/architecture/diagrams/src/F24-independent-successor.mmd b/docs/architecture/diagrams/src/F24-independent-successor.mmd new file mode 100644 index 00000000..317bf421 --- /dev/null +++ b/docs/architecture/diagrams/src/F24-independent-successor.mmd @@ -0,0 +1,28 @@ +flowchart LR + FORK["GitHub fork
protected master
blackstar.1 and .2"] --> EXPORT["Export GitHub metadata
release assets + PR and CI evidence"] + EXPORT --> BACKUP["Remote mirror + all-ref bundle
SHA-256 manifest"] + BACKUP --> DRILL["Two local restore drills
exact reference inventories"] + DRILL --> GATE{"Explicit owner approval
for permanent detachment?"} + + GATE -->|No| ABORT["ABORT
fork remains unchanged"] + GATE -->|Yes| DETACH["GitHub Leave fork network
irreversible boundary"] + DETACH --> VERIFY["Verify standalone identity
isFork false + exact refs"] + VERIFY --> RENAME["Rename master to main
restore strict protection"] + RENAME --> RESTORE["Restore releases, Pages,
labels, Issues, Discussions,
security and repository metadata"] + RESTORE --> SUCCESSOR["INDEPENDENT SUCCESSOR
upstream lineage preserved
BlackSTAR owns maintenance"] + + LIMIT["Git history is recoverable;
fork-network membership is not"] -.-> GATE + EXTERNAL["External pipeline adoption
requires separate authorization"] -.-> SUCCESSOR + + classDef current fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef evidence fill:#dbeafe,stroke:#1d4ed8,color:#111827; + classDef decision fill:#fef3c7,stroke:#a16207,color:#111827; + classDef danger fill:#fee2e2,stroke:#b91c1c,color:#111827,stroke-width:3px; + classDef accepted fill:#dcfce7,stroke:#15803d,color:#111827,stroke-width:3px; + classDef note fill:#ffffff,stroke:#6b7280,color:#374151,stroke-dasharray: 5 5; + class FORK current; + class EXPORT,BACKUP,DRILL,VERIFY,RENAME,RESTORE evidence; + class GATE decision; + class DETACH danger; + class SUCCESSOR accepted; + class ABORT,LIMIT,EXTERNAL note; diff --git a/docs/architecture/diagrams/src/F25-version-compatibility.mmd b/docs/architecture/diagrams/src/F25-version-compatibility.mmd new file mode 100644 index 00000000..6cb8c08c --- /dev/null +++ b/docs/architecture/diagrams/src/F25-version-compatibility.mmd @@ -0,0 +1,31 @@ +flowchart TB + RELEASE["BlackSTAR release
1.0.0
support and API boundary"] + EXEC["Executable lineage token
2.7.11b-blackstar.3
wrapper-compatible legacy output"] + BASE["STAR compatibility base
2.7.11b at pinned commit
behavioral oracle"] + FORMAT["Genome format
versionGenome 2.7.4a
conventional index loading"] + + RELEASE --> JSON["STAR --version-json
reports all identities"] + EXEC --> JSON + BASE --> JSON + FORMAT --> JSON + + BASE --> CLI["Inherited CLI and defaults"] + FORMAT --> FULL["Conventional Full indexes"] + RELEASE --> DELTA["BlackSTAR-only Overlay and Delta"] + + CLI --> TESTS["Compatibility gates
metrics + junctions + counts
canonical BAM + byte identity"] + FULL --> TESTS + DELTA --> TESTS + + RULE["Versions change independently;
no one string represents all contracts"] -.-> JSON + + classDef project fill:#dcfce7,stroke:#15803d,color:#111827,stroke-width:3px; + classDef identity fill:#dbeafe,stroke:#1d4ed8,color:#111827; + classDef interface fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef gate fill:#ecfdf5,stroke:#047857,color:#111827; + classDef note fill:#fef3c7,stroke:#a16207,color:#111827,stroke-dasharray: 5 5; + class RELEASE project; + class EXEC,BASE,FORMAT,JSON identity; + class CLI,FULL,DELTA interface; + class TESTS gate; + class RULE note; diff --git a/docs/architecture/diagrams/src/F26-governance-release.mmd b/docs/architecture/diagrams/src/F26-governance-release.mmd new file mode 100644 index 00000000..3e354d07 --- /dev/null +++ b/docs/architecture/diagrams/src/F26-governance-release.mmd @@ -0,0 +1,36 @@ +flowchart LR + REPORT["Issue, proposal,
security report, or experiment"] --> CLASSIFY{"Origin and change class"} + CLASSIFY -->|Inherited| UPSTREAM["Upstream-origin behavior"] + CLASSIFY -->|BlackSTAR| BLACKSTAR["Regression or feature"] + CLASSIFY -->|Security| PRIVATE["Private advisory path"] + + UPSTREAM --> PR["Focused pull request
tests + compatibility impact"] + BLACKSTAR --> PR + PRIVATE --> PR + + PR --> CI["Required CI
GCC + Clang + CodeQL
sanitizers + deterministic tests"] + CI --> TYPE{"Performance-sensitive?"} + TYPE -->|No| ACCEPT["Maintainer decision"] + TYPE -->|Yes| PERF["Pinned matched control
order-balanced pairs
correctness + resource gates"] + PERF --> ACCEPT + + ACCEPT --> MAIN["Protected main
linear history"] + MAIN --> RC["Exact release candidate
two reproducible builds"] + RC --> PACKAGE["STAR + archive + checksums
build metadata + SPDX SBOM
provenance"] + PACKAGE --> TAG["Immutable vMAJOR.MINOR.PATCH
release and acceptance record"] + + FALLBACK["Prior release + canary
remain the rollback boundary"] -.-> TAG + PROD["Repository release is not
external deployment authorization"] -.-> TAG + + classDef intake fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef decision fill:#fef3c7,stroke:#a16207,color:#111827; + classDef work fill:#dbeafe,stroke:#1d4ed8,color:#111827; + classDef gate fill:#ecfdf5,stroke:#047857,color:#111827; + classDef release fill:#dcfce7,stroke:#15803d,color:#111827,stroke-width:3px; + classDef note fill:#ffffff,stroke:#6b7280,color:#374151,stroke-dasharray: 5 5; + class REPORT,UPSTREAM,BLACKSTAR,PRIVATE intake; + class CLASSIFY,TYPE decision; + class PR,PERF work; + class CI,ACCEPT,MAIN,RC gate; + class PACKAGE,TAG release; + class FALLBACK,PROD note; diff --git a/docs/architecture/diagrams/svg/F24-independent-successor.svg b/docs/architecture/diagrams/svg/F24-independent-successor.svg new file mode 100644 index 00000000..5213b606 --- /dev/null +++ b/docs/architecture/diagrams/svg/F24-independent-successor.svg @@ -0,0 +1,2 @@ + +

No

Yes

GitHub fork
protected master
blackstar.1 and .2

Export GitHub metadata
release assets + PR and CI evidence

Remote mirror + all-ref bundle
SHA-256 manifest

Two local restore drills
exact reference inventories

Explicit owner approval
for permanent detachment?

ABORT
fork remains unchanged

GitHub Leave fork network
irreversible boundary

Verify standalone identity
isFork false + exact refs

Rename master to main
restore strict protection

Restore releases, Pages,
labels, Issues, Discussions,
security and repository metadata

INDEPENDENT SUCCESSOR
upstream lineage preserved
BlackSTAR owns maintenance

Git history is recoverable;
fork-network membership is not

External pipeline adoption
requires separate authorization

diff --git a/docs/architecture/diagrams/svg/F25-version-compatibility.svg b/docs/architecture/diagrams/svg/F25-version-compatibility.svg new file mode 100644 index 00000000..8550e94a --- /dev/null +++ b/docs/architecture/diagrams/svg/F25-version-compatibility.svg @@ -0,0 +1,2 @@ + +

BlackSTAR release
1.0.0
support and API boundary

Executable lineage token
2.7.11b-blackstar.3
wrapper-compatible legacy output

STAR compatibility base
2.7.11b at pinned commit
behavioral oracle

Genome format
versionGenome 2.7.4a
conventional index loading

STAR --version-json
reports all identities

Inherited CLI and defaults

Conventional Full indexes

BlackSTAR-only Overlay and Delta

Compatibility gates
metrics + junctions + counts
canonical BAM + byte identity

Versions change independently;
no one string represents all contracts

diff --git a/docs/architecture/diagrams/svg/F26-governance-release.svg b/docs/architecture/diagrams/svg/F26-governance-release.svg new file mode 100644 index 00000000..8c334cbc --- /dev/null +++ b/docs/architecture/diagrams/svg/F26-governance-release.svg @@ -0,0 +1,2 @@ + +

Inherited

BlackSTAR

Security

No

Yes

Issue, proposal,
security report, or experiment

Origin and change class

Upstream-origin behavior

Regression or feature

Private advisory path

Focused pull request
tests + compatibility impact

Required CI
GCC + Clang + CodeQL
sanitizers + deterministic tests

Performance-sensitive?

Maintainer decision

Pinned matched control
order-balanced pairs
correctness + resource gates

Protected main
linear history

Exact release candidate
two reproducible builds

STAR + archive + checksums
build metadata + SPDX SBOM
provenance

Immutable vMAJOR.MINOR.PATCH
release and acceptance record

Prior release + canary
remain the rollback boundary

Repository release is not
external deployment authorization

diff --git a/docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b/docs/architecture/evidence/direct-upstream-comparison-20260724.tsv new file mode 100644 index 00000000..96741a62 --- /dev/null +++ b/docs/architecture/evidence/direct-upstream-comparison-20260724.tsv @@ -0,0 +1,9 @@ +claim_id metric value unit scope source_receipt +DIRECT-INDEX-001 full CHM13 median paired wall-time reduction 51.4419 percent 96 threads; local SSD; three order-balanced pairs docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json +DIRECT-INDEX-002 full CHM13 median speedup 2.0594 fold same paired benchmark docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json +DIRECT-INDEX-003 full CHM13 median peak-RSS increase 46.1786 percent 52.78 GiB official STAR; 77.15 GiB BlackSTAR docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json +DIRECT-ALIGN-001 uncompressed alignment median wall-time reduction 33.1057 percent 96 threads; 12,768,316 public paired reads docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json +DIRECT-ALIGN-002 zcat alignment median wall-time reduction 30.7477 percent 96 threads; same public paired reads docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json +DIRECT-ALIGN-003 uncompressed alignment median wall-time reduction 2.3842 percent 32 threads; same public paired reads docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/alignment-scaling.tsv +DIRECT-DELTA-001 verified-cold named-sequence insertion speedup 31.4988 fold GFP and GST FASTA plus insert-only GTF; three order-balanced pairs docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json +DIRECT-DELTA-002 cold-to-warm Delta artifact comparisons 57/57 comparisons same named-sequence addition benchmark docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json diff --git a/docs/architecture/figures.json b/docs/architecture/figures.json index 3fb4ee02..1bfb18db 100644 --- a/docs/architecture/figures.json +++ b/docs/architecture/figures.json @@ -280,6 +280,42 @@ "caption": "Blackstar.2 promotes the Q01-qualified H01, A02, A05, and A06 alignment stack on top of blackstar.1 while preserving protected-master CI, explicit exclusions, and a separate external-deployment boundary.", "alt_text": "Promotion diagram from upstream STAR 2.7.11b to the qualified blackstar.1 index and insertion release, then through H01 affinity recovery, A02 adaptive input chunks, A05 NUMA-aware private loading, A06 transcript-recursion copy elision, and Q01 cumulative qualification. Replicated performance, exact-output safety, inherited insertion and SAindex compatibility, versioned release packaging, and protected-master build-and-test gates lead to the accepted blackstar.2 release boundary. A01, A02b, A09, alignReadsMulti, prefork workers, and threaded BAM compression remain excluded, and external deployment requires separate authorization.", "outputs": ["docs/architecture/diagrams/svg/F23-blackstar2-promotion.svg", "docs/architecture/diagrams/pdf/F23-blackstar2-promotion.pdf"] + }, + { + "id": "F24", + "title": "Independent-successor transition", + "maturity": "roadmap", + "visibility": "public", + "code_commit": "d6fbf932ae2b155ce4f689bce106429ab2bc07f6", + "source": "docs/architecture/diagrams/src/F24-independent-successor.mmd", + "evidence": ["docs/decisions/0001-independent-successor.md", "docs/INDEPENDENCE_TRANSITION.md", "extras/maintenance/README.md"], + "caption": "BlackSTAR becomes a standalone successor only after a complete metadata export, exact Git restore drills, and explicit approval at the irreversible GitHub fork-detachment boundary.", + "alt_text": "Transition diagram from the protected BlackSTAR GitHub fork through release and pull-request export, a checksummed remote mirror and all-reference bundle, two exact local restore drills, and an explicit owner decision. Approval crosses the irreversible leave-fork-network boundary, followed by standalone verification, master-to-main rename, protection and metadata restoration, and the independently maintained successor. Declining approval leaves the fork unchanged.", + "outputs": ["docs/architecture/diagrams/svg/F24-independent-successor.svg", "docs/architecture/diagrams/pdf/F24-independent-successor.pdf"] + }, + { + "id": "F25", + "title": "Version and compatibility identities", + "maturity": "roadmap", + "visibility": "public", + "code_commit": "d6fbf932ae2b155ce4f689bce106429ab2bc07f6", + "source": "docs/architecture/diagrams/src/F25-version-compatibility.mmd", + "evidence": ["docs/VERSIONING.md", "docs/COMPATIBILITY.md", "source/VERSION", "source/Parameters.cpp"], + "caption": "Independent BlackSTAR release version, executable lineage token, pinned STAR compatibility base, and genome-format version are separate identities exposed together through machine-readable version metadata.", + "alt_text": "Four-part identity diagram showing BlackSTAR release 1.0.0 as the support and API boundary, the wrapper-compatible executable token 2.7.11b-blackstar.3, official STAR 2.7.11b as a pinned behavioral oracle, and genome format 2.7.4a for conventional index loading. All four feed version JSON, while inherited CLI, Full indexes, and BlackSTAR-only Overlay and Delta paths feed explicit compatibility tests.", + "outputs": ["docs/architecture/diagrams/svg/F25-version-compatibility.svg", "docs/architecture/diagrams/pdf/F25-version-compatibility.pdf"] + }, + { + "id": "F26", + "title": "Governance and release path", + "maturity": "roadmap", + "visibility": "public", + "code_commit": "d6fbf932ae2b155ce4f689bce106429ab2bc07f6", + "source": "docs/architecture/diagrams/src/F26-governance-release.mmd", + "evidence": ["GOVERNANCE.md", "CONTRIBUTING.md", "SECURITY.md", "docs/RELEASE_POLICY.md"], + "caption": "BlackSTAR classifies issue origin, requires compatibility and correctness evidence, adds matched performance gates where needed, and publishes only reproducible protected-main release artifacts.", + "alt_text": "Governance flow from issue, proposal, security report, or experiment through upstream-versus-BlackSTAR origin classification and a focused pull request. Required compiler, CodeQL, sanitizer, deterministic, and compatibility checks lead either directly to maintainer review or through matched performance and resource gates. Accepted work reaches protected main, two exact release builds, checksummed artifacts, SPDX SBOM and provenance, then an immutable semantic-version release. Prior release rollback and external deployment authorization remain separate boundaries.", + "outputs": ["docs/architecture/diagrams/svg/F26-governance-release.svg", "docs/architecture/diagrams/pdf/F26-governance-release.pdf"] } ] } diff --git a/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/README.md b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/README.md new file mode 100644 index 00000000..6a1dc91e --- /dev/null +++ b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/README.md @@ -0,0 +1,26 @@ +# Official STAR 2.7.11b versus BlackSTAR blackstar.2 + +These bounded receipts preserve the direct cumulative comparison summarized in +`docs/PERFORMANCE.md`. + +The source benchmark retained full commands, binary and input identities, +timing files, output comparisons, cache-residency receipts, and raw outputs. +Large raw outputs are not committed. The public receipts contain the aggregate +values needed to audit every published performance claim. + +## Correctness Gates + +- Full index: 42/42 byte comparisons passed. +- Mapping matrix: 24/24 pair comparisons passed. +- Unsorted BAM: canonical records matched. +- Cold-cache entry: 6/6 gates measured zero resident input pages. +- Delta determinism: 57/57 cold-versus-warm artifact checks passed. +- Added-reference alignment: metrics, junctions, counts, and canonical BAM + records matched; GFP and GST each counted 100 fragments. + +## Interpretation + +The index result trades higher memory for lower wall time. Alignment benefit is +thresholded, with little change at 32 threads and the largest improvement at +64 and 96 threads. Cold-cache means input file pages were evicted and measured; +it does not mean a power-cycled host. diff --git a/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/alignment-scaling.tsv b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/alignment-scaling.tsv new file mode 100644 index 00000000..9dce4fe7 --- /dev/null +++ b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/alignment-scaling.tsv @@ -0,0 +1,9 @@ +input threads upstream_median_wall_seconds blackstar_median_wall_seconds median_reduction_percent speedup bootstrap_95_low_percent bootstrap_95_high_percent upstream_cv_percent blackstar_cv_percent rss_change_percent correctness_passed +uncompressed 1 1613.41 1524.61 6.167713060234974 1.0582444034867935 5.314210275131553 7.837341167652007 1.5414338586475425 0.6405146757551086 0.031023759639961533 True +uncompressed 32 101.5 99.22 2.384236453201972 1.0229792380568434 2.1297574442910774 3.1527574426549574 0.562898805414159 0.09529134882885744 0.5670278001129777 True +uncompressed 64 83.35 61.07 26.730653869226153 1.3648272474209922 24.165223843680437 28.050507434505544 2.368928941907279 0.2949567470111258 -4.140881684888811 True +uncompressed 96 82.49 55.2 33.10567936736162 1.4943840579710144 33.05845258307058 33.434355679476305 0.6870349065879955 0.8503455639181003 -5.675287356321839 True +zcat 1 1633.61 1522.24 6.683357716958138 1.0731619192768551 5.986198538232272 8.038977593320885 1.1683271086297622 0.13220820135357825 0.03547448205424122 True +zcat 32 101.14 98.89 2.195628523390366 1.0227525533420974 2.1653154043899523 3.4887129873937335 0.6854049132311419 0.0982387207305085 0.5642064016801493 True +zcat 64 84.96000000000001 64.88 23.304448295782787 1.3094944512946982 23.09151256519678 24.223163841807924 1.326861492357367 1.5960432422813329 -4.084223825369816 True +zcat 96 90.22 63.15 30.74770539511977 1.4286619160728424 29.705165151851027 32.24248927038627 2.224997928296499 1.318238893214275 -5.659602852891375 True diff --git a/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/delta-cold-pairs.tsv b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/delta-cold-pairs.tsv new file mode 100644 index 00000000..7f8ca613 --- /dev/null +++ b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/delta-cold-pairs.tsv @@ -0,0 +1,4 @@ +pair first upstream_full_wall_seconds blackstar_delta_wall_seconds reduction_percent speedup +1 blackstar-delta 1164.6 47.29 95.9393783273227 24.626770987523788 +2 upstream-full 1231.56 39.04 96.83003670141935 31.546106557377048 +3 blackstar-delta 1225.62 38.91 96.8252802663142 31.498843484965306 diff --git a/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/index-pairs.tsv b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/index-pairs.tsv new file mode 100644 index 00000000..061336c2 --- /dev/null +++ b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/index-pairs.tsv @@ -0,0 +1,4 @@ +pair first upstream_wall_seconds blackstar_wall_seconds reduction_percent speedup upstream_max_rss_kib blackstar_max_rss_kib +1 blackstar 1305.81 601.45 53.940466070867885 2.1711031673455814 54913544 80895000 +2 upstream 1256.03 622.46 50.44226650637325 2.0178485364521412 55339820 80898280 +3 blackstar 1250.42 607.18 51.4419155163865 2.0593893079482197 56467356 80870792 diff --git a/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json new file mode 100644 index 00000000..df0d9004 --- /dev/null +++ b/docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/summary.json @@ -0,0 +1,62 @@ +{ + "alignment": { + "canonical_bam_check": true, + "mapping_pair_comparisons": 24, + "uncompressed_96_threads": { + "blackstar_median_wall_seconds": 55.2, + "median_reduction_percent": 33.10567936736162, + "speedup": 1.4943840579710144, + "upstream_median_wall_seconds": 82.49 + }, + "zcat_96_threads": { + "blackstar_median_wall_seconds": 63.15, + "median_reduction_percent": 30.74770539511977, + "speedup": 1.4286619160728424, + "upstream_median_wall_seconds": 90.22 + } + }, + "blackstar": { + "binary_sha256": "5d547c4a3ef69893fe5aa0613957448516b95b03eb17fb0e7c46348a13074202", + "revision": "d6fbf932ae2b155ce4f689bce106429ab2bc07f6", + "version": "2.7.11b-blackstar.2" + }, + "full_chm13_index_96_threads": { + "blackstar_median_peak_rss_gib": 77.1474838256836, + "blackstar_median_wall_seconds": 607.18, + "byte_equivalence_checks": 42, + "median_paired_reduction_percent": 51.4419155163865, + "median_speedup": 2.0593893079482197, + "peak_rss_increase_percent": 46.178646768276444, + "upstream_median_peak_rss_gib": 52.77616500854492, + "upstream_median_wall_seconds": 1256.03 + }, + "host": { + "logical_cpus_used": 96, + "name": "ca2", + "storage": "node-local SSD" + }, + "named_sequence_addition": { + "cold_cache_gates": 6, + "cold_input_file_cache": { + "blackstar_delta_median_wall_seconds": 39.04, + "median_reduction_percent": 96.8252802663142, + "median_speedup": 31.498843484965306, + "upstream_full_median_wall_seconds": 1225.62 + }, + "cold_warm_artifact_comparisons": 57, + "gfp_fragments_counted": 100, + "gst_fragments_counted": 100, + "warm_input_file_cache": { + "blackstar_delta_median_wall_seconds": 23.21, + "median_reduction_percent": 98.02070541683722, + "median_speedup": 50.523050409306336, + "upstream_full_median_wall_seconds": 1172.64 + } + }, + "schema": "blackstar-public-direct-comparison-v1", + "upstream": { + "binary_sha256": "2e8d6900ef1c4b2dd78f93a93ce4ff537d87c5263bdc58f1511428315a0cbdaf", + "revision": "b1edc1208d91a53bf40ebae8669f71d50b994851", + "version": "2.7.11b" + } +} diff --git a/docs/decisions/0001-independent-successor.md b/docs/decisions/0001-independent-successor.md new file mode 100644 index 00000000..cdd1c195 --- /dev/null +++ b/docs/decisions/0001-independent-successor.md @@ -0,0 +1,56 @@ +# ADR 0001: Operate BlackSTAR as an Independent Successor + +- Status: Accepted for implementation +- Date: 2026-07-24 +- Decision owner: BlackSTAR maintainer + +## Context + +BlackSTAR began as a performance and genome-insert fork of STAR 2.7.11b. +Upstream contribution attempts did not provide a dependable path for +maintaining the qualified BlackSTAR feature set. BlackSTAR now has an +independent release process, compatibility tests, architecture evidence, and +substantial validated behavior beyond the pinned upstream baseline. + +Remaining in GitHub's fork network presents BlackSTAR primarily as a patch set +even though releases, support, compatibility decisions, and future development +must be maintained independently. + +## Decision + +BlackSTAR will operate as an independently maintained, performance-oriented +successor derived from STAR 2.7.11b. + +The project will: + +- preserve upstream history, license, attribution, and scientific citation; +- state that it is unofficial and not endorsed by upstream; +- retain the `STAR` executable and default interface where compatibility is + qualified; +- version BlackSTAR releases independently while recording the STAR + compatibility base and genome format separately; +- detach from the GitHub fork network only after a verified recovery archive; +- use `main` as the protected default branch; and +- classify inherited upstream behavior separately from BlackSTAR regressions. + +## Alternatives + +Continue as a conventional fork. This minimizes repository administration but +misstates the maintenance and release relationship. + +Wait for upstream adoption. This leaves qualified features and fixes dependent +on an uncertain external process. + +Rewrite or rename the executable immediately. This would create unnecessary +pipeline compatibility risk and is rejected. + +## Consequences + +BlackSTAR assumes responsibility for project governance, security intake, +support boundaries, releases, compatibility, and future maintenance. Fork +detachment is permanent and loses GitHub metadata unless it is exported and +recreated. The project must remain precise about lineage and must not imply +official STAR status. + +This decision does not authorize integration into an external production +pipeline. diff --git a/docs/releases/1.0.0-release-notes.md b/docs/releases/1.0.0-release-notes.md new file mode 100644 index 00000000..5873ef2d --- /dev/null +++ b/docs/releases/1.0.0-release-notes.md @@ -0,0 +1,61 @@ +# BlackSTAR 1.0.0 + +BlackSTAR 1.0.0 establishes BlackSTAR as an independently maintained, +performance-oriented successor derived from STAR 2.7.11b. + +## Project Contract + +- Independent semantic release versioning. +- Explicit STAR compatibility-base and genome-format identities. +- Maintainer-led governance, security, support, and contribution policies. +- Dedicated issue intake for upstream-origin behavior, BlackSTAR regressions, + compatibility, and performance. +- Protected `main` development and reproducible release publication. + +BlackSTAR remains unofficial and is not affiliated with or endorsed by the +original STAR authors. + +## Runtime and Compatibility + +The `STAR` executable name and qualified command-line behavior remain +available. The transitional `STAR --version` token remains STAR-shaped for +wrapper compatibility; `STAR --version-json` reports the independent release, +compatibility base, executable identity, and genome format. + +No alignment, index algorithm, or genome-insert behavior changed solely for the +independence transition. BlackSTAR 1.0.0 carries the qualified `.2` runtime: + +- deterministic parallel full-index construction; +- Full, Overlay, and Delta named-sequence insertion with GTF support; +- high-thread alignment scheduling, affinity, NUMA, and transcript-copy + improvements; and +- strict package identity, reproducible release artifacts, and rollback tools. + +## Direct Official STAR Comparison + +On the qualified 96-thread local-SSD host: + +- full CHM13 index generation used 51.44% less wall time, with 46.18% higher + median peak RSS; +- uncompressed alignment used 33.11% less wall time; +- `zcat` alignment used 30.75% less wall time; and +- cold GFP/GST Delta insertion was 31.50x faster than an official STAR full + rebuild. + +These results are workload-specific. See `docs/PERFORMANCE.md` and the bounded +machine-readable receipts for the complete claim boundary. + +## Platform + +The stable target is x86-64 Linux with an OpenMP runtime. Other inherited source +paths remain unqualified. + +## Artifacts + +The release publishes the `STAR` executable, deterministic archive, checksum +sidecar, build metadata, linkage metadata, SPDX 2.3 SBOM, and GitHub build +provenance. The archive and release assets retain the MIT license and upstream +attribution. + +Publication does not authorize integration into an external production +pipeline. diff --git a/extras/maintenance/README.md b/extras/maintenance/README.md new file mode 100644 index 00000000..512b6ca0 --- /dev/null +++ b/extras/maintenance/README.md @@ -0,0 +1,78 @@ +# BlackSTAR Repository Maintenance + +`export_github_state.py` captures the Git and GitHub state required before an +irreversible repository migration. It exports repository settings, branch +protection, pull-request evidence, releases and assets, current workflow +evidence, Pages state, a remote mirror, and a bundle of all local refs. + +The export intentionally does not retrieve Actions secrets, credentials, or +other secret values. Treat the resulting directory as private operational +evidence because it can still contain collaborator names and repository +metadata. + +Run the exporter only from a clean checkout: + +```bash +python3 extras/maintenance/export_github_state.py \ + --repo OWNER/REPOSITORY \ + --repo-root "$PWD" \ + --remote fork \ + --output /secure/path/transition-backup +``` + +Workflow evidence is captured for both the live default-branch commit and the +clean checkout's local `HEAD`. Use repeated `--evidence-sha` arguments only when +additional commits must be preserved. + +Verify every checksum and exercise both Git restoration paths: + +```bash +python3 extras/maintenance/verify_transition_backup.py \ + --backup /secure/path/transition-backup \ + --receipt /secure/path/restore-receipt.json +``` + +Fork-network detachment remains blocked until the verification receipt reports +`PASS`. + +The intended post-detachment repository state is declared in +`.github/repository-settings.json`. Review that file together with the dry-run +output before approving the transaction. + +Immediately before detachment, compare the live repository with the accepted +archive: + +```bash +python3 extras/maintenance/check_detachment_readiness.py \ + --repo OWNER/REPOSITORY \ + --backup /secure/path/transition-backup \ + --restore-receipt /secure/path/restore-receipt.json \ + --expected-sha EXPECTED_COMMIT \ + --transition-branch SUCCESSOR_BRANCH \ + --transition-sha SUCCESSOR_COMMIT \ + --output /secure/path/detachment-readiness.json +``` + +The post-detachment configurator can be dry-run while the repository is still a +fork. It reports that apply is blocked until GitHub confirms detachment: + +```bash +python3 extras/maintenance/finalize_successor_repo.py \ + --repo OWNER/REPOSITORY \ + --backup /secure/path/transition-backup \ + --readiness-receipt /secure/path/detachment-readiness.json \ + --expected-sha EXPECTED_COMMIT \ + --receipt /secure/path/post-detachment.json +``` + +Apply it only after reviewing the dry run: + +```bash +python3 extras/maintenance/finalize_successor_repo.py \ + --repo OWNER/REPOSITORY \ + --backup /secure/path/transition-backup \ + --readiness-receipt /secure/path/detachment-readiness.json \ + --expected-sha EXPECTED_COMMIT \ + --receipt /secure/path/post-detachment.json \ + --apply --confirmation DETACHMENT-APPROVED +``` diff --git a/extras/maintenance/check_detachment_readiness.py b/extras/maintenance/check_detachment_readiness.py new file mode 100755 index 00000000..dae31d6a --- /dev/null +++ b/extras/maintenance/check_detachment_readiness.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Check the live and archived gates immediately before fork detachment.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +from pathlib import Path +import subprocess +import sys + + +def gh_api(endpoint: str) -> dict: + result = subprocess.run( + ["gh", "api", endpoint], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip()) + return json.loads(result.stdout) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def check_runs(repo: str, commit: str, name: str) -> list[dict]: + values = gh_api(f"repos/{repo}/commits/{commit}/check-runs?per_page=100")[ + "check_runs" + ] + return [value for value in values if value["name"] == name] + + +def check_run_receipts(values: list[dict]) -> list[dict]: + return [ + { + "id": value["id"], + "status": value["status"], + "conclusion": value["conclusion"], + "html_url": value["html_url"], + } + for value in values + ] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True) + parser.add_argument("--backup", type=Path, required=True) + parser.add_argument("--restore-receipt", type=Path, required=True) + parser.add_argument("--expected-sha", required=True) + parser.add_argument("--expected-parent", default="alexdobin/STAR") + parser.add_argument("--required-check", default="build-and-test") + parser.add_argument("--transition-branch") + parser.add_argument("--transition-sha") + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if bool(args.transition_branch) != bool(args.transition_sha): + raise RuntimeError( + "--transition-branch and --transition-sha must be provided together" + ) + + backup = args.backup.resolve() + snapshot = json.loads((backup / "snapshot.json").read_text(encoding="utf-8")) + restore = json.loads(args.restore_receipt.read_text(encoding="utf-8")) + repository = gh_api(f"repos/{args.repo}") + default_branch = repository["default_branch"] + branch = gh_api(f"repos/{args.repo}/branches/{default_branch}") + + failures = [] + if restore.get("status") != "PASS": + failures.append("restore drill did not pass") + if restore.get("backup") != str(backup): + failures.append("restore receipt names a different backup") + manifest = backup / "checksums.sha256" + if restore.get("checksum_manifest_sha256") != sha256(manifest): + failures.append("restore receipt does not match the checksum manifest") + if restore.get("repository") != args.repo: + failures.append("restore receipt names a different repository") + if restore.get("default_branch_commit") != args.expected_sha: + failures.append("restore receipt default-branch commit differs") + if snapshot["repository"] != args.repo: + failures.append("archive names a different repository") + if snapshot["default_branch_commit"] != args.expected_sha: + failures.append("archived default-branch commit differs") + if branch["commit"]["sha"] != args.expected_sha: + failures.append("live default-branch commit differs") + if default_branch != "master": + failures.append("pre-detachment default branch is not master") + if not repository["fork"]: + failures.append("repository is already detached") + elif repository.get("parent", {}).get("full_name") != args.expected_parent: + failures.append("repository has an unexpected fork-network parent") + if repository["size"] >= 1_000_000: + failures.append("repository is not below the native 1 GB detachment limit") + if repository["forks_count"] != 0: + failures.append("repository has child forks") + + archived_releases = json.loads( + (backup / "github" / "releases.json").read_text(encoding="utf-8") + ) + live_releases = gh_api(f"repos/{args.repo}/releases?per_page=100") + archived_tags = sorted(value["tag_name"] for value in archived_releases) + live_tags = sorted(value["tag_name"] for value in live_releases) + if archived_tags != live_tags: + failures.append("live release tags differ from the archive") + + archived_git_tags = { + value["name"]: value["commit"]["sha"] + for value in json.loads( + (backup / "github" / "tags.json").read_text(encoding="utf-8") + ) + } + live_git_tags = { + value["name"]: value["commit"]["sha"] + for value in gh_api(f"repos/{args.repo}/tags?per_page=100") + } + if archived_git_tags != live_git_tags: + failures.append("live Git tag inventory differs from the archive") + + matching_checks = check_runs( + args.repo, args.expected_sha, args.required_check + ) + if not any( + value["status"] == "completed" and value["conclusion"] == "success" + for value in matching_checks + ): + failures.append(f"required check is not successful: {args.required_check}") + + transition = None + if args.transition_branch: + transition_branch = gh_api( + f"repos/{args.repo}/branches/{args.transition_branch}" + ) + transition_checks = check_runs( + args.repo, args.transition_sha, args.required_check + ) + archived_branches = { + value["name"]: value["commit"]["sha"] + for value in json.loads( + (backup / "github" / "branches.json").read_text(encoding="utf-8") + ) + } + evidence_commits = snapshot.get("workflow_evidence_commits", []) + if transition_branch["commit"]["sha"] != args.transition_sha: + failures.append("live transition branch commit differs") + if archived_branches.get(args.transition_branch) != args.transition_sha: + failures.append("archived transition branch commit differs") + if args.transition_sha not in evidence_commits: + failures.append("transition SHA is absent from archived workflow evidence") + if not any( + value["status"] == "completed" and value["conclusion"] == "success" + for value in transition_checks + ): + failures.append( + f"transition check is not successful: {args.required_check}" + ) + transition = { + "branch": args.transition_branch, + "commit": args.transition_sha, + "required_check_runs": check_run_receipts(transition_checks), + } + + receipt = { + "checked_utc": dt.datetime.now(dt.timezone.utc).isoformat(), + "repository": args.repo, + "ready": not failures, + "irreversible_operation": "leave GitHub fork network", + "live_default_branch": default_branch, + "live_default_branch_commit": branch["commit"]["sha"], + "repository_size_kb": repository["size"], + "child_forks": repository["forks_count"], + "fork_parent": repository.get("parent", {}).get("full_name"), + "archived_release_tags": archived_tags, + "archived_git_tags": archived_git_tags, + "required_check": args.required_check, + "required_check_runs": check_run_receipts(matching_checks), + "transition": transition, + "restore_drill": restore.get("status"), + "failures": failures, + } + rendered = json.dumps(receipt, indent=2, sort_keys=True) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered, encoding="utf-8") + print(rendered, end="") + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (RuntimeError, OSError, KeyError, json.JSONDecodeError) as error: + print(f"ERROR: {error}", file=sys.stderr) + sys.exit(1) diff --git a/extras/maintenance/export_github_state.py b/extras/maintenance/export_github_state.py new file mode 100755 index 00000000..bf4cf4ab --- /dev/null +++ b/extras/maintenance/export_github_state.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +"""Export the Git and GitHub state needed before an irreversible migration.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +from typing import Any + + +SCRIPT_VERSION = "2" + + +def run( + args: list[str], + *, + cwd: Path | None = None, + check: bool = True, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + args, + cwd=cwd, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if check and result.returncode != 0: + command = " ".join(args) + raise RuntimeError(f"{command} failed:\n{result.stderr.strip()}") + return result + + +def write_text(path: Path, value: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value, encoding="utf-8") + + +def write_json(path: Path, value: Any) -> None: + write_text(path, json.dumps(value, indent=2, sort_keys=True) + "\n") + + +def gh_api(endpoint: str, *, paginate: bool = False) -> Any: + args = ["gh", "api"] + if paginate: + args.append("--paginate") + args.append(endpoint) + result = run(args) + if not paginate: + return json.loads(result.stdout) + + decoder = json.JSONDecoder() + offset = 0 + flattened: list[Any] = [] + while offset < len(result.stdout): + while offset < len(result.stdout) and result.stdout[offset].isspace(): + offset += 1 + if offset == len(result.stdout): + break + page, offset = decoder.raw_decode(result.stdout, offset) + if isinstance(page, list): + flattened.extend(page) + else: + flattened.append(page) + return flattened + + +def optional_gh_api(endpoint: str, *, paginate: bool = False) -> Any: + try: + return gh_api(endpoint, paginate=paginate) + except (RuntimeError, json.JSONDecodeError) as error: + return {"unavailable": True, "reason": str(error)} + + +def ref_inventory(git_dir: Path | None, repo_root: Path | None = None) -> str: + args = ["git"] + if git_dir is not None: + args.extend([f"--git-dir={git_dir}"]) + args.extend( + [ + "for-each-ref", + "--format=%(refname)\t%(objectname)\t%(objecttype)", + ] + ) + return run(args, cwd=repo_root).stdout + + +def checksum(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def write_checksums(root: Path) -> None: + checksum_path = root / "checksums.sha256" + rows = [] + for path in sorted(root.rglob("*")): + if not path.is_file() or path == checksum_path: + continue + rows.append(f"{checksum(path)} {path.relative_to(root)}") + write_text(checksum_path, "\n".join(rows) + "\n") + + +def export_pull_request(repo: str, number: int, output: Path) -> None: + pull_root = output / "github" / "pulls" / str(number) + endpoints = { + "pull.json": f"repos/{repo}/pulls/{number}", + "issue-comments.json": f"repos/{repo}/issues/{number}/comments?per_page=100", + "review-comments.json": f"repos/{repo}/pulls/{number}/comments?per_page=100", + "reviews.json": f"repos/{repo}/pulls/{number}/reviews?per_page=100", + "commits.json": f"repos/{repo}/pulls/{number}/commits?per_page=100", + "files.json": f"repos/{repo}/pulls/{number}/files?per_page=100", + } + for filename, endpoint in endpoints.items(): + write_json( + pull_root / filename, + optional_gh_api(endpoint, paginate=endpoint.endswith("per_page=100")), + ) + diff = run( + ["gh", "pr", "diff", str(number), "--repo", repo], + check=False, + ) + write_text(pull_root / "diff.patch", diff.stdout) + if diff.returncode != 0: + write_text(pull_root / "diff.error.txt", diff.stderr) + + +def export_release(repo: str, tag: str, output: Path) -> None: + release_root = output / "github" / "releases" / tag + release_root.mkdir(parents=True, exist_ok=True) + fields = ( + "name,tagName,isDraft,isPrerelease,publishedAt,targetCommitish,url," + "assets,body,author,createdAt" + ) + result = run( + [ + "gh", + "release", + "view", + tag, + "--repo", + repo, + "--json", + fields, + ] + ) + write_json(release_root / "release.json", json.loads(result.stdout)) + assets = release_root / "assets" + assets.mkdir(exist_ok=True) + download = run( + [ + "gh", + "release", + "download", + tag, + "--repo", + repo, + "--dir", + str(assets), + ], + check=False, + ) + if download.returncode != 0: + write_text(release_root / "download.error.txt", download.stderr) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True, help="GitHub OWNER/REPO") + parser.add_argument("--repo-root", type=Path, required=True) + parser.add_argument( + "--remote", + default="fork", + help="Local remote that tracks the GitHub repository (default: fork)", + ) + parser.add_argument( + "--evidence-sha", + action="append", + default=[], + help="Additional workflow head SHA to archive; may be repeated", + ) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + os.umask(0o077) + repo_root = args.repo_root.resolve() + output = args.output.resolve() + if output.exists(): + raise SystemExit(f"output already exists: {output}") + output.mkdir(parents=True) + + for executable in ("git", "gh"): + if shutil.which(executable) is None: + raise SystemExit(f"required executable not found: {executable}") + + status = run(["git", "status", "--porcelain"], cwd=repo_root).stdout + if status: + raise SystemExit("repository must be clean before export") + + repo_api = f"repos/{args.repo}" + repository = gh_api(repo_api) + default_branch = repository["default_branch"] + live_branch = gh_api(f"{repo_api}/branches/{default_branch}") + head_commit = live_branch["commit"]["sha"] + local_remote_ref = f"{args.remote}/{default_branch}" + local_remote_commit = run( + ["git", "rev-parse", local_remote_ref], + cwd=repo_root, + ).stdout.strip() + if local_remote_commit != head_commit: + raise SystemExit( + f"{local_remote_ref} is stale: {local_remote_commit} != {head_commit}" + ) + + local_head = run(["git", "rev-parse", "HEAD"], cwd=repo_root).stdout.strip() + evidence_commits = sorted({head_commit, local_head, *args.evidence_sha}) + metadata = { + "schema_version": SCRIPT_VERSION, + "created_utc": dt.datetime.now(dt.timezone.utc).isoformat(), + "repository": args.repo, + "repository_url": repository["html_url"], + "default_branch": default_branch, + "default_branch_commit": head_commit, + "local_remote": args.remote, + "local_remote_commit": local_remote_commit, + "local_head": local_head, + "workflow_evidence_commits": evidence_commits, + "git_version": run(["git", "--version"]).stdout.strip(), + "gh_version": run(["gh", "--version"]).stdout.splitlines()[0], + } + write_json(output / "snapshot.json", metadata) + + github_root = output / "github" + endpoints = { + "repository.json": repo_api, + "pages.json": f"{repo_api}/pages", + "rulesets.json": f"{repo_api}/rulesets", + "actions-permissions.json": f"{repo_api}/actions/permissions", + "environments.json": f"{repo_api}/environments", + "community-profile.json": f"{repo_api}/community/profile", + "private-vulnerability-reporting.json": ( + f"{repo_api}/private-vulnerability-reporting" + ), + "workflows.json": f"{repo_api}/actions/workflows?per_page=100", + "workflow-runs.json": f"{repo_api}/actions/runs?per_page=100", + } + for filename, endpoint in endpoints.items(): + write_json(github_root / filename, optional_gh_api(endpoint)) + + paginated = { + "branches.json": f"{repo_api}/branches?per_page=100", + "tags.json": f"{repo_api}/tags?per_page=100", + "releases.json": f"{repo_api}/releases?per_page=100", + "pulls.json": f"{repo_api}/pulls?state=all&per_page=100", + "labels.json": f"{repo_api}/labels?per_page=100", + "collaborators.json": f"{repo_api}/collaborators?per_page=100", + } + exported: dict[str, Any] = {} + for filename, endpoint in paginated.items(): + value = optional_gh_api(endpoint, paginate=True) + exported[filename] = value + write_json(github_root / filename, value) + + branches = exported["branches.json"] + if isinstance(branches, list): + for branch in branches: + if not branch.get("protected"): + continue + name = branch["name"] + protection = optional_gh_api(f"{repo_api}/branches/{name}/protection") + write_json(github_root / "branch-protection" / f"{name}.json", protection) + + pulls = exported["pulls.json"] + if isinstance(pulls, list): + for pull in pulls: + export_pull_request(args.repo, int(pull["number"]), output) + + releases = exported["releases.json"] + if isinstance(releases, list): + for release in releases: + export_release(args.repo, release["tag_name"], output) + + workflow_runs = optional_gh_api(f"{repo_api}/actions/runs?per_page=100") + if isinstance(workflow_runs, dict): + for workflow_run in workflow_runs.get("workflow_runs", []): + if workflow_run.get("head_sha") not in evidence_commits: + continue + run_id = str(workflow_run["id"]) + run_root = github_root / "workflow-run-evidence" / run_id + run_root.mkdir(parents=True, exist_ok=True) + log_result = run( + ["gh", "run", "view", run_id, "--repo", args.repo, "--log"], + check=False, + ) + write_text(run_root / "log.txt", log_result.stdout) + if log_result.returncode != 0: + write_text(run_root / "log.error.txt", log_result.stderr) + artifact_result = run( + [ + "gh", + "run", + "download", + run_id, + "--repo", + args.repo, + "--dir", + str(run_root / "artifacts"), + ], + check=False, + ) + if artifact_result.returncode != 0: + write_text( + run_root / "artifact-download.error.txt", + artifact_result.stderr, + ) + + git_root = output / "git" + git_root.mkdir() + write_text(git_root / "local-refs.tsv", ref_inventory(None, repo_root)) + write_text( + git_root / "local-remotes.txt", + run(["git", "remote", "-v"], cwd=repo_root).stdout, + ) + write_text( + git_root / "local-fsck.txt", + run(["git", "fsck", "--full"], cwd=repo_root).stdout, + ) + run( + ["git", "bundle", "create", str(git_root / "local-all.bundle"), "--all"], + cwd=repo_root, + ) + + mirror = git_root / "remote-mirror.git" + run(["git", "clone", "--mirror", repository["clone_url"], str(mirror)]) + write_text(git_root / "remote-refs.tsv", ref_inventory(mirror)) + write_text( + git_root / "remote-fsck.txt", + run(["git", f"--git-dir={mirror}", "fsck", "--full"]).stdout, + ) + + wiki = git_root / "wiki.git" + wiki_result = run( + [ + "git", + "clone", + "--mirror", + repository["clone_url"].removesuffix(".git") + ".wiki.git", + str(wiki), + ], + check=False, + ) + if wiki_result.returncode != 0: + write_text(git_root / "wiki-unavailable.txt", wiki_result.stderr) + + write_checksums(output) + print(output) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except RuntimeError as error: + print(f"ERROR: {error}", file=sys.stderr) + sys.exit(1) diff --git a/extras/maintenance/finalize_successor_repo.py b/extras/maintenance/finalize_successor_repo.py new file mode 100755 index 00000000..f5cb6153 --- /dev/null +++ b/extras/maintenance/finalize_successor_repo.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +"""Idempotently configure BlackSTAR after GitHub fork detachment.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import tempfile +from typing import Any +from urllib.parse import quote + + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_SETTINGS = REPO_ROOT / ".github" / "repository-settings.json" + + +def run( + args: list[str], + *, + input_text: str | None = None, + check: bool = True, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + args, + input=input_text, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if check and result.returncode != 0: + raise RuntimeError(f"{' '.join(args)} failed:\n{result.stderr.strip()}") + return result + + +def gh_api( + endpoint: str, + *, + method: str = "GET", + payload: Any | None = None, + check: bool = True, +) -> Any: + args = ["gh", "api", "-X", method, endpoint] + input_text = None + if payload is not None: + args.extend(["--input", "-"]) + input_text = json.dumps(payload) + result = run(args, input_text=input_text, check=check) + if result.returncode != 0: + return None + if not result.stdout.strip(): + return {} + return json.loads(result.stdout) + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def download_release_assets(repo: str, tag: str, output: Path) -> bool: + result = run( + [ + "gh", + "release", + "download", + tag, + "--repo", + repo, + "--dir", + str(output), + ], + check=False, + ) + return result.returncode == 0 + + +def restore_releases(repo: str, backup: Path) -> list[dict[str, Any]]: + release_root = backup / "github" / "releases" + results = [] + for directory in sorted(path for path in release_root.iterdir() if path.is_dir()): + release = json.loads( + (directory / "release.json").read_text(encoding="utf-8") + ) + tag = release["tagName"] + current = run( + [ + "gh", + "release", + "view", + tag, + "--repo", + repo, + "--json", + "body,isDraft,isPrerelease,name,tagName", + ], + check=False, + ) + expected = { + path.name: file_sha256(path) + for path in (directory / "assets").iterdir() + if path.is_file() + } + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", delete=False + ) as notes: + notes.write(release["body"]) + notes_path = Path(notes.name) + try: + if current.returncode != 0: + command = [ + "gh", + "release", + "create", + tag, + "--repo", + repo, + "--verify-tag", + "--title", + release["name"], + "--notes-file", + str(notes_path), + ] + if release["isPrerelease"]: + command.append("--prerelease") + if release["isDraft"]: + command.append("--draft") + command.extend( + str(path) for path in sorted((directory / "assets").iterdir()) + ) + run(command) + else: + run( + [ + "gh", + "release", + "edit", + tag, + "--repo", + repo, + "--title", + release["name"], + "--notes-file", + str(notes_path), + f"--draft={str(release['isDraft']).lower()}", + f"--prerelease={str(release['isPrerelease']).lower()}", + ] + ) + finally: + notes_path.unlink(missing_ok=True) + + with tempfile.TemporaryDirectory(prefix=f"blackstar-release-{tag}.") as temp: + downloaded = Path(temp) + download_release_assets(repo, tag, downloaded) + actual = { + path.name: file_sha256(path) + for path in downloaded.iterdir() + if path.is_file() + } + if actual != expected: + assets = [ + str(path) + for path in sorted((directory / "assets").iterdir()) + if path.is_file() + ] + if assets: + run( + [ + "gh", + "release", + "upload", + tag, + "--repo", + repo, + "--clobber", + *assets, + ] + ) + for path in downloaded.iterdir(): + if path.is_file(): + path.unlink() + download_release_assets(repo, tag, downloaded) + actual = { + path.name: file_sha256(path) + for path in downloaded.iterdir() + if path.is_file() + } + if actual != expected: + raise RuntimeError(f"release asset digest mismatch for {tag}") + results.append({"tag": tag, "asset_count": len(expected), "status": "PASS"}) + return results + + +def configure_label(repo: str, name: str, color: str, description: str) -> None: + endpoint = f"repos/{repo}/labels/{quote(name, safe='')}" + existing = gh_api(endpoint, check=False) + payload = {"name": name, "color": color, "description": description} + if existing is None: + gh_api(f"repos/{repo}/labels", method="POST", payload=payload) + else: + gh_api(endpoint, method="PATCH", payload=payload) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True) + parser.add_argument("--backup", type=Path, required=True) + parser.add_argument("--settings", type=Path, default=DEFAULT_SETTINGS) + parser.add_argument("--readiness-receipt", type=Path) + parser.add_argument("--readiness-max-age-hours", type=float, default=24.0) + parser.add_argument("--expected-sha", required=True) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--apply", action="store_true") + parser.add_argument( + "--confirmation", + help="Required with --apply: DETACHMENT-APPROVED", + ) + args = parser.parse_args() + + settings = json.loads(args.settings.read_text(encoding="utf-8")) + if settings.get("schema_version") != 1: + raise RuntimeError("unsupported repository-settings schema") + repository_settings = settings["repository"] + target_branch = repository_settings["default_branch"] + + repository = gh_api(f"repos/{args.repo}") + if args.apply and repository["fork"]: + raise RuntimeError("repository remains in a fork network") + if args.apply and args.confirmation != "DETACHMENT-APPROVED": + raise RuntimeError("explicit post-detachment confirmation is missing") + readiness = None + if args.readiness_receipt: + readiness = json.loads(args.readiness_receipt.read_text(encoding="utf-8")) + checked = dt.datetime.fromisoformat(readiness["checked_utc"]) + age = dt.datetime.now(dt.timezone.utc) - checked + if age.total_seconds() < -300: + raise RuntimeError("readiness receipt timestamp is in the future") + if age > dt.timedelta(hours=args.readiness_max_age_hours): + raise RuntimeError("readiness receipt is stale") + if not readiness.get("ready"): + raise RuntimeError("readiness receipt did not pass") + if readiness.get("repository") != args.repo: + raise RuntimeError("readiness receipt names a different repository") + if readiness.get("live_default_branch_commit") != args.expected_sha: + raise RuntimeError("readiness receipt names a different commit") + if args.apply and readiness.get("transition") is None: + raise RuntimeError("readiness receipt does not verify a transition branch") + elif args.apply: + raise RuntimeError("apply requires a fresh detachment-readiness receipt") + + branches = { + value["name"]: value + for value in gh_api(f"repos/{args.repo}/branches?per_page=100") + } + source_branch = "main" if "main" in branches else "master" + if source_branch not in branches: + raise RuntimeError("neither main nor master exists") + if branches[source_branch]["commit"]["sha"] != args.expected_sha: + raise RuntimeError("default source branch does not match expected SHA") + + planned = { + "current_is_fork": repository["fork"], + "apply_blocked_until_detached": repository["fork"], + "readiness_receipt": ( + { + "path": str(args.readiness_receipt.resolve()), + "checked_utc": readiness["checked_utc"], + "ready": readiness["ready"], + } + if readiness is not None + else None + ), + "rename": source_branch != target_branch, + "default_branch": target_branch, + "repository": repository_settings, + "branch_protection": settings["branch_protection"], + "labels": settings["labels"], + "pages": settings["pages"], + "release_tags": sorted( + path.name + for path in (args.backup / "github" / "releases").iterdir() + if path.is_dir() + ), + } + if not args.apply: + print(json.dumps({"mode": "dry-run", "planned": planned}, indent=2)) + return 0 + + if source_branch != target_branch: + gh_api( + f"repos/{args.repo}/branches/{source_branch}/rename", + method="POST", + payload={"new_name": target_branch}, + ) + + features = repository_settings["features"] + gh_api( + f"repos/{args.repo}", + method="PATCH", + payload={ + "default_branch": target_branch, + "description": repository_settings["description"], + "homepage": repository_settings["homepage"], + "has_issues": features["issues"], + "has_discussions": features["discussions"], + "has_wiki": features["wiki"], + }, + ) + gh_api( + f"repos/{args.repo}/topics", + method="PUT", + payload={"names": repository_settings["topics"]}, + ) + if repository_settings["private_vulnerability_reporting"]: + gh_api( + f"repos/{args.repo}/private-vulnerability-reporting", + method="PUT", + ) + + gh_api( + f"repos/{args.repo}/branches/{target_branch}/protection", + method="PUT", + payload=settings["branch_protection"], + ) + for name, label in settings["labels"].items(): + configure_label(args.repo, name, label["color"], label["description"]) + + pages_settings = settings["pages"] + pages_payload = { + "source": { + "branch": pages_settings["branch"], + "path": pages_settings["path"], + } + } + pages = gh_api(f"repos/{args.repo}/pages", check=False) + if pages is None: + gh_api( + f"repos/{args.repo}/pages", + method="POST", + payload=pages_payload, + ) + else: + gh_api( + f"repos/{args.repo}/pages", + method="PUT", + payload=pages_payload, + ) + + releases = restore_releases(args.repo, args.backup.resolve()) + final_repository = gh_api(f"repos/{args.repo}") + final_main = gh_api(f"repos/{args.repo}/branches/{target_branch}") + final_protection = gh_api( + f"repos/{args.repo}/branches/{target_branch}/protection" + ) + if final_repository["fork"]: + raise RuntimeError("repository unexpectedly reports fork status") + if final_repository["default_branch"] != target_branch: + raise RuntimeError(f"default branch is not {target_branch}") + if final_main["commit"]["sha"] != args.expected_sha: + raise RuntimeError("main SHA changed during configuration") + + receipt = { + "status": "PASS", + "completed_utc": dt.datetime.now(dt.timezone.utc).isoformat(), + "repository": args.repo, + "is_fork": final_repository["fork"], + "default_branch": final_repository["default_branch"], + "default_branch_commit": final_main["commit"]["sha"], + "required_status_checks": final_protection["required_status_checks"], + "releases": releases, + "settings": planned, + } + args.receipt.parent.mkdir(parents=True, exist_ok=True) + args.receipt.write_text( + json.dumps(receipt, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(receipt, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (RuntimeError, OSError, KeyError, json.JSONDecodeError) as error: + print(f"ERROR: {error}", file=sys.stderr) + sys.exit(1) diff --git a/extras/maintenance/validate_successor_metadata.py b/extras/maintenance/validate_successor_metadata.py new file mode 100755 index 00000000..0151b4e6 --- /dev/null +++ b/extras/maintenance/validate_successor_metadata.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Validate BlackSTAR successor metadata and bounded benchmark receipts.""" + +from __future__ import annotations + +import csv +import json +from pathlib import Path +import re +import statistics +import sys + +try: + import yaml +except ImportError as error: + raise SystemExit("PyYAML is required to validate GitHub metadata") from error + + +def macro(path: Path, name: str) -> str: + pattern = re.compile(rf'^#define {re.escape(name)} "([^"]+)"$') + for line in path.read_text(encoding="utf-8").splitlines(): + match = pattern.match(line) + if match: + return match.group(1) + raise ValueError(f"missing {name} in {path}") + + +def read_tsv(path: Path) -> list[dict[str, str]]: + with path.open(encoding="utf-8", newline="") as stream: + return list(csv.DictReader(stream, delimiter="\t")) + + +def assert_close(actual: float, expected: float, label: str) -> None: + if abs(actual - expected) > 1e-9: + raise ValueError(f"{label}: {actual} != {expected}") + + +def main() -> int: + repo = Path(__file__).resolve().parents[2] + required = [ + "README.md", + "ATTRIBUTION.md", + "GOVERNANCE.md", + "SECURITY.md", + "SUPPORT.md", + "CONTRIBUTING.md", + "CHANGELOG.md", + "docs/COMPATIBILITY.md", + "docs/BLACKSTAR_RELEASE.md", + "docs/PERFORMANCE.md", + "docs/MIGRATING_FROM_STAR.md", + "docs/VERSIONING.md", + "docs/RELEASE_POLICY.md", + "docs/INDEPENDENCE_TRANSITION.md", + "docs/releases/1.0.0-release-notes.md", + ".github/repository-settings.json", + ] + for relative in required: + if not (repo / relative).is_file(): + raise ValueError(f"required successor document missing: {relative}") + + version_file = repo / "source" / "VERSION" + blackstar_version = macro(version_file, "BLACKSTAR_VERSION") + executable_version = macro(version_file, "STAR_VERSION") + compatibility_version = macro(version_file, "STAR_COMPATIBILITY_VERSION") + genome_format_version = macro( + version_file, "BLACKSTAR_GENOME_FORMAT_VERSION" + ) + if not re.fullmatch(r"\d+\.\d+\.\d+", blackstar_version): + raise ValueError(f"BlackSTAR version is not stable SemVer: {blackstar_version}") + if "blackstar" not in executable_version: + raise ValueError("executable version does not preserve BlackSTAR identity") + if compatibility_version != "2.7.11b": + raise ValueError("unexpected STAR compatibility base") + + defaults = (repo / "source" / "parametersDefault").read_text(encoding="utf-8") + default_match = re.search(r"^versionGenome\s+(\S+)", defaults, re.MULTILINE) + if not default_match or default_match.group(1) != genome_format_version: + raise ValueError("genome-format macro differs from parametersDefault") + + for path in sorted((repo / ".github").rglob("*.yml")): + with path.open(encoding="utf-8") as stream: + yaml.safe_load(stream) + for path in sorted((repo / ".github").rglob("*.yaml")): + with path.open(encoding="utf-8") as stream: + yaml.safe_load(stream) + for path in sorted((repo / "docs").rglob("*.json")) + sorted( + (repo / ".github").rglob("*.json") + ): + with path.open(encoding="utf-8") as stream: + json.load(stream) + + readme = (repo / "README.md").read_text(encoding="utf-8") + contributing = (repo / "CONTRIBUTING.md").read_text(encoding="utf-8") + compatibility = (repo / "docs" / "COMPATIBILITY.md").read_text( + encoding="utf-8" + ) + versioning = (repo / "docs" / "VERSIONING.md").read_text(encoding="utf-8") + forbidden = [ + "github.com/alexdobin/STAR/issues", + "github.com/alexdobin/STAR/releases", + "BlackSTAR 2.7.11b-blackstar.1", + ] + for value in forbidden: + if value in readme or value in contributing: + raise ValueError(f"stale upstream project routing remains: {value}") + for value, label in ( + (blackstar_version, "BlackSTAR release"), + (executable_version, "executable identity"), + (compatibility_version, "STAR compatibility base"), + (genome_format_version, "genome format"), + ): + if value not in compatibility: + raise ValueError(f"{label} missing from compatibility contract") + if executable_version not in versioning: + raise ValueError("current executable identity missing from versioning policy") + + settings = json.loads( + (repo / ".github" / "repository-settings.json").read_text( + encoding="utf-8" + ) + ) + if settings.get("schema_version") != 1: + raise ValueError("unsupported repository-settings schema") + repository_settings = settings["repository"] + if repository_settings["default_branch"] != "main": + raise ValueError("successor default branch is not main") + if repository_settings["features"] != { + "discussions": True, + "issues": True, + "wiki": False, + }: + raise ValueError("unexpected successor repository feature policy") + contexts = settings["branch_protection"]["required_status_checks"]["contexts"] + if contexts != ["build-and-test"]: + raise ValueError("required branch-protection check is not build-and-test") + + for relative in required: + text = (repo / relative).read_text(encoding="utf-8") + if "/mnt/datavault/" in text: + raise ValueError(f"private local path in public document: {relative}") + + evidence = ( + repo + / "docs" + / "benchmarks" + / "official-star-2.7.11b-vs-blackstar.2" + ) + summary = json.loads((evidence / "summary.json").read_text(encoding="utf-8")) + + index_rows = read_tsv(evidence / "index-pairs.tsv") + assert_close( + statistics.median(float(row["upstream_wall_seconds"]) for row in index_rows), + summary["full_chm13_index_96_threads"]["upstream_median_wall_seconds"], + "upstream index median", + ) + assert_close( + statistics.median(float(row["blackstar_wall_seconds"]) for row in index_rows), + summary["full_chm13_index_96_threads"]["blackstar_median_wall_seconds"], + "BlackSTAR index median", + ) + + alignment_rows = read_tsv(evidence / "alignment-scaling.tsv") + for input_mode in ("uncompressed", "zcat"): + row = next( + value + for value in alignment_rows + if value["input"] == input_mode and value["threads"] == "96" + ) + values = summary["alignment"][f"{input_mode}_96_threads"] + assert_close( + float(row["blackstar_median_wall_seconds"]), + values["blackstar_median_wall_seconds"], + f"{input_mode} BlackSTAR median", + ) + if row["correctness_passed"] != "True": + raise ValueError(f"{input_mode} correctness receipt failed") + + delta_rows = read_tsv(evidence / "delta-cold-pairs.tsv") + cold = summary["named_sequence_addition"]["cold_input_file_cache"] + assert_close( + statistics.median( + float(row["blackstar_delta_wall_seconds"]) for row in delta_rows + ), + cold["blackstar_delta_median_wall_seconds"], + "Delta cold median", + ) + + print( + "BlackSTAR successor metadata: PASS " + f"(release={blackstar_version}, executable={executable_version})" + ) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (ValueError, OSError, json.JSONDecodeError, yaml.YAMLError) as error: + print(f"ERROR: {error}", file=sys.stderr) + sys.exit(1) diff --git a/extras/maintenance/verify_transition_backup.py b/extras/maintenance/verify_transition_backup.py new file mode 100755 index 00000000..abfafc3f --- /dev/null +++ b/extras/maintenance/verify_transition_backup.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Verify a BlackSTAR transition backup and perform a local restore drill.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import tempfile + + +def checksum(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def run(args: list[str]) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + args, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if result.returncode != 0: + raise RuntimeError(f"{' '.join(args)} failed:\n{result.stderr.strip()}") + return result + + +def refs(git_dir: Path) -> str: + return run( + [ + "git", + f"--git-dir={git_dir}", + "for-each-ref", + "--format=%(refname)\t%(objectname)\t%(objecttype)", + ] + ).stdout + + +def verify_checksums(backup: Path) -> int: + checked = 0 + for line in (backup / "checksums.sha256").read_text(encoding="utf-8").splitlines(): + expected, relative = line.split(" ", 1) + path = backup / relative + if not path.is_file(): + raise RuntimeError(f"missing backup file: {relative}") + actual = checksum(path) + if actual != expected: + raise RuntimeError( + f"checksum mismatch for {relative}: {actual} != {expected}" + ) + checked += 1 + return checked + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--backup", type=Path, required=True) + parser.add_argument("--receipt", type=Path) + args = parser.parse_args() + + backup = args.backup.resolve() + checked = verify_checksums(backup) + bundle = backup / "git" / "local-all.bundle" + run(["git", "bundle", "verify", str(bundle)]) + + with tempfile.TemporaryDirectory(prefix="blackstar-restore-drill.") as temp: + temp_root = Path(temp) + restored_bundle = temp_root / "bundle.git" + restored_remote = temp_root / "remote.git" + run(["git", "clone", "--mirror", str(bundle), str(restored_bundle)]) + run( + [ + "git", + "clone", + "--mirror", + str(backup / "git" / "remote-mirror.git"), + str(restored_remote), + ] + ) + + expected_local = (backup / "git" / "local-refs.tsv").read_text( + encoding="utf-8" + ) + expected_remote = (backup / "git" / "remote-refs.tsv").read_text( + encoding="utf-8" + ) + if refs(restored_bundle) != expected_local: + raise RuntimeError("bundle restore reference inventory differs") + if refs(restored_remote) != expected_remote: + raise RuntimeError("remote mirror restore reference inventory differs") + run(["git", f"--git-dir={restored_bundle}", "fsck", "--full"]) + run(["git", f"--git-dir={restored_remote}", "fsck", "--full"]) + + snapshot = json.loads((backup / "snapshot.json").read_text(encoding="utf-8")) + receipt = { + "status": "PASS", + "checked_utc": dt.datetime.now(dt.timezone.utc).isoformat(), + "backup": str(backup), + "checksum_manifest_sha256": checksum(backup / "checksums.sha256"), + "repository": snapshot["repository"], + "default_branch": snapshot["default_branch"], + "default_branch_commit": snapshot["default_branch_commit"], + "files_verified": checked, + "bundle_restore": "PASS", + "remote_mirror_restore": "PASS", + } + rendered = json.dumps(receipt, indent=2, sort_keys=True) + "\n" + if args.receipt: + args.receipt.parent.mkdir(parents=True, exist_ok=True) + args.receipt.write_text(rendered, encoding="utf-8") + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except RuntimeError as error: + print(f"ERROR: {error}", file=sys.stderr) + sys.exit(1) diff --git a/extras/scripts/buildBlackSTARRelease.sh b/extras/scripts/buildBlackSTARRelease.sh index f6186041..0455fd91 100755 --- a/extras/scripts/buildBlackSTARRelease.sh +++ b/extras/scripts/buildBlackSTARRelease.sh @@ -18,17 +18,25 @@ fi commit="$(git rev-parse HEAD)" source_date_epoch="${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}" -version="$(sed -n 's/^#define STAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" -if [[ -z "${version}" || "${version}" != *blackstar* ]]; then +executable_version="$(sed -n 's/^#define STAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" +version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" +compatibility_version="$(sed -n 's/^#define STAR_COMPATIBILITY_VERSION "\(.*\)"$/\1/p' source/VERSION)" +genome_format_version="$(sed -n 's/^#define BLACKSTAR_GENOME_FORMAT_VERSION "\(.*\)"$/\1/p' source/VERSION)" +if [[ -z "${version}" || -z "${executable_version}" || "${executable_version}" != *blackstar* ]]; then echo "ERROR: source/VERSION does not identify a BlackSTAR release" >&2 exit 1 fi +if [[ -z "${compatibility_version}" || -z "${genome_format_version}" ]]; then + echo "ERROR: source/VERSION does not identify the compatibility boundary" >&2 + exit 1 +fi dist_root="${DIST_DIR:-${repo_root}/dist}" package_name="blackstar-${version}-linux-x86_64" package_final="${dist_root}/${package_name}" archive="${dist_root}/${package_name}.tar.gz" -if [[ -e "${package_final}" || -e "${archive}" || -e "${archive}.sha256" ]]; then +sbom="${dist_root}/${package_name}.spdx.json" +if [[ -e "${package_final}" || -e "${archive}" || -e "${archive}.sha256" || -e "${sbom}" ]]; then echo "ERROR: release destination already exists for ${package_name}" >&2 exit 1 fi @@ -45,7 +53,7 @@ cleanup() { trap cleanup EXIT mkdir "${package_dir}" -provenance="commit=${commit};tree=$([[ -z "$(git status --porcelain)" ]] && echo clean || echo dirty);release=${version}" +provenance="commit=${commit};tree=$([[ -z "$(git status --porcelain)" ]] && echo clean || echo dirty);release=${version};executable=${executable_version}" export SOURCE_DATE_EPOCH="${source_date_epoch}" make -C source clean make -C source -j"${jobs}" STAR \ @@ -55,7 +63,7 @@ make -C source -j"${jobs}" STAR \ CXXFLAGSextra="${CXXFLAGSEXTRA:-}" \ LDFLAGSextra="${LDFLAGSEXTRA:-}" -if [[ "$(source/STAR --version)" != "${version}" ]]; then +if [[ "$(source/STAR --version)" != "${executable_version}" ]]; then echo "ERROR: built binary reports an unexpected version" >&2 exit 1 fi @@ -65,12 +73,26 @@ if ! ldd source/STAR | grep -Eq 'libgomp|libomp'; then fi install -m 0755 source/STAR "${package_dir}/STAR" +install -m 0644 LICENSE "${package_dir}/LICENSE" +install -m 0644 ATTRIBUTION.md "${package_dir}/ATTRIBUTION.md" binary_sha256="$(sha256sum "${package_dir}/STAR" | awk '{print $1}')" +license_sha256="$(sha256sum "${package_dir}/LICENSE" | awk '{print $1}')" +attribution_sha256="$(sha256sum "${package_dir}/ATTRIBUTION.md" | awk '{print $1}')" compiler_version="$("${cxx}" --version | sed -n '1p')" build_utc="$(date -u -d "@${source_date_epoch}" '+%Y-%m-%dT%H:%M:%SZ')" +python3 extras/scripts/generateBlackSTARSbom.py \ + --repo-root "${repo_root}" \ + --binary "${package_dir}/STAR" \ + --commit "${commit}" \ + --source-date-epoch "${source_date_epoch}" \ + --output "${package_dir}/sbom.spdx.json" +sbom_sha256="$(sha256sum "${package_dir}/sbom.spdx.json" | awk '{print $1}')" { printf 'key\tvalue\n' - printf 'version\t%s\n' "${version}" + printf 'blackstar_version\t%s\n' "${version}" + printf 'star_executable_version\t%s\n' "${executable_version}" + printf 'star_compatibility_version\t%s\n' "${compatibility_version}" + printf 'genome_format_version\t%s\n' "${genome_format_version}" printf 'git_commit\t%s\n' "${commit}" printf 'source_tree\t%s\n' "$([[ -z "$(git status --porcelain)" ]] && echo clean || echo dirty)" printf 'source_date_epoch\t%s\n' "${source_date_epoch}" @@ -80,6 +102,9 @@ build_utc="$(date -u -d "@${source_date_epoch}" '+%Y-%m-%dT%H:%M:%SZ')" printf 'cxxflags_extra\t%s\n' "${CXXFLAGSEXTRA:-}" printf 'ldflags_extra\t%s\n' "${LDFLAGSEXTRA:-}" printf 'binary_sha256\t%s\n' "${binary_sha256}" + printf 'license_sha256\t%s\n' "${license_sha256}" + printf 'attribution_sha256\t%s\n' "${attribution_sha256}" + printf 'sbom_sha256\t%s\n' "${sbom_sha256}" } > "${package_dir}/build-info.tsv" ldd "${package_dir}/STAR" | sed -E 's/ \(0x[0-9a-f]+\)$//' > "${package_dir}/ldd.txt" @@ -95,6 +120,7 @@ printf '%s %s\n' "${archive_sha256}" "${package_name}.tar.gz" > "${checksum_sta mv "${package_dir}" "${package_final}" mv "${archive_staged}" "${archive}" mv "${checksum_staged}" "${archive}.sha256" +cp -p "${package_final}/sbom.spdx.json" "${sbom}" rmdir "${stage_root}" stage_root="" trap - EXIT diff --git a/extras/scripts/generateBlackSTARSbom.py b/extras/scripts/generateBlackSTARSbom.py new file mode 100755 index 00000000..ffca47ac --- /dev/null +++ b/extras/scripts/generateBlackSTARSbom.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Generate a deterministic SPDX 2.3 SBOM for a BlackSTAR release binary.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +from pathlib import Path +import re + + +def macro(path: Path, name: str) -> str: + pattern = re.compile(rf'^#define {re.escape(name)} "([^"]+)"$') + for line in path.read_text(encoding="utf-8").splitlines(): + match = pattern.match(line) + if match: + return match.group(1) + raise ValueError(f"missing {name} in {path}") + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo-root", type=Path, required=True) + parser.add_argument("--binary", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--source-date-epoch", type=int, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + repo_root = args.repo_root.resolve() + binary = args.binary.resolve() + version_file = repo_root / "source" / "VERSION" + blackstar_version = macro(version_file, "BLACKSTAR_VERSION") + executable_version = macro(version_file, "STAR_VERSION") + compatibility_version = macro(version_file, "STAR_COMPATIBILITY_VERSION") + + created = dt.datetime.fromtimestamp( + args.source_date_epoch, tz=dt.timezone.utc + ).strftime("%Y-%m-%dT%H:%M:%SZ") + namespace = ( + "https://github.com/justinblethrow-cloud/blackSTAR/" + f"sbom/{blackstar_version}/{args.commit}" + ) + document = { + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": created, + "creators": ["Tool: BlackSTAR-generateBlackSTARSbom.py-1"], + }, + "dataLicense": "CC0-1.0", + "documentNamespace": namespace, + "name": f"BlackSTAR-{blackstar_version}", + "packages": [ + { + "SPDXID": "SPDXRef-Package-BlackSTAR", + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": file_sha256(binary), + } + ], + "copyrightText": ( + "Copyright (c) 2019 Alexander Dobin; " + "BlackSTAR modifications copyright their contributors" + ), + "downloadLocation": ( + "https://github.com/justinblethrow-cloud/blackSTAR" + ), + "filesAnalyzed": False, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "BlackSTAR", + "sourceInfo": ( + f"Git commit {args.commit}; executable identity " + f"{executable_version}" + ), + "versionInfo": blackstar_version, + }, + { + "SPDXID": "SPDXRef-Package-STAR-Upstream", + "copyrightText": "Copyright (c) 2019 Alexander Dobin", + "downloadLocation": "https://github.com/alexdobin/STAR", + "filesAnalyzed": False, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "STAR", + "versionInfo": compatibility_version, + }, + { + "SPDXID": "SPDXRef-Package-HTSlib-Bundled", + "copyrightText": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "name": "HTSlib (bundled STAR snapshot)", + "versionInfo": "0.0.1", + }, + { + "SPDXID": "SPDXRef-Package-Opal-Bundled", + "copyrightText": "Copyright (c) 2014 Martin Sosic", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "Opal (bundled)", + "versionInfo": "NOASSERTION", + }, + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-Package-BlackSTAR", + "relationshipType": "DESCRIBES", + "spdxElementId": "SPDXRef-DOCUMENT", + }, + { + "relatedSpdxElement": "SPDXRef-Package-STAR-Upstream", + "relationshipType": "VARIANT_OF", + "spdxElementId": "SPDXRef-Package-BlackSTAR", + }, + { + "relatedSpdxElement": "SPDXRef-Package-HTSlib-Bundled", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-Package-BlackSTAR", + }, + { + "relatedSpdxElement": "SPDXRef-Package-Opal-Bundled", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-Package-BlackSTAR", + }, + ], + "spdxVersion": "SPDX-2.3", + } + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(document, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/extras/tests/scripts/testBlackstarVersion.sh b/extras/tests/scripts/testBlackstarVersion.sh new file mode 100755 index 00000000..7cad6846 --- /dev/null +++ b/extras/tests/scripts/testBlackstarVersion.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +star="${STAR_BIN:-${repo_root}/source/STAR}" + +if [[ ! -x "${star}" ]]; then + echo "ERROR: STAR binary is not executable: ${star}" >&2 + exit 1 +fi + +expected_executable="$(sed -n 's/^#define STAR_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" +expected_blackstar="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" +expected_compatibility="$(sed -n 's/^#define STAR_COMPATIBILITY_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" +expected_genome="$(sed -n 's/^#define BLACKSTAR_GENOME_FORMAT_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" + +[[ "$("${star}" --version)" == "${expected_executable}" ]] + +version_json="$("${star}" --version-json)" +python3 - "${version_json}" \ + "${expected_blackstar}" \ + "${expected_compatibility}" \ + "${expected_executable}" \ + "${expected_genome}" <<'PY' +import json +import sys + +actual = json.loads(sys.argv[1]) +expected = { + "blackstar_version": sys.argv[2], + "star_compatibility_version": sys.argv[3], + "executable_version": sys.argv[4], + "genome_format_version": sys.argv[5], +} +if actual != expected: + raise SystemExit(f"version metadata differs: {actual!r} != {expected!r}") +PY + +default_genome="$(awk '$1 == "versionGenome" { print $2; exit }' "${repo_root}/source/parametersDefault")" +[[ "${default_genome}" == "${expected_genome}" ]] + +printf 'BlackSTAR version metadata: PASS\n' diff --git a/source/Parameters.cpp b/source/Parameters.cpp index db6c6a2e..fa7753f6 100755 --- a/source/Parameters.cpp +++ b/source/Parameters.cpp @@ -366,6 +366,17 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters std::cout << STAR_VERSION <