diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ada28d1fb..ef5e298f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -875,6 +875,12 @@ jobs: python3 scripts/ci/cargo-bazel-drift-check.py python3 scripts/ci/test-cargo-bazel-drift-check.py + - name: Migration ledger + release platform inventory (fail-closed) + run: | + python3 scripts/ci/bazel-migration-ledger-check.py + python3 scripts/ci/test-bazel-migration-ledger-check.py + python3 scripts/ci/test-cargo-bazel-parity-check.py + - name: Bazel smoke + first-party libs/tests/CLI/resources run: | # Do not set --remote_cache; Blacksmith injects repository cache. @@ -884,7 +890,8 @@ jobs: //:first_party_libs \ //:runtime_libs \ //:cli_bins \ - //:resource_inputs + //:resource_inputs \ + //:release_bins - name: Bazel binding cdylibs + packaging handoff run: | @@ -892,6 +899,24 @@ jobs: bazelisk build //:binding_cdylibs //:python_wheel_smoke //:node_package_smoke python3 scripts/ci/test-assemble-bazel-binding-packages.py + - name: Same-SHA Cargo/Bazel dual-build parity + run: | + # Dual-build remains under CI Gate; Bazel is not sole authority yet (#4). + # Do not set --remote_cache; Blacksmith injects repository cache. + mkdir -p dist + python3 scripts/ci/cargo-bazel-parity-check.py \ + --mode all \ + --write-evidence "dist/cargo-bazel-parity-evidence.json" + + - name: Upload Cargo/Bazel parity evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: cargo-bazel-parity-evidence-${{ github.run_id }} + path: dist/cargo-bazel-parity-evidence.json + if-no-files-found: error + retention-days: 1 + ci-gate: name: CI Gate runs-on: blacksmith-4vcpu-ubuntu-2404 diff --git a/BUILD.bazel b/BUILD.bazel index 832fd6e5b..5e04f73bf 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,4 +1,4 @@ -"""Root Bazel package for GraphForge (#10/#9/#7 libs+cdylibs + #8 tests/CLI/resources).""" +"""Root Bazel package for GraphForge (#10–#6 libs/tests/bindings/release/parity).""" load("@bazel_skylib//rules:build_test.bzl", "build_test") @@ -207,6 +207,7 @@ build_test( "//examples/agent_grounding:notebooks", "//tests/features:api_features", "//tests/tck:corpus", + "//tests/release_workflows:sna_phase2_advisory", "//crates/graphforge-cypher:corpus_data", "//crates/graphforge-exec:explain_goldens", "//crates/graphforge-ir:ir_goldens", @@ -235,3 +236,48 @@ test_suite( "//crates/graphforge-api:value_semantics", ], ) + +# Host-native release artifact aggregate (#6). Cross-OS Binding RC surfaces are +# modeled under //platforms:* and tools/bazel/release/release_platforms.json. +build_test( + name = "release_bins", + targets = [ + "//crates/graphforge-cli:gf", + "//crates/graphforge-api:atomic_recovery_workflow", + "//crates/graphforge-api:correction_churn_workflow", + "//crates/graphforge-api:cyber_intrusion_workflow", + "//crates/graphforge-api:derived_state_freshness_workflow", + "//crates/graphforge-api:finance_fraud_workflow", + "//crates/graphforge-api:knowledge_evolution_workflow", + "//crates/graphforge-api:ontology_emergence_strict_handoff", + "//crates/graphforge-api:probate_genealogy_workflow", + "//crates/graphforge-api:release_load_probe", + "//crates/graphforge-api:sna_intelligence_workflow", + "//crates/graphforge-api:strict_add_node_fixture", + ], +) + +alias( + name = "release_platforms", + actual = "//tools/bazel/release:release_platforms", + visibility = ["//visibility:public"], +) + +alias( + name = "migration_target_map", + actual = "//tools/bazel/parity:migration_target_map", + visibility = ["//visibility:public"], +) + +# Dual-build parity suite labels (#6); outcomes compared by +# scripts/ci/cargo-bazel-parity-check.py against Cargo at the same SHA. +test_suite( + name = "parity_suite", + tests = [ + "//crates/graphforge-ast:graphforge_ast_test", + "//crates/graphforge-ir:golden", + "//crates/graphforge-api:clear", + "//crates/graphforge-api:value_semantics", + "//crates/graphforge-rel:logical_plan_golden", + ], +) diff --git a/MODULE.bazel b/MODULE.bazel index 4bec707c1..f5d5e054b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,9 +1,10 @@ -"""GraphForge Bazel module (M2 / #10–#7 libs+cdylibs + #8 tests/CLI). +"""GraphForge Bazel module (M2 / #10–#6 libs/tests/bindings/release/parity). Canonical build-system contract: GitHub issue #1. Activates crate_universe `from_cargo` against the frozen Cargo workspace and -models first-party libraries, tests, CLI, resources, and PyO3/napi cdylibs as -real rules_rust targets (no Cargo shell-out for ordinary compilation or tests). +models first-party libraries, tests, CLI, resources, PyO3/napi cdylibs, and +release platforms as real rules_rust / platforms targets (no Cargo shell-out +for ordinary compilation or tests). """ module( diff --git a/crates/graphforge-api/BUILD.bazel b/crates/graphforge-api/BUILD.bazel index 12e92a218..b661e2047 100644 --- a/crates/graphforge-api/BUILD.bazel +++ b/crates/graphforge-api/BUILD.bazel @@ -1,6 +1,13 @@ -"""Bazel targets for graphforge-api (#9 runtime + #8 integration/BDD).""" +"""Bazel targets for graphforge-api (#9 runtime + #8 integration/BDD + #6 examples).""" -load("//tools/bazel:gf_rust.bzl", "gf_rust_integration_test", "gf_rust_library", "gf_rust_test") +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load( + "//tools/bazel:gf_rust.bzl", + "gf_rust_binary", + "gf_rust_integration_test", + "gf_rust_library", + "gf_rust_test", +) exports_files(["Cargo.toml"]) @@ -441,3 +448,43 @@ test_suite( ":graphforge_api_test", ], ) + +# API example binaries (#6). Release load probe is required for M1 certification; +# remaining examples are opt-in workflow evidence mapped for ledger completeness. +_EXAMPLE_RUSTC_ENV = { + "CARGO_MANIFEST_DIR": "crates/graphforge-api", +} + +_EXAMPLE_NAMES = [ + "atomic_recovery_workflow", + "correction_churn_workflow", + "cyber_intrusion_workflow", + "derived_state_freshness_workflow", + "finance_fraud_workflow", + "knowledge_evolution_workflow", + "ontology_emergence_strict_handoff", + "probate_genealogy_workflow", + "release_load_probe", + "sna_intelligence_workflow", + "strict_add_node_fixture", +] + +_EXAMPLE_COMPILE_DATA = { + "sna_intelligence_workflow": ["//tests/release_workflows:sna_phase2_advisory"], +} + +[ + gf_rust_binary( + name = example_name, + srcs = ["examples/%s.rs" % example_name], + compile_data = _EXAMPLE_COMPILE_DATA.get(example_name, []), + rustc_env = _EXAMPLE_RUSTC_ENV, + deps = _API_DEPS + [":graphforge_api"], + ) + for example_name in _EXAMPLE_NAMES +] + +build_test( + name = "api_example_bins", + targets = [":%s" % name for name in _EXAMPLE_NAMES], +) diff --git a/docs/development/bazel-bootstrap.md b/docs/development/bazel-bootstrap.md index 7bd001b44..8c2b8b841 100644 --- a/docs/development/bazel-bootstrap.md +++ b/docs/development/bazel-bootstrap.md @@ -92,8 +92,9 @@ skills are declared via root `//:project_skills_bundle` / `//:project-skills/man | Integration-test mapped | 59 | `//:integration_tests` (+ BDD harness) | | CLI `bin` mapped | 1 | `//crates/graphforge-cli:gf` | | CLI `custom-build` mapped | 1 | `cargo_build_script` + `//:project_skills_bundle` | -| Explicit retained exception | 1 | API examples (`RT-examples` → #6) | -| Justified Cargo tool | 1 | `RT-fuzz` (cargo-fuzz workflow; keep until #6) | +| Example binaries mapped | 11 | `//crates/graphforge-api:*` + `//:release_bins` (#6) | +| Justified Cargo tools | 2 | `RT-fuzz`, `RT-publish-crates` | +| Release platforms | 5 OS/arch + 8 Binding RC surfaces | `//platforms:*` + `tools/bazel/release/release_platforms.json` | ### Residual gaps (justified) @@ -102,13 +103,14 @@ skills are declared via root `//:project_skills_bundle` / `//:project-skills/man for lint CI until a later slice). - Doctests are not separate Bazel targets (documented equivalent: coverage attaches to crate unit-test targets; same policy as #10/#9). -- API `example` binaries are `RT-examples` for #6 (not required to close #8). -- Blacksmith `Bazel Bootstrap` runs `//:bazel_test_graph_smoke` (unit + CLI + - representative integration/snapshot probes). Full `//:integration_tests` and - `//:bdd_tests` remain executable under Bazel for parity (#6) and local runs. -- Full cross-platform binding/release matrix remains #6. +- Blacksmith `Bazel Bootstrap` runs `//:bazel_test_graph_smoke`, release bins, + binding smokes, and the #6 dual-build parity gate. Full `//:integration_tests` + and `//:bdd_tests` remain executable under Bazel for local/full runs. +- Cross-OS Binding RC still produces macOS/Windows natives on those runners; + Bazel models every certified platform and builds host-native release artifacts. + Sole Bazel authority is #4 after #5 cache/perf. - Bazel storage always enables `test-failpoints` (env-gated no-ops); Cargo release - builds keep the const no-op body — track under #6 parity if needed. + builds keep the const no-op body — documented dual-build parity surface. ## Local commands @@ -124,6 +126,12 @@ bazelisk build //:first_party_libs //:cli_bins //:resource_inputs bazelisk build //:binding_cdylibs //:python_wheel_smoke //:node_package_smoke python3 scripts/ci/test-assemble-bazel-binding-packages.py +# Release bins + dual-build parity (#6) +bazelisk build //:release_bins +python3 scripts/ci/bazel-migration-ledger-check.py +python3 scripts/ci/cargo-bazel-parity-check.py --mode all \ + --write-evidence dist/cargo-bazel-parity-evidence.json + # Full mapped suites (longer) bazelisk test //:unit_tests //:integration_tests //:snapshot_tests //:cli_tests bazelisk test //:bdd_tests @@ -145,5 +153,6 @@ CARGO_BAZEL_REPIN=1 bazelisk build --repo_env=CARGO_BAZEL_REPIN=1 //:first_party ## Next -1. [#6](https://github.com/CurateLabs/graphforge/issues/6) — cross-platform release - targets and same-SHA Cargo/Bazel parity (blocked by #7 and #8). +1. [#5](https://github.com/CurateLabs/graphforge/issues/5) — Blacksmith remote-cache + enablement + cold/warm performance gates (may need org-admin Bazel Build Caching). +2. See [bazel-migration-parity.md](bazel-migration-parity.md) for #6 evidence. diff --git a/docs/development/bazel-migration-ledger.md b/docs/development/bazel-migration-ledger.md index 5916b1717..6df1cd9be 100644 --- a/docs/development/bazel-migration-ledger.md +++ b/docs/development/bazel-migration-ledger.md @@ -14,8 +14,9 @@ Performance baseline: [bazel-migration-baseline.md](bazel-migration-baseline.md) | Authoritative source | `cargo metadata --format-version=1 --no-deps` | | Workspace packages | 17 | | Cargo metadata targets | **90** | -| Bazel modeling claimed complete? | **No** — #10/#9/#7/#8 libs+cdylibs+tests+CLI mapped; examples (`RT-examples`) remain for #6 | -| Bootstrap note | See [bazel-bootstrap.md](bazel-bootstrap.md); crate_universe + library/test/CLI/cdylib labels from #10/#9/#8/#7 | +| Bazel modeling claimed complete? | **Yes** — all 90 Cargo targets mapped (#10–#6); retained tools justified in exceptions | +| Machine-readable map | `tools/bazel/parity/migration_target_map.json` (fail-closed via `scripts/ci/bazel-migration-ledger-check.py`) | +| Bootstrap note | See [bazel-bootstrap.md](bazel-bootstrap.md); parity evidence [bazel-migration-parity.md](bazel-migration-parity.md) | Issue #1 historically cited ~71 Cargo targets / ~53 integration-test binaries. This freeze uses the **current authoritative** metadata count (**90** targets; @@ -31,7 +32,7 @@ not silently ignore new targets. | `cdylib` | 2 | PyO3 + napi-rs native libs | | `bin` | 1 | CLI (`gf`) | | `custom-build` | 2 | `build.rs` scripts | -| `example` | 11 | API examples (map or justify retained exception in later slices) | +| `example` | 11 | API examples mapped as `//crates/graphforge-api:` (#6) | | **Total** | **90** | | ### Unit tests and doctests @@ -46,23 +47,23 @@ doctests. For migration accounting: ## Cargo target ledger -Columns `bazel_label` and `status` are filled by modeling slices (#10–#7). -Example rows stay `unmapped` until #6; libs/tests/CLI/cdylibs mapped through #8/#7. +Columns `bazel_label` and `status` are filled by modeling slices (#10–#6). +Authoritative machine-readable map: `tools/bazel/parity/migration_target_map.json`. | Package | Target | Class | Source | Bazel label | Status | Notes | | --- | --- | --- | --- | --- | --- | --- | | `graphforge-api` | `graphforge_api` | `lib` | `crates/graphforge-api/src/lib.rs` | `//crates/graphforge-api:graphforge_api` | `mapped` | #9; unit tests `//crates/graphforge-api:graphforge_api_test` | -| `graphforge-api` | `atomic_recovery_workflow` | `example` | `crates/graphforge-api/examples/atomic_recovery_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `correction_churn_workflow` | `example` | `crates/graphforge-api/examples/correction_churn_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `cyber_intrusion_workflow` | `example` | `crates/graphforge-api/examples/cyber_intrusion_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `derived_state_freshness_workflow` | `example` | `crates/graphforge-api/examples/derived_state_freshness_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `finance_fraud_workflow` | `example` | `crates/graphforge-api/examples/finance_fraud_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `knowledge_evolution_workflow` | `example` | `crates/graphforge-api/examples/knowledge_evolution_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `ontology_emergence_strict_handoff` | `example` | `crates/graphforge-api/examples/ontology_emergence_strict_handoff.rs` | — | `unmapped` | | -| `graphforge-api` | `probate_genealogy_workflow` | `example` | `crates/graphforge-api/examples/probate_genealogy_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `release_load_probe` | `example` | `crates/graphforge-api/examples/release_load_probe.rs` | — | `unmapped` | | -| `graphforge-api` | `sna_intelligence_workflow` | `example` | `crates/graphforge-api/examples/sna_intelligence_workflow.rs` | — | `unmapped` | | -| `graphforge-api` | `strict_add_node_fixture` | `example` | `crates/graphforge-api/examples/strict_add_node_fixture.rs` | — | `unmapped` | | +| `graphforge-api` | `atomic_recovery_workflow` | `example` | `crates/graphforge-api/examples/atomic_recovery_workflow.rs` | `//crates/graphforge-api:atomic_recovery_workflow` | `mapped` | #6 | +| `graphforge-api` | `correction_churn_workflow` | `example` | `crates/graphforge-api/examples/correction_churn_workflow.rs` | `//crates/graphforge-api:correction_churn_workflow` | `mapped` | #6 | +| `graphforge-api` | `cyber_intrusion_workflow` | `example` | `crates/graphforge-api/examples/cyber_intrusion_workflow.rs` | `//crates/graphforge-api:cyber_intrusion_workflow` | `mapped` | #6 | +| `graphforge-api` | `derived_state_freshness_workflow` | `example` | `crates/graphforge-api/examples/derived_state_freshness_workflow.rs` | `//crates/graphforge-api:derived_state_freshness_workflow` | `mapped` | #6 | +| `graphforge-api` | `finance_fraud_workflow` | `example` | `crates/graphforge-api/examples/finance_fraud_workflow.rs` | `//crates/graphforge-api:finance_fraud_workflow` | `mapped` | #6 | +| `graphforge-api` | `knowledge_evolution_workflow` | `example` | `crates/graphforge-api/examples/knowledge_evolution_workflow.rs` | `//crates/graphforge-api:knowledge_evolution_workflow` | `mapped` | #6 | +| `graphforge-api` | `ontology_emergence_strict_handoff` | `example` | `crates/graphforge-api/examples/ontology_emergence_strict_handoff.rs` | `//crates/graphforge-api:ontology_emergence_strict_handoff` | `mapped` | #6 | +| `graphforge-api` | `probate_genealogy_workflow` | `example` | `crates/graphforge-api/examples/probate_genealogy_workflow.rs` | `//crates/graphforge-api:probate_genealogy_workflow` | `mapped` | #6 | +| `graphforge-api` | `release_load_probe` | `example` | `crates/graphforge-api/examples/release_load_probe.rs` | `//crates/graphforge-api:release_load_probe` | `mapped` | #6; M1 release certification probe | +| `graphforge-api` | `sna_intelligence_workflow` | `example` | `crates/graphforge-api/examples/sna_intelligence_workflow.rs` | `//crates/graphforge-api:sna_intelligence_workflow` | `mapped` | #6 | +| `graphforge-api` | `strict_add_node_fixture` | `example` | `crates/graphforge-api/examples/strict_add_node_fixture.rs` | `//crates/graphforge-api:strict_add_node_fixture` | `mapped` | #6 | | `graphforge-api` | `bdd` | `integration-test` | `crates/graphforge-api/tests/bdd/main.rs` | `//crates/graphforge-api:bdd` | `mapped` | #8 | | `graphforge-api` | `bdd_timing` | `integration-test` | `crates/graphforge-api/tests/bdd_timing.rs` | `//crates/graphforge-api:bdd_timing` | `mapped` | #8 | | `graphforge-api` | `belief_subject_contract` | `integration-test` | `crates/graphforge-api/tests/belief_subject_contract.rs` | `//crates/graphforge-api:belief_subject_contract` | `mapped` | #8 | @@ -142,22 +143,30 @@ Example rows stay `unmapped` until #6; libs/tests/CLI/cdylibs mapped through #8/ | `graphforge-storage` | `graph_writer` | `integration-test` | `crates/graphforge-storage/tests/graph_writer.rs` | `//crates/graphforge-storage:graph_writer` | `mapped` | #8 | | `graphforge-storage` | `io_stats` | `integration-test` | `crates/graphforge-storage/tests/io_stats.rs` | `//crates/graphforge-storage:io_stats` | `mapped` | #8 | -## Retained-tool exception stubs +## Retained-tool exceptions -These are **not** claimed as Bazel-complete at freeze. Each needs a justification -before #6 parity can pass with the exception still open. +Justified retained Cargo/ecosystem tools after #6. Stub status is forbidden; the +ledger check fails closed on `stub` or missing justification. | ID | Tool / surface | Why Bazel may not replace cleanly | Owning follow-up | Status | | --- | --- | --- | --- | --- | -| RT-fuzz | `cargo fuzz` (`fuzz/` workspace, workflow `fuzz.yml`) | cargo-fuzz driver + corpus workflow outside ordinary `rules_rust` test graph | #6 parity may keep Cargo; justified retained tool | explicit | -| RT-publish-crates | `cargo publish` / crates.io authorize flows | Ecosystem publication metadata and registry auth | keep Cargo; ledger must remain explicit | stub | +| RT-fuzz | `cargo fuzz` (`fuzz/` workspace, workflow `fuzz.yml`) | cargo-fuzz driver + corpus workflow outside ordinary `rules_rust` test graph | keep Cargo | justified | +| RT-publish-crates | `cargo publish` / crates.io authorize flows | Ecosystem publication metadata and registry auth | keep Cargo | justified | | RT-maturin-assemble | `maturin build` / `maturin sdist` packaging assembly | Bazel handoff: `//:python_wheel_smoke` + `assemble_bazel_binding_packages.py` consume Bazel cdylibs (no silent `maturin build` recompile). Maturin may still sign/publish later. | #7 handoff | handoff | | RT-napi-assemble | `napi build` / `napi artifacts` / `napi pre-publish` | Bazel handoff: `//:node_package_smoke` consumes Bazel cdylib (no silent `napi build` recompile). napi may still assemble/sign/publish later. | #7 handoff | handoff | | RT-cli-build-script | `graphforge-cli` lib (`build.rs` → embedded `project-skills`) | Mapped via `cargo_build_script` + `//:project_skills_bundle`; bin/tests mapped | #8 complete | closed | | RT-bindings-cdylib | `graphforge-bindings-py` / `graphforge-bindings-node` packages | Mapped as `rust_shared_library` cdylibs + packaging smoke targets | #7 | mapped | -| RT-examples | `graphforge-api` examples (11) | Developer samples + release-load probes; not required for #8 test graph; map under #6 if parity demands binaries | #6 | explicit | +| RT-examples | `graphforge-api` examples (11) | All 11 example binaries mapped under `//crates/graphforge-api:*` | #6 | closed | | RT-mobile | Swift / Kotlin / UniFFI / XCFramework / JVM AAR | **Abandoned for M2** — not a deliverable; do not inventory as required targets | excluded | excluded | +## Cross-platform release platforms (#6) + +Checked-in model: `tools/bazel/release/release_platforms.json` + `//platforms:*`. +Must cover every Binding RC target in +`tests/contracts/binding-release-candidate-targets.json` and every +`package.json` `napi.targets` triple (including `aarch64-unknown-linux-gnu` +cross-target). Host-native release bins aggregate: `//:release_bins`. + ## CI / release build command sites Frozen scan of `.github/workflows/`, `scripts/`, and `Makefile` for `cargo`, @@ -346,9 +355,12 @@ Primary `test.yml` sticky key pattern: ## Update rules -1. Modeling PRs (#11–#7) must update `bazel_label` / `status` for touched rows in the - same change. -2. New Cargo targets require a new ledger row before #6 parity can pass. -3. Unjustified retained exceptions fail the migration at #6. +1. Modeling PRs must update `bazel_label` / `status` (markdown + + `migration_target_map.json`) for touched rows in the same change. +2. New Cargo targets require a new map/ledger row; unmapped rows fail + `scripts/ci/bazel-migration-ledger-check.py`. +3. Unjustified retained exceptions (`stub` or empty justification) fail the ledger. 4. Mobile bindings stay `excluded` — never promote to required M2 targets. +5. Release platform additions must update `release_platforms.json` and + `//platforms:*` together with the Binding RC contract. diff --git a/docs/development/bazel-migration-parity.md b/docs/development/bazel-migration-parity.md new file mode 100644 index 000000000..8ec3b20ff --- /dev/null +++ b/docs/development/bazel-migration-parity.md @@ -0,0 +1,75 @@ +# Bazel migration parity (#6) + +Same-SHA Cargo/Bazel dual-build parity and cross-platform release modeling for +M2 issue [#6](https://github.com/CurateLabs/graphforge/issues/6) / canonical +[#1](https://github.com/CurateLabs/graphforge/issues/1) step 7. + +Orchestration: [bazel-migration-orchestration.md](bazel-migration-orchestration.md). +Ledger: [bazel-migration-ledger.md](bazel-migration-ledger.md). +Bootstrap: [bazel-bootstrap.md](bazel-bootstrap.md). + +## What landed + +| Piece | Location | +| --- | --- | +| Release platforms | `//platforms:{linux_x86_64,linux_aarch64,macos_x86_64,macos_aarch64,windows_x86_64}` | +| Platform inventory | `tools/bazel/release/release_platforms.json` | +| Target map (90 rows) | `tools/bazel/parity/migration_target_map.json` | +| Ledger fail-closed check | `scripts/ci/bazel-migration-ledger-check.py` | +| Dual-build parity gate | `scripts/ci/cargo-bazel-parity-check.py` | +| Representative suite | `tools/bazel/parity/parity_suite.json` / `//:parity_suite` | +| Host release bins | `//:release_bins` (CLI + all 11 API examples) | +| Packaging tags | `assemble_bazel_binding_packages.py --wheel-tag` / `--platform-tag` | +| CI | `Bazel Bootstrap` dual-build steps; required check remains `CI Gate` | + +## Acceptance mapping + +| #6 / #1 AC theme | Evidence | +| --- | --- | +| Every mapped test/public contract same pass/fail on Cargo and Bazel at one SHA | `cargo-bazel-parity-check.py --mode all` writes `dist/cargo-bazel-parity-evidence.json`; Cargo `rust-test` + Bazel Bootstrap remain dual-build under `CI Gate` | +| Linux/macOS/Windows + Node cross-target release evidence | Platform inventory covers Binding RC contract + `napi.targets` (incl. `aarch64-unknown-linux-gnu`); host `//:release_bins` + binding smokes build under Bazel | +| Unmapped target / unjustified exception fails ledger | `bazel-migration-ledger-check.py` rejects `unmapped` rows and `stub` exceptions | + +## Dual-build contract + +- Cargo remains required for ordinary CI compilation/tests through #4 cutover. +- Bazel Bootstrap runs drift, ledger, release-platform inventory, smoke tests, + release bins, binding packaging, and the dual-build parity suite. +- Required check name stays **`CI Gate`**. Do not make Bazel the sole path yet. +- Do **not** set `--remote_cache` (Blacksmith injects cache; enablement is #5). + +## Local commands + +```bash +python3 scripts/ci/bazel-migration-ledger-check.py +python3 scripts/ci/test-bazel-migration-ledger-check.py +python3 scripts/ci/test-cargo-bazel-parity-check.py + +# Inventory only (no dual suite execution) +python3 scripts/ci/cargo-bazel-parity-check.py --mode inventory + +# Full dual-build parity at HEAD +python3 scripts/ci/cargo-bazel-parity-check.py \ + --mode all \ + --write-evidence dist/cargo-bazel-parity-evidence.json + +bazelisk build //:release_bins //:binding_cdylibs +bazelisk test //:parity_suite //:bazel_test_graph_smoke +``` + +## Retained Cargo tools (justified) + +| ID | Status | Why retained | +| --- | --- | --- | +| RT-fuzz | justified | cargo-fuzz driver/corpus outside ordinary rules_rust graph | +| RT-publish-crates | justified | crates.io publish/auth metadata | +| RT-maturin-assemble / RT-napi-assemble | handoff | Assemble/sign/publish only; no silent native recompile | +| RT-examples | closed | All 11 examples mapped as Bazel binaries | +| RT-mobile | excluded | Abandoned for M2 | + +## Next + +1. [#5](https://github.com/CurateLabs/graphforge/issues/5) — Blacksmith Bazel Build + Caching (org-admin enablement) + cold/warm performance gates. +2. [#4](https://github.com/CurateLabs/graphforge/issues/4) — `CI Gate` cutover and + Cargo sticky-disk retirement after parity + perf evidence. diff --git a/platforms/BUILD.bazel b/platforms/BUILD.bazel new file mode 100644 index 000000000..9f8b90530 --- /dev/null +++ b/platforms/BUILD.bazel @@ -0,0 +1,49 @@ +"""Certified GraphForge release platforms (#6). + +These labels model the Linux / macOS / Windows (and Node cross-target) surfaces +already certified by Binding RC. Host-native Bazel builds exercise the matching +platform; cross-OS release evidence continues via Binding RC until #4 cutover +consumes Bazel natives on each runner. +""" + +package(default_visibility = ["//visibility:public"]) + +platform( + name = "linux_x86_64", + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], +) + +platform( + name = "linux_aarch64", + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], +) + +platform( + name = "macos_x86_64", + constraint_values = [ + "@platforms//os:osx", + "@platforms//cpu:x86_64", + ], +) + +platform( + name = "macos_aarch64", + constraint_values = [ + "@platforms//os:osx", + "@platforms//cpu:aarch64", + ], +) + +platform( + name = "windows_x86_64", + constraint_values = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], +) diff --git a/scripts/ci/BUILD.bazel b/scripts/ci/BUILD.bazel index 6aa808c75..366a31087 100644 --- a/scripts/ci/BUILD.bazel +++ b/scripts/ci/BUILD.bazel @@ -4,6 +4,10 @@ exports_files([ "assemble_bazel_binding_packages.py", "cargo-bazel-drift-check.py", "test-cargo-bazel-drift-check.py", + "bazel-migration-ledger-check.py", + "test-bazel-migration-ledger-check.py", + "cargo-bazel-parity-check.py", + "test-cargo-bazel-parity-check.py", ]) filegroup( diff --git a/scripts/ci/assemble_bazel_binding_packages.py b/scripts/ci/assemble_bazel_binding_packages.py index 48db1e223..430b10065 100755 --- a/scripts/ci/assemble_bazel_binding_packages.py +++ b/scripts/ci/assemble_bazel_binding_packages.py @@ -59,7 +59,32 @@ def _native_python_module_name(native: Path) -> str: return "_graphforge_rs.abi3.so" -def _napi_platform_tag() -> str: +_NAPI_PLATFORM_TAGS = frozenset( + { + "darwin-arm64", + "darwin-x64", + "linux-arm64-gnu", + "linux-x64-gnu", + "win32-x64-msvc", + } +) + +_WHEEL_TAGS = frozenset( + { + "cp310-abi3-manylinux_2_17_x86_64", + "cp310-abi3-manylinux_2_17_aarch64", + "cp310-abi3-macosx_11_0_arm64", + "cp310-abi3-macosx_10_12_x86_64", + "cp310-abi3-win_amd64", + } +) + + +def _napi_platform_tag(explicit: str | None = None) -> str: + if explicit is not None: + if explicit not in _NAPI_PLATFORM_TAGS: + _die(f"unsupported napi platform tag: {explicit}") + return explicit system = platform.system() machine = platform.machine().lower() if system == "Darwin": @@ -79,6 +104,27 @@ def _napi_platform_tag() -> str: return "" # unreachable; _die raises SystemExit +def _python_wheel_tag(explicit: str | None = None) -> str: + if explicit is not None: + if explicit not in _WHEEL_TAGS: + _die(f"unsupported abi3 wheel tag: {explicit}") + return explicit + sys_name = platform.system() + mach = platform.machine().lower() + if sys_name == "Linux" and mach in ("x86_64", "amd64"): + return "cp310-abi3-manylinux_2_17_x86_64" + if sys_name == "Linux" and mach in ("aarch64", "arm64"): + return "cp310-abi3-manylinux_2_17_aarch64" + if sys_name == "Darwin" and mach in ("arm64", "aarch64"): + return "cp310-abi3-macosx_11_0_arm64" + if sys_name == "Darwin" and mach in ("x86_64", "amd64"): + return "cp310-abi3-macosx_10_12_x86_64" + if sys_name == "Windows" and mach in ("amd64", "x86_64"): + return "cp310-abi3-win_amd64" + _die(f"unsupported host platform for abi3 wheel tagging: {sys_name}/{mach}") + return "" + + def _read_version(package_root: Path, language: str) -> str: if language == "python": text = (package_root / "pyproject.toml").read_text(encoding="utf-8") @@ -93,7 +139,13 @@ def _read_version(package_root: Path, language: str) -> str: return version -def assemble_python(*, native: Path, package_root: Path, out: Path) -> dict[str, str]: +def assemble_python( + *, + native: Path, + package_root: Path, + out: Path, + wheel_tag: str | None = None, +) -> dict[str, str]: if not native.is_file(): _die(f"native library not found: {native}") python_pkg = package_root / "python" / "graphforge" @@ -103,6 +155,7 @@ def assemble_python(*, native: Path, package_root: Path, out: Path) -> dict[str, version = _read_version(package_root, "python") module_name = _native_python_module_name(native) native_hash = _sha256(native) + resolved_wheel_tag = _python_wheel_tag(wheel_tag) with tempfile.TemporaryDirectory(prefix="gf-bazel-py-wheel-") as tmp: staging = Path(tmp) @@ -122,22 +175,9 @@ def assemble_python(*, native: Path, package_root: Path, out: Path) -> dict[str, ) shutil.copy2(native, package_dir / module_name) - # Platform-specific abi3 wheel tag keeps clean-install expectations honest - # for CI smoke; full cross-platform matrix remains #6. - sys_name = platform.system() - mach = platform.machine().lower() - if sys_name == "Linux" and mach in ("x86_64", "amd64"): - wheel_tag = "cp310-abi3-manylinux_2_17_x86_64" - elif sys_name == "Linux" and mach in ("aarch64", "arm64"): - wheel_tag = "cp310-abi3-manylinux_2_17_aarch64" - elif sys_name == "Darwin" and mach in ("arm64", "aarch64"): - wheel_tag = "cp310-abi3-macosx_11_0_arm64" - elif sys_name == "Darwin" and mach in ("x86_64", "amd64"): - wheel_tag = "cp310-abi3-macosx_10_12_x86_64" - elif sys_name == "Windows" and mach in ("amd64", "x86_64"): - wheel_tag = "cp310-abi3-win_amd64" - else: - _die(f"unsupported host platform for abi3 wheel tagging: {sys_name}/{mach}") + # Explicit --wheel-tag models the #6 cross-platform matrix; host default + # remains for local/CI smoke when the tag is omitted. + wheel_tag = resolved_wheel_tag (dist_info / "METADATA").write_text( "\n".join( @@ -190,19 +230,26 @@ def assemble_python(*, native: Path, package_root: Path, out: Path) -> dict[str, "native_module": module_name, "version": version, "wheel": str(out), + "wheel_tag": resolved_wheel_tag, "recompiled": "false", } -def assemble_node(*, native: Path, package_root: Path, out: Path) -> dict[str, str]: +def assemble_node( + *, + native: Path, + package_root: Path, + out: Path, + platform_tag: str | None = None, +) -> dict[str, str]: if not native.is_file(): _die(f"native library not found: {native}") if not (package_root / "package.json").is_file(): _die(f"missing package.json under {package_root}") version = _read_version(package_root, "node") - platform_tag = _napi_platform_tag() - addon_name = f"graphforge.{platform_tag}.node" + resolved_platform_tag = _napi_platform_tag(platform_tag) + addon_name = f"graphforge.{resolved_platform_tag}.node" native_hash = _sha256(native) write_zip = out.suffix == ".zip" @@ -256,6 +303,7 @@ def assemble_node(*, native: Path, package_root: Path, out: Path) -> dict[str, s "language": "node", "native_sha256": native_hash, "addon": addon_name, + "platform_tag": resolved_platform_tag, "version": version, "recompiled": "false", } @@ -298,6 +346,16 @@ def main(argv: list[str] | None = None) -> int: default=None, help="Optional JSON evidence path (native hash, no-recompile assertion).", ) + parser.add_argument( + "--wheel-tag", + default=None, + help="Explicit abi3 wheel tag for #6 cross-platform matrix (Python).", + ) + parser.add_argument( + "--platform-tag", + default=None, + help="Explicit napi platform tag for #6 cross-platform matrix (Node).", + ) args = parser.parse_args(argv) native = args.native.resolve() @@ -305,9 +363,19 @@ def main(argv: list[str] | None = None) -> int: out = args.out.resolve() if args.language == "python": - evidence = assemble_python(native=native, package_root=package_root, out=out) + evidence = assemble_python( + native=native, + package_root=package_root, + out=out, + wheel_tag=args.wheel_tag, + ) else: - evidence = assemble_node(native=native, package_root=package_root, out=out) + evidence = assemble_node( + native=native, + package_root=package_root, + out=out, + platform_tag=args.platform_tag, + ) if args.write_evidence is not None: args.write_evidence.parent.mkdir(parents=True, exist_ok=True) diff --git a/scripts/ci/bazel-migration-ledger-check.py b/scripts/ci/bazel-migration-ledger-check.py new file mode 100644 index 000000000..797fb231d --- /dev/null +++ b/scripts/ci/bazel-migration-ledger-check.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Fail-closed Bazel migration ledger completeness check (#6). + +Every Cargo workspace target must appear in the checked-in target map as either +`mapped` (with a Bazel label) or `exception` (with a justified exception id). +Unmapped rows and stub retained-tool exceptions fail the gate. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import re +import subprocess +import sys +from typing import Any + +SCHEMA = "graphforge.bazel-migration-target-map.v1" +DEFAULT_MAP = Path("tools/bazel/parity/migration_target_map.json") +DEFAULT_LEDGER = Path("docs/development/bazel-migration-ledger.md") +ALLOWED_EXCEPTION_STATUS = frozenset({"justified", "handoff", "closed", "mapped", "excluded"}) +KIND_TO_CLASS = { + "lib": "lib", + "rlib": "lib", + "dylib": "lib", + "cdylib": "cdylib", + "bin": "bin", + "test": "integration-test", + "example": "example", + "custom-build": "custom-build", +} + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def cargo_targets(root: Path) -> list[tuple[str, str, str, str]]: + metadata = json.loads( + subprocess.check_output( + ["cargo", "metadata", "--format-version=1", "--no-deps", "--locked"], + cwd=root, + text=True, + ) + ) + workspace_members = set(metadata["workspace_members"]) + rows: list[tuple[str, str, str, str]] = [] + for package in metadata["packages"]: + if package["id"] not in workspace_members: + continue + for target in package["targets"]: + kind = (target.get("kind") or ["?"])[0] + cls = KIND_TO_CLASS.get(kind, kind) + src = target["src_path"] + prefix = str(root) + "/" + if src.startswith(prefix): + src = src[len(prefix) :] + rows.append((package["name"], target["name"], cls, src)) + rows.sort() + return rows + + +def load_map(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if payload.get("schema") != SCHEMA: + raise SystemExit(f"unexpected target-map schema: {payload.get('schema')!r}") + return payload + + +def markdown_has_unmapped(ledger: Path) -> list[str]: + if not ledger.is_file(): + return [f"missing ledger markdown: {ledger}"] + text = ledger.read_text(encoding="utf-8") + hits: list[str] = [] + for lineno, line in enumerate(text.splitlines(), start=1): + if re.search(r"\| `unmapped` \|", line): + hits.append(f"{ledger}:{lineno}: unmapped ledger row") + return hits + + +def check( + *, + root: Path, + map_path: Path, + ledger_path: Path, +) -> list[str]: + errors: list[str] = [] + payload = load_map(map_path) + mapped_entries = {(entry["package"], entry["target"]): entry for entry in payload["targets"]} + exceptions = {entry["id"]: entry for entry in payload.get("exceptions", [])} + + cargo_rows = cargo_targets(root) + if len(cargo_rows) != payload.get("cargo_target_count"): + errors.append( + "cargo_target_count mismatch: " + f"map={payload.get('cargo_target_count')} cargo={len(cargo_rows)}" + ) + + cargo_keys = {(pkg, tgt) for pkg, tgt, _cls, _src in cargo_rows} + map_keys = set(mapped_entries) + for missing in sorted(cargo_keys - map_keys): + errors.append(f"cargo target missing from map: {missing[0]}::{missing[1]}") + for extra in sorted(map_keys - cargo_keys): + errors.append(f"map entry not in cargo metadata: {extra[0]}::{extra[1]}") + + for pkg, tgt, cls, src in cargo_rows: + entry = mapped_entries.get((pkg, tgt)) + if entry is None: + continue + status = entry.get("status") + if status == "unmapped": + errors.append(f"unmapped target fails ledger: {pkg}::{tgt}") + continue + if status == "mapped": + label = entry.get("bazel_label") + if not label or not str(label).startswith("//"): + errors.append(f"mapped target missing bazel_label: {pkg}::{tgt}") + if entry.get("class") != cls: + # Allow ledger class names that match cargo normalization. + errors.append( + f"class mismatch for {pkg}::{tgt}: map={entry.get('class')} cargo={cls}" + ) + if entry.get("source") != src: + errors.append( + f"source mismatch for {pkg}::{tgt}: map={entry.get('source')} cargo={src}" + ) + continue + if status == "exception": + exception_id = entry.get("exception_id") + if not exception_id or exception_id not in exceptions: + errors.append( + f"exception target missing justified id: {pkg}::{tgt} ({exception_id!r})" + ) + continue + exc_status = exceptions[exception_id].get("status") + if exc_status not in ALLOWED_EXCEPTION_STATUS: + errors.append( + f"unjustified retained exception {exception_id}: status={exc_status!r}" + ) + continue + errors.append(f"unknown status {status!r} for {pkg}::{tgt}") + + for exception_id, exc in sorted(exceptions.items()): + status = exc.get("status") + if status == "stub": + errors.append(f"unjustified retained exception stub: {exception_id}") + elif status not in ALLOWED_EXCEPTION_STATUS: + errors.append(f"retained exception {exception_id} has invalid status {status!r}") + if not (exc.get("justification") or "").strip(): + errors.append(f"retained exception {exception_id} missing justification") + + errors.extend(markdown_has_unmapped(ledger_path)) + return errors + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=None) + parser.add_argument("--map", type=Path, default=DEFAULT_MAP) + parser.add_argument("--ledger", type=Path, default=DEFAULT_LEDGER) + args = parser.parse_args(argv) + + root = args.root.resolve() if args.root else repo_root() + map_path = args.map if args.map.is_absolute() else root / args.map + ledger_path = args.ledger if args.ledger.is_absolute() else root / args.ledger + + errors = check(root=root, map_path=map_path, ledger_path=ledger_path) + if errors: + print("bazel migration ledger check FAILED:", file=sys.stderr) + for error in errors: + print(f" - {error}", file=sys.stderr) + return 1 + + payload = load_map(map_path) + print( + "bazel migration ledger check OK: " + f"targets={payload['cargo_target_count']} " + f"exceptions={len(payload.get('exceptions', []))}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/cargo-bazel-parity-check.py b/scripts/ci/cargo-bazel-parity-check.py new file mode 100644 index 000000000..16a1868c7 --- /dev/null +++ b/scripts/ci/cargo-bazel-parity-check.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Same-SHA Cargo/Bazel dual-build parity gate (#6). + +Modes: + --inventory Fail-closed ledger + release-platform completeness + label query + --dual-run Execute the representative parity suite under Cargo and Bazel + --all inventory then dual-run (CI default) + +Writes machine-readable evidence JSON when --write-evidence is set. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import subprocess +import sys +from typing import Any + +SCHEMA = "graphforge.cargo-bazel-parity-evidence.v1" +RELEASE_SCHEMA = "graphforge.bazel-release-platforms.v1" +DEFAULT_MAP = Path("tools/bazel/parity/migration_target_map.json") +DEFAULT_PLATFORMS = Path("tools/bazel/release/release_platforms.json") +DEFAULT_SUITE = Path("tools/bazel/parity/parity_suite.json") +DEFAULT_RC_CONTRACT = Path("tests/contracts/binding-release-candidate-targets.json") +NODE_PACKAGE = Path("crates/graphforge-bindings-node/package.json") + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def git_sha(root: Path) -> str: + return subprocess.check_output( + ["git", "rev-parse", "HEAD"], + cwd=root, + text=True, + ).strip() + + +def run( + argv: list[str], + *, + cwd: Path, + check: bool = False, +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + argv, + cwd=cwd, + check=check, + capture_output=True, + text=True, + ) + + +def check_release_platforms(root: Path, platforms_path: Path, rc_path: Path) -> list[str]: + errors: list[str] = [] + payload = json.loads(platforms_path.read_text(encoding="utf-8")) + if payload.get("schema") != RELEASE_SCHEMA: + errors.append(f"unexpected release platform schema: {payload.get('schema')!r}") + return errors + + rc = json.loads(rc_path.read_text(encoding="utf-8")) + rc_ids = set((rc.get("targets") or {}).keys()) + platform_ids = {entry["id"] for entry in payload["platforms"]} + for missing in sorted(rc_ids - platform_ids): + errors.append(f"release platform missing Bazel model: {missing}") + for extra in sorted(platform_ids - rc_ids): + errors.append(f"Bazel release platform not in Binding RC contract: {extra}") + + node_pkg = json.loads((root / NODE_PACKAGE).read_text(encoding="utf-8")) + napi_targets = set((node_pkg.get("napi") or {}).get("targets") or []) + modeled_node_triples = { + entry["rust_triple"] for entry in payload["platforms"] if entry.get("language") == "node" + } + for missing in sorted(napi_targets - modeled_node_triples): + errors.append(f"napi package.json target missing Bazel platform: {missing}") + for extra in sorted(modeled_node_triples - napi_targets): + errors.append(f"Bazel node platform not in package.json napi.targets: {extra}") + + for entry in payload["platforms"]: + label = entry.get("bazel_platform") + if not label or not str(label).startswith("//platforms:"): + errors.append(f"platform {entry.get('id')} missing //platforms label") + if entry.get("language") == "python" and not entry.get("wheel_tag"): + errors.append(f"python platform {entry.get('id')} missing wheel_tag") + if entry.get("language") == "node" and not entry.get("node_platform_tag"): + errors.append(f"node platform {entry.get('id')} missing node_platform_tag") + + host = payload.get("host_release_artifacts") or {} + for key in ( + "cli", + "release_load_probe", + "api_examples", + "python_wheel_smoke", + "node_package_smoke", + "binding_cdylibs", + ): + if key not in host: + errors.append(f"host_release_artifacts missing {key}") + return errors + + +def check_labels_exist(root: Path, map_path: Path) -> list[str]: + payload = json.loads(map_path.read_text(encoding="utf-8")) + labels = { + entry["bazel_label"] + for entry in payload["targets"] + if entry.get("status") == "mapped" and entry.get("bazel_label") + } + platforms = json.loads((root / DEFAULT_PLATFORMS).read_text(encoding="utf-8")) + for value in (platforms.get("host_release_artifacts") or {}).values(): + if isinstance(value, str) and value.startswith("//"): + labels.add(value) + for entry in platforms.get("platforms") or []: + plat = entry.get("bazel_platform") + if isinstance(plat, str) and plat.startswith("//"): + labels.add(plat) + + errors: list[str] = [] + ordered = sorted(labels) + # `bazel query` accepts a set expression of labels. + result = run( + ["bazelisk", "query", f"set({' '.join(ordered)})"], + cwd=root, + ) + if result.returncode != 0: + for label in ordered: + one = run(["bazelisk", "query", label], cwd=root) + if one.returncode != 0: + errors.append(f"bazel label missing: {label}") + if not errors: + errors.append( + "bazelisk query failed for mapped labels:\n" + (result.stderr or result.stdout) + ) + return errors + + found = {line.strip() for line in result.stdout.splitlines() if line.strip()} + for label in ordered: + if label not in found: + errors.append(f"bazel label missing from query results: {label}") + return errors + + +def run_dual_suite(root: Path, suite_path: Path) -> tuple[list[str], list[dict[str, Any]]]: + suite = json.loads(suite_path.read_text(encoding="utf-8")) + errors: list[str] = [] + cases: list[dict[str, Any]] = [] + for case in suite.get("cases") or []: + case_id = case["id"] + cargo_cmd = ["cargo", *case["cargo"]] + bazel_cmd = ["bazelisk", "test", *case["bazel"], "--test_output=errors"] + cargo = run(cargo_cmd, cwd=root) + bazel = run(bazel_cmd, cwd=root) + cargo_ok = cargo.returncode == 0 + bazel_ok = bazel.returncode == 0 + record = { + "id": case_id, + "cargo_argv": cargo_cmd, + "bazel_argv": bazel_cmd, + "cargo_ok": cargo_ok, + "bazel_ok": bazel_ok, + "same_outcome": cargo_ok == bazel_ok, + } + cases.append(record) + if cargo_ok != bazel_ok: + errors.append( + f"parity outcome mismatch for {case_id}: cargo_ok={cargo_ok} bazel_ok={bazel_ok}" + ) + if not cargo_ok and not bazel_ok: + errors.append( + f"parity suite case failed on both paths: {case_id} " + "(fix the tests; do not weaken the gate)" + ) + return errors, cases + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=None) + parser.add_argument("--map", type=Path, default=DEFAULT_MAP) + parser.add_argument("--platforms", type=Path, default=DEFAULT_PLATFORMS) + parser.add_argument("--suite", type=Path, default=DEFAULT_SUITE) + parser.add_argument("--rc-contract", type=Path, default=DEFAULT_RC_CONTRACT) + parser.add_argument( + "--mode", + choices=("inventory", "dual-run", "all"), + default="all", + ) + parser.add_argument( + "--skip-label-query", + action="store_true", + help="Skip bazelisk query (unit tests / environments without Bazel).", + ) + parser.add_argument("--write-evidence", type=Path, default=None) + args = parser.parse_args(argv) + + root = args.root.resolve() if args.root else repo_root() + map_path = args.map if args.map.is_absolute() else root / args.map + platforms_path = args.platforms if args.platforms.is_absolute() else root / args.platforms + suite_path = args.suite if args.suite.is_absolute() else root / args.suite + rc_path = args.rc_contract if args.rc_contract.is_absolute() else root / args.rc_contract + + errors: list[str] = [] + evidence: dict[str, Any] = { + "schema": SCHEMA, + "sha": git_sha(root), + "mode": args.mode, + "dual_build": True, + "required_check": "CI Gate", + "inventory": {}, + "cases": [], + } + + if args.mode in ("inventory", "all"): + ledger = run( + [ + "python3", + str(root / "scripts/ci/bazel-migration-ledger-check.py"), + "--map", + str(map_path), + ], + cwd=root, + ) + if ledger.returncode != 0: + errors.append("ledger check failed:\n" + (ledger.stderr or ledger.stdout)) + platform_errors = check_release_platforms(root, platforms_path, rc_path) + errors.extend(platform_errors) + label_errors: list[str] = [] + if not args.skip_label_query: + label_errors = check_labels_exist(root, map_path) + errors.extend(label_errors) + evidence["inventory"] = { + "ledger_ok": ledger.returncode == 0, + "platforms_ok": not platform_errors, + "labels_ok": not label_errors, + "label_query_skipped": bool(args.skip_label_query), + } + + if args.mode in ("dual-run", "all"): + dual_errors, cases = run_dual_suite(root, suite_path) + errors.extend(dual_errors) + evidence["cases"] = cases + + evidence["ok"] = not errors + if args.write_evidence is not None: + out = ( + args.write_evidence if args.write_evidence.is_absolute() else root / args.write_evidence + ) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"wrote evidence {out}") + + if errors: + print("cargo/bazel parity check FAILED:", file=sys.stderr) + for error in errors: + print(f" - {error}", file=sys.stderr) + return 1 + + print(f"cargo/bazel parity check OK: sha={evidence['sha']} mode={args.mode} dual_build=true") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/classify-changes.sh b/scripts/ci/classify-changes.sh index a95dc3c35..1941d06aa 100755 --- a/scripts/ci/classify-changes.sh +++ b/scripts/ci/classify-changes.sh @@ -108,14 +108,22 @@ while IFS= read -r -d '' path; do MODULE.bazel | MODULE.bazel.lock | BUILD.bazel | .bazelrc | .bazelversion | \ cargo-bazel-lock.json | tools/bazel/* | tools/bazel/**/* | \ + platforms/BUILD.bazel | \ crates/BUILD.bazel | crates/*/BUILD.bazel | \ docs/contracts/examples/BUILD.bazel | docs/reference/BUILD.bazel | \ tests/features/BUILD.bazel | tests/tck/BUILD.bazel | \ + tests/release_workflows/BUILD.bazel | \ examples/agent_grounding/BUILD.bazel | \ + docs/development/bazel-migration-parity.md | \ + docs/development/bazel-migration-ledger.md | \ scripts/ci/cargo-bazel-drift-check.py | \ scripts/ci/test-cargo-bazel-drift-check.py | \ scripts/ci/assemble_bazel_binding_packages.py | \ scripts/ci/test-assemble-bazel-binding-packages.py | \ + scripts/ci/bazel-migration-ledger-check.py | \ + scripts/ci/test-bazel-migration-ledger-check.py | \ + scripts/ci/cargo-bazel-parity-check.py | \ + scripts/ci/test-cargo-bazel-parity-check.py | \ scripts/ci/BUILD.bazel) bazel=true ;; diff --git a/scripts/ci/test-assemble-bazel-binding-packages.py b/scripts/ci/test-assemble-bazel-binding-packages.py index 39be62d76..6e5b64eb0 100755 --- a/scripts/ci/test-assemble-bazel-binding-packages.py +++ b/scripts/ci/test-assemble-bazel-binding-packages.py @@ -92,6 +92,33 @@ def test_assemble_node_zip_embeds_native_bytes(self) -> None: body = json.loads(archive.read("bazel-native-evidence.json")) self.assertEqual(body["recompiled"], "false") + def test_explicit_cross_platform_tags(self) -> None: + py_root = ROOT / "crates" / "graphforge-bindings-py" + node_root = ROOT / "crates" / "graphforge-bindings-node" + with tempfile.TemporaryDirectory() as tmp: + py_native = Path(tmp) / "libgraphforge_bindings_py.so" + py_native.write_bytes(b"FAKE_PY") + py_out = Path(tmp) / "win.whl" + py_evidence = assemble_python( + native=py_native, + package_root=py_root, + out=py_out, + wheel_tag="cp310-abi3-win_amd64", + ) + self.assertEqual(py_evidence["wheel_tag"], "cp310-abi3-win_amd64") + + node_native = Path(tmp) / "libgraphforge_bindings_node.so" + node_native.write_bytes(b"FAKE_NODE") + node_out = Path(tmp) / "linux-arm.zip" + node_evidence = assemble_node( + native=node_native, + package_root=node_root, + out=node_out, + platform_tag="linux-arm64-gnu", + ) + self.assertEqual(node_evidence["platform_tag"], "linux-arm64-gnu") + self.assertEqual(node_evidence["addon"], "graphforge.linux-arm64-gnu.node") + if __name__ == "__main__": unittest.main() diff --git a/scripts/ci/test-bazel-migration-ledger-check.py b/scripts/ci/test-bazel-migration-ledger-check.py new file mode 100644 index 000000000..3eaa2fe15 --- /dev/null +++ b/scripts/ci/test-bazel-migration-ledger-check.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Prove the migration ledger check fails closed on unmapped / stub exceptions.""" + +from __future__ import annotations + +import json +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +CHECK = ROOT / "scripts/ci/bazel-migration-ledger-check.py" +MAP = ROOT / "tools/bazel/parity/migration_target_map.json" +LEDGER = ROOT / "docs/development/bazel-migration-ledger.md" + + +def main() -> None: + ok = subprocess.run( + ["python3", str(CHECK), "--map", str(MAP), "--ledger", str(LEDGER)], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + if ok.returncode != 0: + raise SystemExit(f"expected current ledger map to pass:\n{ok.stdout}\n{ok.stderr}") + + with tempfile.TemporaryDirectory(prefix="gf-ledger-") as tmp: + tmp_path = Path(tmp) + bad_map = tmp_path / "bad-map.json" + bad_ledger = tmp_path / "bad-ledger.md" + shutil.copy2(LEDGER, bad_ledger) + + payload = json.loads(MAP.read_text(encoding="utf-8")) + # Force an unmapped row and a stub exception. + payload["targets"][0]["status"] = "unmapped" + payload["targets"][0]["bazel_label"] = None + for exc in payload["exceptions"]: + if exc["id"] == "RT-fuzz": + exc["status"] = "stub" + break + bad_map.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + bad_ledger.write_text( + bad_ledger.read_text(encoding="utf-8") + + "\n| `graphforge-api` | `fake` | `example` | `x.rs` | — | `unmapped` | |\n", + encoding="utf-8", + ) + + drifted = subprocess.run( + ["python3", str(CHECK), "--map", str(bad_map), "--ledger", str(bad_ledger)], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + if drifted.returncode == 0: + raise SystemExit("expected mutated ledger to fail closed") + combined = (drifted.stdout + drifted.stderr).lower() + if "unmapped" not in combined: + raise SystemExit(f"expected unmapped failure:\n{drifted.stderr}") + if "stub" not in combined and "unjustified" not in combined: + raise SystemExit(f"expected stub/unjustified failure:\n{drifted.stderr}") + + print("bazel migration ledger check tests passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/test-cargo-bazel-parity-check.py b/scripts/ci/test-cargo-bazel-parity-check.py new file mode 100644 index 000000000..faa1b1252 --- /dev/null +++ b/scripts/ci/test-cargo-bazel-parity-check.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Unit tests for the Cargo/Bazel parity inventory gate (#6).""" + +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +CHECK = ROOT / "scripts/ci/cargo-bazel-parity-check.py" +PLATFORMS = ROOT / "tools/bazel/release/release_platforms.json" +MAP = ROOT / "tools/bazel/parity/migration_target_map.json" +RC = ROOT / "tests/contracts/binding-release-candidate-targets.json" + + +def main() -> None: + ok = subprocess.run( + [ + "python3", + str(CHECK), + "--mode", + "inventory", + "--skip-label-query", + "--map", + str(MAP), + "--platforms", + str(PLATFORMS), + "--rc-contract", + str(RC), + ], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + if ok.returncode != 0: + raise SystemExit(f"expected inventory parity check to pass:\n{ok.stdout}\n{ok.stderr}") + + with tempfile.TemporaryDirectory(prefix="gf-parity-") as tmp: + tmp_path = Path(tmp) + bad_platforms = tmp_path / "bad-platforms.json" + payload = json.loads(PLATFORMS.read_text(encoding="utf-8")) + # Drop a certified Binding RC target from the Bazel model. + payload["platforms"] = [ + entry for entry in payload["platforms"] if entry["id"] != "python-windows" + ] + bad_platforms.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + + failed = subprocess.run( + [ + "python3", + str(CHECK), + "--mode", + "inventory", + "--skip-label-query", + "--map", + str(MAP), + "--platforms", + str(bad_platforms), + "--rc-contract", + str(RC), + ], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + if failed.returncode == 0: + raise SystemExit("expected missing release platform to fail closed") + if "python-windows" not in (failed.stdout + failed.stderr): + raise SystemExit("expected python-windows missing-platform error:\n" + failed.stderr) + + print("cargo-bazel parity check tests passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/test-ci-storage-policy.py b/scripts/ci/test-ci-storage-policy.py index 52025da3d..6e011f0bc 100644 --- a/scripts/ci/test-ci-storage-policy.py +++ b/scripts/ci/test-ci-storage-policy.py @@ -55,6 +55,7 @@ "visualization-limits-stress-${{ github.sha }}": 1, "pr-python-wheel-${{ github.sha }}": 1, "pr-node-addon-${{ github.sha }}": 1, + "cargo-bazel-parity-evidence-${{ github.run_id }}": 1, } ) EXPECTED_ARTIFACT_DOWNLOADS = Counter( @@ -232,6 +233,7 @@ def artifact_contracts(text: str) -> tuple[list[str], list[str]]: ("candidate/release-artifacts/evidence/\ncandidate/release-artifacts/node-addons/"), "reconciliation/summary.json", "examples/visualization/stress/results/", + "dist/cargo-bazel-parity-evidence.json", }, f"artifact upload contains unapproved bytes: {path}" uploaded.append(name) for step in action_steps(text, "actions/download-artifact@"): diff --git a/scripts/ci/test-classify-changes.sh b/scripts/ci/test-classify-changes.sh index 7d5ac6afc..576f5faac 100755 --- a/scripts/ci/test-classify-changes.sh +++ b/scripts/ci/test-classify-changes.sh @@ -122,6 +122,11 @@ assert_classification "$bazel_only" tools/bazel/bindings/BUILD.bazel bazel-bindi assert_classification "$bazel_only" tests/features/BUILD.bazel bazel-features-build assert_classification "$bazel_only" tests/tck/BUILD.bazel bazel-tck-build assert_classification "$bazel_only" examples/agent_grounding/BUILD.bazel bazel-notebook-build +assert_classification "$bazel_only" platforms/BUILD.bazel bazel-release-platforms +assert_classification "$bazel_only" tests/release_workflows/BUILD.bazel bazel-release-workflow-inputs +assert_classification "$bazel_only" scripts/ci/cargo-bazel-parity-check.py bazel-parity-check +assert_classification "$bazel_only" scripts/ci/bazel-migration-ledger-check.py bazel-ledger-check +assert_classification "$bazel_only" docs/development/bazel-migration-parity.md bazel-parity-doc assert_classification "$none" "docs/a file with spaces.md" docs-only # Packaging-only Cargo metadata must not compile the workspace. diff --git a/tests/release_workflows/BUILD.bazel b/tests/release_workflows/BUILD.bazel new file mode 100644 index 000000000..7de087ea9 --- /dev/null +++ b/tests/release_workflows/BUILD.bazel @@ -0,0 +1,8 @@ +"""Non-source inputs for API release-workflow example binaries (#6).""" + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "sna_phase2_advisory", + srcs = ["sna-intelligence/ontologies/phase-2-advisory.yaml"], +) diff --git a/tools/bazel/gf_rust.bzl b/tools/bazel/gf_rust.bzl index 3070c0b9b..25c5c6bc7 100644 --- a/tools/bazel/gf_rust.bzl +++ b/tools/bazel/gf_rust.bzl @@ -92,12 +92,13 @@ def gf_cargo_build_script(name, deps = [], data = [], crate_features = [], **kwa **kwargs ) -def gf_rust_binary(name, deps = [], data = [], crate_features = [], **kwargs): +def gf_rust_binary(name, deps = [], data = [], compile_data = [], crate_features = [], **kwargs): """rust_binary wired to crate_universe deps for this package's Cargo.toml.""" rust_binary( name = name, srcs = kwargs.pop("srcs", native.glob(["src/**/*.rs"])), aliases = aliases(), + compile_data = compile_data, crate_features = crate_features, data = data, edition = "2024", diff --git a/tools/bazel/parity/BUILD.bazel b/tools/bazel/parity/BUILD.bazel new file mode 100644 index 000000000..1abb34662 --- /dev/null +++ b/tools/bazel/parity/BUILD.bazel @@ -0,0 +1,18 @@ +"""Same-SHA Cargo/Bazel parity inventory (#6).""" + +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "migration_target_map.json", + "parity_suite.json", +]) + +filegroup( + name = "migration_target_map", + srcs = ["migration_target_map.json"], +) + +filegroup( + name = "parity_suite", + srcs = ["parity_suite.json"], +) diff --git a/tools/bazel/parity/migration_target_map.json b/tools/bazel/parity/migration_target_map.json new file mode 100644 index 000000000..e61d03238 --- /dev/null +++ b/tools/bazel/parity/migration_target_map.json @@ -0,0 +1,957 @@ +{ + "schema": "graphforge.bazel-migration-target-map.v1", + "issue": 6, + "cargo_target_count": 90, + "targets": [ + { + "package": "graphforge-api", + "target": "atomic_recovery_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/atomic_recovery_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:atomic_recovery_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "bdd", + "class": "integration-test", + "source": "crates/graphforge-api/tests/bdd/main.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:bdd", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "bdd_timing", + "class": "integration-test", + "source": "crates/graphforge-api/tests/bdd_timing.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:bdd_timing", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "belief_subject_contract", + "class": "integration-test", + "source": "crates/graphforge-api/tests/belief_subject_contract.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:belief_subject_contract", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "bind_error_spans", + "class": "integration-test", + "source": "crates/graphforge-api/tests/bind_error_spans.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:bind_error_spans", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "clear", + "class": "integration-test", + "source": "crates/graphforge-api/tests/clear.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:clear", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "conductance", + "class": "integration-test", + "source": "crates/graphforge-api/tests/conductance.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:conductance", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "correction_churn_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/correction_churn_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:correction_churn_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "create_scaling", + "class": "integration-test", + "source": "crates/graphforge-api/tests/create_scaling.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:create_scaling", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "cyber_intrusion_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/cyber_intrusion_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:cyber_intrusion_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "derived_state_freshness_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/derived_state_freshness_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:derived_state_freshness_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "e2e_baseline", + "class": "integration-test", + "source": "crates/graphforge-api/tests/e2e_baseline.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:e2e_baseline", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "existential_subquery", + "class": "integration-test", + "source": "crates/graphforge-api/tests/existential_subquery.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:existential_subquery", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "facade_methods", + "class": "integration-test", + "source": "crates/graphforge-api/tests/facade_methods.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:facade_methods", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "finance_fraud_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/finance_fraud_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:finance_fraud_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "fixed_hop_limit", + "class": "integration-test", + "source": "crates/graphforge-api/tests/fixed_hop_limit.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:fixed_hop_limit", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "graph_internal_metadata", + "class": "integration-test", + "source": "crates/graphforge-api/tests/graph_internal_metadata.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:graph_internal_metadata", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "graphforge_api", + "class": "lib", + "source": "crates/graphforge-api/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:graphforge_api", + "exception_id": null, + "notes": "#9; unit tests `//crates/graphforge-api:graphforge_api_test`" + }, + { + "package": "graphforge-api", + "target": "inference_provenance", + "class": "integration-test", + "source": "crates/graphforge-api/tests/inference_provenance.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:inference_provenance", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "knowledge_evolution_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/knowledge_evolution_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:knowledge_evolution_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "knowledge_isolation", + "class": "integration-test", + "source": "crates/graphforge-api/tests/knowledge_isolation.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:knowledge_isolation", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "list_semantics", + "class": "integration-test", + "source": "crates/graphforge-api/tests/list_semantics.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:list_semantics", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "m22_m18_public_surface", + "class": "integration-test", + "source": "crates/graphforge-api/tests/m22_m18_public_surface.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:m22_m18_public_surface", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "m22_m19_public_surface", + "class": "integration-test", + "source": "crates/graphforge-api/tests/m22_m19_public_surface.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:m22_m19_public_surface", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "m22_provider_public_surface", + "class": "integration-test", + "source": "crates/graphforge-api/tests/m22_provider_public_surface.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:m22_provider_public_surface", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "max_bipartite_matching", + "class": "integration-test", + "source": "crates/graphforge-api/tests/max_bipartite_matching.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:max_bipartite_matching", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "max_cardinality_matching", + "class": "integration-test", + "source": "crates/graphforge-api/tests/max_cardinality_matching.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:max_cardinality_matching", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "max_weight_matching", + "class": "integration-test", + "source": "crates/graphforge-api/tests/max_weight_matching.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:max_weight_matching", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "minimum_k_spanning_tree", + "class": "integration-test", + "source": "crates/graphforge-api/tests/minimum_k_spanning_tree.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:minimum_k_spanning_tree", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "modularity", + "class": "integration-test", + "source": "crates/graphforge-api/tests/modularity.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:modularity", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "multi_label_scaling", + "class": "integration-test", + "source": "crates/graphforge-api/tests/multi_label_scaling.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:multi_label_scaling", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "ontology_emergence_strict_handoff", + "class": "example", + "source": "crates/graphforge-api/examples/ontology_emergence_strict_handoff.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:ontology_emergence_strict_handoff", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "pattern_comprehension", + "class": "integration-test", + "source": "crates/graphforge-api/tests/pattern_comprehension.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:pattern_comprehension", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "percentile_aggregates", + "class": "integration-test", + "source": "crates/graphforge-api/tests/percentile_aggregates.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:percentile_aggregates", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "probate_genealogy_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/probate_genealogy_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:probate_genealogy_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "provider_session", + "class": "integration-test", + "source": "crates/graphforge-api/tests/provider_session.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:provider_session", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "public_facade_remaining_conformance", + "class": "integration-test", + "source": "crates/graphforge-api/tests/public_facade_remaining_conformance.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:public_facade_remaining_conformance", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "public_lifecycle_conformance", + "class": "integration-test", + "source": "crates/graphforge-api/tests/public_lifecycle_conformance.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:public_lifecycle_conformance", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "release_load_construction", + "class": "integration-test", + "source": "crates/graphforge-api/tests/release_load_construction.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:release_load_construction", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "release_load_probe", + "class": "example", + "source": "crates/graphforge-api/examples/release_load_probe.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:release_load_probe", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "sna_intelligence_workflow", + "class": "example", + "source": "crates/graphforge-api/examples/sna_intelligence_workflow.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:sna_intelligence_workflow", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "strict_add_node_fixture", + "class": "example", + "source": "crates/graphforge-api/examples/strict_add_node_fixture.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:strict_add_node_fixture", + "exception_id": null, + "notes": "#6 example binary" + }, + { + "package": "graphforge-api", + "target": "strict_runtime_properties", + "class": "integration-test", + "source": "crates/graphforge-api/tests/strict_runtime_properties.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:strict_runtime_properties", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "value_access_semantics", + "class": "integration-test", + "source": "crates/graphforge-api/tests/value_access_semantics.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:value_access_semantics", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "value_semantics", + "class": "integration-test", + "source": "crates/graphforge-api/tests/value_semantics.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:value_semantics", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "with_aggregation", + "class": "integration-test", + "source": "crates/graphforge-api/tests/with_aggregation.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:with_aggregation", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-api", + "target": "xor_scaling", + "class": "integration-test", + "source": "crates/graphforge-api/tests/xor_scaling.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-api:xor_scaling", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-ast", + "target": "graphforge_ast", + "class": "lib", + "source": "crates/graphforge-ast/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-ast:graphforge_ast", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-ast:graphforge_ast_test`" + }, + { + "package": "graphforge-bindings-node", + "target": "build-script-build", + "class": "custom-build", + "source": "crates/graphforge-bindings-node/build.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-bindings-node:graphforge_bindings_node_build_script", + "exception_id": null, + "notes": "#7; `napi-build` via `gf_cargo_build_script`" + }, + { + "package": "graphforge-bindings-node", + "target": "graphforge_bindings_node", + "class": "cdylib", + "source": "crates/graphforge-bindings-node/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-bindings-node:graphforge_bindings_node", + "exception_id": null, + "notes": "#7; packaging `//:node_package_smoke`" + }, + { + "package": "graphforge-bindings-py", + "target": "graphforge_bindings_py", + "class": "cdylib", + "source": "crates/graphforge-bindings-py/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-bindings-py:graphforge_bindings_py", + "exception_id": null, + "notes": "#7; packaging `//:python_wheel_smoke`" + }, + { + "package": "graphforge-cli", + "target": "build-script-build", + "class": "custom-build", + "source": "crates/graphforge-cli/build.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cli:graphforge_cli_build_script", + "exception_id": null, + "notes": "#7/#8; RT-cli-build-script closed" + }, + { + "package": "graphforge-cli", + "target": "checkpoints", + "class": "integration-test", + "source": "crates/graphforge-cli/tests/checkpoints.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cli:checkpoints", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-cli", + "target": "gf", + "class": "bin", + "source": "crates/graphforge-cli/src/main.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cli:gf", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-cli", + "target": "graphforge_cli", + "class": "lib", + "source": "crates/graphforge-cli/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cli:graphforge_cli", + "exception_id": null, + "notes": "#7/#8; unit `//crates/graphforge-cli:graphforge_cli_test`" + }, + { + "package": "graphforge-cli", + "target": "portable", + "class": "integration-test", + "source": "crates/graphforge-cli/tests/portable.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cli:portable", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-cli", + "target": "repository", + "class": "integration-test", + "source": "crates/graphforge-cli/tests/repository.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cli:repository", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-core", + "target": "graphforge_core", + "class": "lib", + "source": "crates/graphforge-core/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-core:graphforge_core", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-core:graphforge_core_test`" + }, + { + "package": "graphforge-cypher", + "target": "corpus", + "class": "integration-test", + "source": "crates/graphforge-cypher/tests/corpus.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cypher:corpus", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-cypher", + "target": "graphforge_cypher", + "class": "lib", + "source": "crates/graphforge-cypher/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-cypher:graphforge_cypher", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-cypher:graphforge_cypher_test`" + }, + { + "package": "graphforge-exec", + "target": "adjacency_expand", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/adjacency_expand.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:adjacency_expand", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "bench_traversal_scaling", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/bench_traversal_scaling.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:bench_traversal_scaling", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "create_execution", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/create_execution.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:create_execution", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "create_input_driven", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/create_input_driven.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:create_input_driven", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "differential_traversal", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/differential_traversal.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:differential_traversal", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "explain_snapshots", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/explain_snapshots.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:explain_snapshots", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "graphforge_exec", + "class": "lib", + "source": "crates/graphforge-exec/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:graphforge_exec", + "exception_id": null, + "notes": "#9; unit tests `//crates/graphforge-exec:graphforge_exec_test`" + }, + { + "package": "graphforge-exec", + "target": "optional_match", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/optional_match.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:optional_match", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "persistent_adjacency", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/persistent_adjacency.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:persistent_adjacency", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "read_execution", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/read_execution.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:read_execution", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "unwind", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/unwind.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:unwind", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "var_len_expand", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/var_len_expand.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:var_len_expand", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-exec", + "target": "write_statement", + "class": "integration-test", + "source": "crates/graphforge-exec/tests/write_statement.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-exec:write_statement", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-io", + "target": "graphforge_io", + "class": "lib", + "source": "crates/graphforge-io/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-io:graphforge_io", + "exception_id": null, + "notes": "#9; unit tests `//crates/graphforge-io:graphforge_io_test`" + }, + { + "package": "graphforge-ir", + "target": "golden", + "class": "integration-test", + "source": "crates/graphforge-ir/tests/golden.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-ir:golden", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-ir", + "target": "graphforge_ir", + "class": "lib", + "source": "crates/graphforge-ir/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-ir:graphforge_ir", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-ir:graphforge_ir_test`" + }, + { + "package": "graphforge-knowledge", + "target": "graphforge_knowledge", + "class": "lib", + "source": "crates/graphforge-knowledge/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-knowledge:graphforge_knowledge", + "exception_id": null, + "notes": "#9; unit tests `//crates/graphforge-knowledge:graphforge_knowledge_test`" + }, + { + "package": "graphforge-ontology", + "target": "graphforge_ontology", + "class": "lib", + "source": "crates/graphforge-ontology/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-ontology:graphforge_ontology", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-ontology:graphforge_ontology_test`" + }, + { + "package": "graphforge-ontology", + "target": "integration", + "class": "integration-test", + "source": "crates/graphforge-ontology/tests/integration.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-ontology:integration", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-plan", + "target": "graphforge_plan", + "class": "lib", + "source": "crates/graphforge-plan/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-plan:graphforge_plan", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-plan:graphforge_plan_test`" + }, + { + "package": "graphforge-provenance", + "target": "graphforge_provenance", + "class": "lib", + "source": "crates/graphforge-provenance/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-provenance:graphforge_provenance", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-provenance:graphforge_provenance_test`" + }, + { + "package": "graphforge-rel", + "target": "expression_lowering_matrix", + "class": "integration-test", + "source": "crates/graphforge-rel/tests/expression_lowering_matrix.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-rel:expression_lowering_matrix", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-rel", + "target": "graphforge_rel", + "class": "lib", + "source": "crates/graphforge-rel/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-rel:graphforge_rel", + "exception_id": null, + "notes": "#10; unit tests `//crates/graphforge-rel:graphforge_rel_test`" + }, + { + "package": "graphforge-rel", + "target": "logical_plan_golden", + "class": "integration-test", + "source": "crates/graphforge-rel/tests/logical_plan_golden.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-rel:logical_plan_golden", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-search", + "target": "graphforge_search", + "class": "lib", + "source": "crates/graphforge-search/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-search:graphforge_search", + "exception_id": null, + "notes": "#9; unit tests `//crates/graphforge-search:graphforge_search_test`" + }, + { + "package": "graphforge-storage", + "target": "adjacency_delta_write", + "class": "integration-test", + "source": "crates/graphforge-storage/tests/adjacency_delta_write.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-storage:adjacency_delta_write", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-storage", + "target": "filtered_read", + "class": "integration-test", + "source": "crates/graphforge-storage/tests/filtered_read.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-storage:filtered_read", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-storage", + "target": "graph_writer", + "class": "integration-test", + "source": "crates/graphforge-storage/tests/graph_writer.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-storage:graph_writer", + "exception_id": null, + "notes": "#8" + }, + { + "package": "graphforge-storage", + "target": "graphforge_storage", + "class": "lib", + "source": "crates/graphforge-storage/src/lib.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-storage:graphforge_storage", + "exception_id": null, + "notes": "#10 early / #9; Bazel enables `test-failpoints` for api subprocess unification; unit tests `//crates/graphforge-storage:graphforge_storage_test`" + }, + { + "package": "graphforge-storage", + "target": "io_stats", + "class": "integration-test", + "source": "crates/graphforge-storage/tests/io_stats.rs", + "status": "mapped", + "bazel_label": "//crates/graphforge-storage:io_stats", + "exception_id": null, + "notes": "#8" + } + ], + "exceptions": [ + { + "id": "RT-fuzz", + "tool": "cargo fuzz", + "status": "justified", + "justification": "cargo-fuzz driver + corpus workflow outside ordinary rules_rust test graph; keep Cargo until a dedicated fuzz modeling slice" + }, + { + "id": "RT-publish-crates", + "tool": "cargo publish / crates.io authorize", + "status": "justified", + "justification": "Ecosystem publication metadata and registry auth are not Bazel compilation; Cargo remains authoritative for crates.io" + }, + { + "id": "RT-maturin-assemble", + "tool": "maturin assemble/sign/publish", + "status": "handoff", + "justification": "Bazel cdylib + assemble_bazel_binding_packages.py; maturin may sign/publish without silent recompile" + }, + { + "id": "RT-napi-assemble", + "tool": "napi assemble/sign/publish", + "status": "handoff", + "justification": "Bazel cdylib + assemble_bazel_binding_packages.py; napi may assemble/sign/publish without silent recompile" + }, + { + "id": "RT-cli-build-script", + "tool": "graphforge-cli build.rs", + "status": "closed", + "justification": "Mapped via cargo_build_script + project_skills_bundle (#7/#8)" + }, + { + "id": "RT-bindings-cdylib", + "tool": "PyO3/napi cdylibs", + "status": "mapped", + "justification": "Mapped as rust_shared_library + packaging smoke (#7)" + }, + { + "id": "RT-examples", + "tool": "graphforge-api examples", + "status": "closed", + "justification": "All 11 example binaries mapped under //crates/graphforge-api:* (#6)" + }, + { + "id": "RT-mobile", + "tool": "Swift/Kotlin/UniFFI/mobile", + "status": "excluded", + "justification": "Abandoned for M2; not required targets" + } + ] +} diff --git a/tools/bazel/parity/parity_suite.json b/tools/bazel/parity/parity_suite.json new file mode 100644 index 000000000..11f79d6c9 --- /dev/null +++ b/tools/bazel/parity/parity_suite.json @@ -0,0 +1,32 @@ +{ + "schema": "graphforge.cargo-bazel-parity-suite.v1", + "issue": 6, + "description": "Representative mapped targets exercised by both Cargo and Bazel at one SHA", + "cases": [ + { + "id": "ast-unit", + "cargo": ["test", "-p", "graphforge-ast", "--lib", "--no-fail-fast"], + "bazel": ["//crates/graphforge-ast:graphforge_ast_test"] + }, + { + "id": "ir-golden", + "cargo": ["test", "-p", "graphforge-ir", "--test", "golden", "--no-fail-fast"], + "bazel": ["//crates/graphforge-ir:golden"] + }, + { + "id": "api-clear", + "cargo": ["test", "-p", "graphforge-api", "--test", "clear", "--no-fail-fast"], + "bazel": ["//crates/graphforge-api:clear"] + }, + { + "id": "api-value-semantics", + "cargo": ["test", "-p", "graphforge-api", "--test", "value_semantics", "--no-fail-fast"], + "bazel": ["//crates/graphforge-api:value_semantics"] + }, + { + "id": "rel-logical-plan-golden", + "cargo": ["test", "-p", "graphforge-rel", "--test", "logical_plan_golden", "--no-fail-fast"], + "bazel": ["//crates/graphforge-rel:logical_plan_golden"] + } + ] +} diff --git a/tools/bazel/release/BUILD.bazel b/tools/bazel/release/BUILD.bazel new file mode 100644 index 000000000..d2fe053d9 --- /dev/null +++ b/tools/bazel/release/BUILD.bazel @@ -0,0 +1,10 @@ +"""Release platform inventory for #6 Cargo/Bazel parity.""" + +package(default_visibility = ["//visibility:public"]) + +exports_files(["release_platforms.json"]) + +filegroup( + name = "release_platforms", + srcs = ["release_platforms.json"], +) diff --git a/tools/bazel/release/release_platforms.json b/tools/bazel/release/release_platforms.json new file mode 100644 index 000000000..03b5b02f1 --- /dev/null +++ b/tools/bazel/release/release_platforms.json @@ -0,0 +1,89 @@ +{ + "schema": "graphforge.bazel-release-platforms.v1", + "issue": 6, + "contract": "tests/contracts/binding-release-candidate-targets.json", + "platforms": [ + { + "id": "python-ubuntu", + "language": "python", + "execution_mode": "native", + "bazel_platform": "//platforms:linux_x86_64", + "rust_triple": "x86_64-unknown-linux-gnu", + "wheel_tag": "cp310-abi3-manylinux_2_17_x86_64", + "node_platform_tag": null + }, + { + "id": "python-macos", + "language": "python", + "execution_mode": "native", + "bazel_platform": "//platforms:macos_aarch64", + "rust_triple": "aarch64-apple-darwin", + "wheel_tag": "cp310-abi3-macosx_11_0_arm64", + "node_platform_tag": null, + "notes": "Binding RC macOS Python runs on blacksmith-12vcpu-macos-15 (arm64)" + }, + { + "id": "python-windows", + "language": "python", + "execution_mode": "native", + "bazel_platform": "//platforms:windows_x86_64", + "rust_triple": "x86_64-pc-windows-msvc", + "wheel_tag": "cp310-abi3-win_amd64", + "node_platform_tag": null + }, + { + "id": "node-x86_64-unknown-linux-gnu", + "language": "node", + "execution_mode": "native", + "bazel_platform": "//platforms:linux_x86_64", + "rust_triple": "x86_64-unknown-linux-gnu", + "wheel_tag": null, + "node_platform_tag": "linux-x64-gnu" + }, + { + "id": "node-aarch64-unknown-linux-gnu", + "language": "node", + "execution_mode": "package-validation", + "bazel_platform": "//platforms:linux_aarch64", + "rust_triple": "aarch64-unknown-linux-gnu", + "wheel_tag": null, + "node_platform_tag": "linux-arm64-gnu", + "notes": "Supported Node cross-target (napi-cross on x86_64 Linux host)" + }, + { + "id": "node-x86_64-apple-darwin", + "language": "node", + "execution_mode": "native", + "bazel_platform": "//platforms:macos_x86_64", + "rust_triple": "x86_64-apple-darwin", + "wheel_tag": null, + "node_platform_tag": "darwin-x64" + }, + { + "id": "node-aarch64-apple-darwin", + "language": "node", + "execution_mode": "native", + "bazel_platform": "//platforms:macos_aarch64", + "rust_triple": "aarch64-apple-darwin", + "wheel_tag": null, + "node_platform_tag": "darwin-arm64" + }, + { + "id": "node-x86_64-pc-windows-msvc", + "language": "node", + "execution_mode": "native", + "bazel_platform": "//platforms:windows_x86_64", + "rust_triple": "x86_64-pc-windows-msvc", + "wheel_tag": null, + "node_platform_tag": "win32-x64-msvc" + } + ], + "host_release_artifacts": { + "cli": "//crates/graphforge-cli:gf", + "release_load_probe": "//crates/graphforge-api:release_load_probe", + "api_examples": "//crates/graphforge-api:api_example_bins", + "python_wheel_smoke": "//:python_wheel_smoke", + "node_package_smoke": "//:node_package_smoke", + "binding_cdylibs": "//:binding_cdylibs" + } +}