mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* ci: parallelize affected crate tests with nextest * ci: preserve process-local test contracts under nextest - serialize composition runtime and CLI trace-policy tests\n- preserve zero-test package buckets\n- restore fail-closed metadata routing * fix(ci): validate cargo runner bucket subsets * Address PR review feedback (#8013) - Skip nextest installation for exact-target and Cargo-only buckets. - Validate serialized nextest groups against compiled test inventory. * fix(ci): parse nextest inventory specifications Replace the YAML-sensitive heredoc with a Bash array and syntax-check the rendered crate runner in the workflow contract suite. --------- Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com>
1589 lines
71 KiB
Python
1589 lines
71 KiB
Python
#!/usr/bin/env python3
|
|
"""Select focused Reborn test lanes for pull requests and merge groups.
|
|
|
|
Diff events run direct evidence for changed packages and test surfaces.
|
|
Global or unattributable merge-group changes widen to exhaustive coverage;
|
|
main and manual runs remain exhaustive.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import functools
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tomllib
|
|
from collections import defaultdict
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# The WebUI frontend path prefix is resolved by crate NAME through the shared
|
|
# inventory (scripts/ci/lib/crate_tree.py), not a literal `crates/ironclaw_webui`
|
|
# prefix. Under the target-architecture family move
|
|
# (crates/<family>/ironclaw_*, PROPOSAL §5) a literal prefix stops matching,
|
|
# frontend diffs stop routing to the Code Style lane, and the planner reports
|
|
# "no Reborn test surface changed" for a WebUI change — silently, since
|
|
# nothing else covers that lane. See
|
|
# docs/internal/reborn/target-architecture/CHECKLIST.md WS10.
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent / "lib"))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from crate_tree import CrateTreeError, crate_directory # noqa: E402
|
|
from integration_test_inventory import ( # noqa: E402
|
|
INTEGRATION_PARTITION_COUNT,
|
|
planner_test_lanes,
|
|
)
|
|
|
|
# Owns the publication fence (.mintignore parsing and matching).
|
|
import docs_publication_boundary # noqa: E402
|
|
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
def _webui_frontend_prefix() -> str:
|
|
"""`<ironclaw_webui crate dir>/frontend/`, resolved once per process."""
|
|
try:
|
|
directory = crate_directory("ironclaw_webui", ROOT)
|
|
except CrateTreeError as error:
|
|
raise RuntimeError(
|
|
"reborn_pr_test_plan: cannot resolve the ironclaw_webui crate, so "
|
|
f"the frontend path prefix used to route Code Style is unknown: {error}"
|
|
) from error
|
|
return f"{directory}/frontend/"
|
|
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
def _sandbox_crate_directory() -> str:
|
|
"""`<ironclaw_sandbox crate dir>`, resolved once per process."""
|
|
try:
|
|
return crate_directory("ironclaw_sandbox", ROOT)
|
|
except CrateTreeError as error:
|
|
raise RuntimeError(
|
|
"reborn_pr_test_plan: cannot resolve the ironclaw_sandbox crate, so "
|
|
f"the source paths used to route the Docker lane are unknown: {error}"
|
|
) from error
|
|
|
|
|
|
def _sandbox_docker_prefixes() -> tuple[str, ...]:
|
|
"""Sandbox source prefixes whose changes require the Docker lane."""
|
|
directory = _sandbox_crate_directory()
|
|
return (
|
|
f"{directory}/src/sandbox_process",
|
|
f"{directory}/tests/user_sandbox_docker_live/",
|
|
)
|
|
|
|
|
|
MAX_PR_CRATE_BUCKETS = 3
|
|
DIFF_EVENTS = {"pull_request", "merge_group"}
|
|
FULL_EVENTS = {"push", "workflow_call", "workflow_dispatch", "schedule"}
|
|
ALL_ROOT_PARTITIONS = (0, 1, 2, 3)
|
|
ALL_INTEGRATION_LANES = (*range(INTEGRATION_PARTITION_COUNT), "groups")
|
|
NEXTEST_LIBTEST_TARGET_KINDS = {
|
|
"lib",
|
|
"bin",
|
|
"test",
|
|
"bench",
|
|
"example",
|
|
"proc-macro",
|
|
}
|
|
# Doc-fact contract tests (#7378) read `docs/` pages from inside owning
|
|
# crates, so a published-page edit can fail a cargo test. Route those edits
|
|
# to exactly the doc-fact test binaries (no reverse-dependency widening —
|
|
# prose can only change the assertions that read it); otherwise docs-only
|
|
# PRs merge green and the failure lands on an unrelated change later.
|
|
DOC_FACT_PAGE_TESTS = {
|
|
"docs/using/cli.mdx": ("ironclaw", "docs_cli_reference"),
|
|
"docs/api/responses.mdx": ("ironclaw_openai_compat", "docs_responses_contract"),
|
|
}
|
|
DOC_FACT_PUBLISHED_SWEEP = (
|
|
"ironclaw_extension_registry",
|
|
"docs_manifest_schema_version",
|
|
)
|
|
DOCS_PREFIX = "docs/"
|
|
DOCS_MINTIGNORE = "docs/.mintignore"
|
|
|
|
|
|
@functools.cache
|
|
def _publication_fence() -> list[str]:
|
|
"""The `.mintignore` patterns, parsed from the authoritative file so a
|
|
removed fence entry widens the routing with the sweep it selects. A
|
|
missing file means no fence (everything published), matching
|
|
docs_publication_boundary.find_violations()."""
|
|
path = Path(__file__).resolve().parents[2] / DOCS_MINTIGNORE
|
|
if not path.exists():
|
|
return []
|
|
return docs_publication_boundary.parse_mintignore(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _doc_fact_selections(path: str) -> list[tuple[str, str]]:
|
|
"""(package, test target) pairs whose doc-fact tests read this path."""
|
|
if not path.startswith(DOCS_PREFIX):
|
|
return []
|
|
if path == DOCS_MINTIGNORE:
|
|
# A fence edit changes the sweep's scope; run it.
|
|
return [DOC_FACT_PUBLISHED_SWEEP]
|
|
selections = []
|
|
page = DOC_FACT_PAGE_TESTS.get(path)
|
|
if page is not None:
|
|
selections.append(page)
|
|
if Path(path).suffix in {".md", ".mdx"} and not docs_publication_boundary.is_ignored(
|
|
PurePosixPath(path[len(DOCS_PREFIX) :]), _publication_fence()
|
|
):
|
|
selections.append(DOC_FACT_PUBLISHED_SWEEP)
|
|
return selections
|
|
|
|
|
|
# Path classes with no Rust or E2E surface any Reborn lane can exercise.
|
|
# `.claude/` is agent guidance (skills, commands, rules) — prose in the same
|
|
# class as `docs/` (whose published Markdown is escalated by the doc-fact arm
|
|
# above before this class applies; only fenced trees and non-page files reach
|
|
# it). It was unclassified until 2026-08-03, which meant the
|
|
# planner's fail-closed arm rejected every PR that touched agent guidance:
|
|
# the only satisfiable behaviour for that class was "never edit it", which is
|
|
# not a policy anyone chose. Classifying it is the fix; loosening the
|
|
# fail-closed arm is not.
|
|
# `openwiki/` is the auto-generated prose wiki, regenerated by
|
|
# `.github/workflows/openwiki-update.yml` and explicitly not hand-edited (see
|
|
# the root CLAUDE.md). It has no build or test surface — exactly `docs/`'s
|
|
# footing — and was unclassified until 2026-08-05, so the fail-closed arm
|
|
# rejected every PR that touched it. A crate rename touches it by construction
|
|
# (its prose names crate directories), which is how the gap surfaced. Same fix
|
|
# as the `.claude/` and `.env.example` gaps above: classify it, rather than
|
|
# loosen the arm that catches genuinely unknown paths.
|
|
IGNORED_PREFIXES = (
|
|
"docs/",
|
|
"openwiki/",
|
|
".claude/",
|
|
# IronLoop reads this repository configuration and optional role guidance;
|
|
# no Reborn crate or test lane consumes it.
|
|
".ironloop/",
|
|
".github/ISSUE_TEMPLATE/",
|
|
# `ISSUE_TEMPLATE/`'s exact sibling: a GitHub UI template that changes no
|
|
# crate, test, or runtime surface (`classify-test-scope.sh` already pairs
|
|
# the two in its docs-only arm). The rest of `.github/` stays undecided.
|
|
".github/pull_request_template.md",
|
|
# Agent bootstrap data only. The compressed graph and its attributes do
|
|
# not change an Ironclaw crate, test, or runtime surface. (#7215 commits
|
|
# the artifact; ported from main's planner at the #7152 refresh.)
|
|
".codebase-memory/",
|
|
)
|
|
IGNORED_GUIDANCE_PATHS = {
|
|
"tests/AGENTS.md",
|
|
"tests/CLAUDE.md",
|
|
"tests/integration/AGENTS.md",
|
|
"tests/integration/CLAUDE.md",
|
|
# `check-guidance.py`'s alias rule also requires this pair (`crates/` or
|
|
# `TESTS_PREFIX`); without an explicit entry here it falls into the
|
|
# `tests/support/reborn_parity_qa/` arm below and over-selects the root
|
|
# partition for a pure guidance edit.
|
|
"tests/support/reborn_parity_qa/AGENTS.md",
|
|
"tests/support/reborn_parity_qa/CLAUDE.md",
|
|
}
|
|
# Repo-root prose/example files with no build or test surface. Root `*.md` is
|
|
# already handled inline below; this covers the non-`.md` siblings.
|
|
#
|
|
# `.env.example` is documentation of environment variables, not an input to
|
|
# anything: no crate, test, or workflow reads the file (only doc comments
|
|
# mention it by name). It was unclassified until 2026-08-04, so the
|
|
# fail-closed arm rejected every PR that corrected an env-var comment — the
|
|
# same shape as the `.claude/` gap above, and the same fix: classify it,
|
|
# rather than loosen the arm that catches genuinely unknown paths.
|
|
IGNORED_ROOT_FILES = (".env.example",)
|
|
DEDICATED_WORKFLOW_PREFIXES = (
|
|
"tools/ironclaw_stress/",
|
|
# `harness/latency/runner` is a standalone cargo project with its own
|
|
# Cargo.lock, excluded from the workspace. No Reborn test lane builds
|
|
# it, so it selects nothing rather than failing the planner closed.
|
|
# (Same *class* as WORKSPACE_EXCLUDED_PREFIXES below; kept here because
|
|
# renaming its reason string would churn nothing useful.)
|
|
"harness/",
|
|
)
|
|
# Cargo projects this workspace deliberately does not build, with no workflow
|
|
# of their own either. No lane can be selected for them, so they select nothing
|
|
# — the honest answer, and not the same claim as "a dedicated workflow owns it".
|
|
#
|
|
# `tools/ironclaw_silk_decoder` is `[workspace]`-rooted and named in the root
|
|
# `exclude`: `cargo build --workspace` never sees it and `cargo metadata` never
|
|
# lists it. It was reachable only through the `crates/**` arm until WS7 moved
|
|
# it to `tools/` (PROPOSAL §12.13 D-O), and that arm answered **badly** — a
|
|
# one-line edit to the excluded helper's `main.rs` selected the ENTIRE
|
|
# workspace, because the crate-attribution fall-through treats a path under
|
|
# `crates/` with no owning member as broad risk. So this entry does not merely
|
|
# restore the old behaviour; it replaces a silent over-selection with an
|
|
# explicit "nothing builds this".
|
|
WORKSPACE_EXCLUDED_PREFIXES = ("tools/ironclaw_silk_decoder/",)
|
|
DEDICATED_E2E_PREFIX = "tests/e2e/"
|
|
QA_HARNESS_PREFIXES = (
|
|
# ⚠ `scripts/live-canary/` and `scripts/live_canary/` are BOTH real
|
|
# directories that differ only by hyphen-vs-underscore, and classifying one
|
|
# does not classify the other. The hyphen tree is the canary's fixtures and
|
|
# reporters; the underscore tree is its importable Python package
|
|
# (`__init__.py`, `common.py`, `auth_runtime.py`). Both are read only by
|
|
# `live-canary.yml` and `reborn-e2e.yml` — dedicated workflows with their
|
|
# own scope detectors — and by no `Tests (Reborn)` lane.
|
|
#
|
|
# The underscore tree was unclassified until 2026-08-05: a crate rename
|
|
# reaches it (`common.py` builds a `RUST_LOG` value naming a crate), so the
|
|
# fail-closed arm rejected the whole of WS6 on one string. Same class as
|
|
# `scripts/reborn_qa_matrix/` below, and the same fix.
|
|
"scripts/live-canary/",
|
|
"scripts/live_canary/",
|
|
"scripts/reborn_webui_v2_live_qa/",
|
|
# The QA surface-inventory auditor and its self-test. Same footing as the
|
|
# two above: an offline reporting tool over the WebUI/OpenAI-compat route
|
|
# descriptors, run by hand and by the QA matrix, never by a `Tests (Reborn)`
|
|
# lane. Added 2026-08-04 (WS10) — before this, editing it raised
|
|
# `unmapped test or CI path` and failed `Detect Reborn test scope`, skipping
|
|
# every downstream Reborn lane. Same class as the `.claude/` gap this row
|
|
# already records.
|
|
"scripts/reborn_qa_matrix/",
|
|
# The tool-discovery benchmark is a manual live-model harness. It records
|
|
# QA evidence across disclosure modes and catalog sizes; no Reborn Rust
|
|
# lane invokes it.
|
|
"scripts/tool_discovery_benchmark/",
|
|
# The live Telegram release smoke harness (`run_smoke.py` + config +
|
|
# README): run by hand against a real bot, referenced by no workflow, never
|
|
# by a `Tests (Reborn)` lane. Unclassified until 2026-08-06, when PR
|
|
# #7264's stale-command fix in its README hit the fail-closed arm.
|
|
"scripts/telegram_smoke/",
|
|
)
|
|
CHANGED_COVERAGE_MANIFEST = "tests/integration/changed-coverage-exemptions.toml"
|
|
SANDBOX_DOCKER_PREFIXES = ("docker/sandbox/",)
|
|
SANDBOX_DOCKER_EXACT_PATHS = {
|
|
"Dockerfile.sandbox-worker",
|
|
"crates/app/ironclaw_cli/src/runtime/mod.rs",
|
|
"crates/app/ironclaw_composition/src/sandbox.rs",
|
|
"crates/app/ironclaw_composition/src/builtin_capability_policy.rs",
|
|
"crates/app/ironclaw_composition/src/deployment.rs",
|
|
"crates/app/ironclaw_composition/src/factory/production_backend_assembly.rs",
|
|
"crates/app/ironclaw_composition/src/factory/runtime_lane_assembly.rs",
|
|
"crates/app/ironclaw_composition/src/input.rs",
|
|
"crates/app/ironclaw_config/src/profile.rs",
|
|
"crates/kernel/ironclaw_host_runtime/src/first_party_tools/mod.rs",
|
|
"crates/kernel/ironclaw_host_runtime/src/invocation_services.rs",
|
|
"crates/kernel/ironclaw_host_runtime/src/process_port.rs",
|
|
"crates/kernel/ironclaw_host_runtime/src/services.rs",
|
|
"crates/kernel/ironclaw_host_runtime/src/services/builder.rs",
|
|
"crates/kernel/ironclaw_runtime_policy/src/planner.rs",
|
|
"crates/kernel/ironclaw_runtime_policy/src/resolver.rs",
|
|
"crates/lanes/ironclaw_sandbox/tests/support/docker_gate.rs",
|
|
"crates/lanes/ironclaw_sandbox/tests/user_sandbox_docker_live.rs",
|
|
"tests/integration/reborn_sandbox_shell_turn.rs",
|
|
"tests/e2e_trace_runtime_policy_serde.rs",
|
|
"tests/fixtures/llm_traces/runtime_policy/hosted_dev_no_shell.json",
|
|
"tests/integration/support/builder.rs",
|
|
"tests/integration/support/capability_backend.rs",
|
|
"tests/integration/support/docker_gate.rs",
|
|
"tests/integration/support/harness/mod.rs",
|
|
"tests/integration/support/harness/options.rs",
|
|
"tests/integration/support/harness/profiles/sandbox_shell.rs",
|
|
}
|
|
# Asset trees that live outside every crate root but are compiled *into* a
|
|
# workspace crate through a relative `include_bytes!` / `include_str!` that
|
|
# escapes its own crate (the §11.2.7 reach-ins inventoried by
|
|
# `crates/app/ironclaw_architecture_tests/tests/reborn_cross_crate_include_scan.rs`).
|
|
# Cargo's package directories cannot see them, so the `crates/` arm below
|
|
# resolves no package and the planner used to fail closed on every PR that
|
|
# touched a first-party extension package.
|
|
#
|
|
# They must NOT be classified as ignored. A `wasm/*.wasm` under
|
|
# `crates/extensions/packages/` is a *shipped artifact* that
|
|
# `ironclaw_extension_support` embeds byte-for-byte; calling it prose would
|
|
# turn today's loud failure into a silent under-schedule of a change to
|
|
# production output. Route each tree to the crate that compiles it instead, so
|
|
# a change still schedules that crate's tests and everything downstream of it.
|
|
#
|
|
# `scripts/ci/test_reborn_pr_test_plan.py` pins both halves: that the mapping
|
|
# routes (not ignores), and that every prefix and owner below still exists.
|
|
BUNDLED_SKILLS_PREFIX = "skills/"
|
|
EMBEDDED_ASSET_OWNERS: tuple[tuple[str, str], ...] = (
|
|
# manifests, prompts, schemas and built `wasm/*.wasm` for the first-party
|
|
# extension packages -> `ironclaw_extension_support`
|
|
# (`src/packages/*.rs`). Packages that are themselves workspace crates
|
|
# (slack, telegram, mem0, memory-native) resolve to their own package
|
|
# first and never reach this table.
|
|
("crates/extensions/packages/", "ironclaw_extension_support"),
|
|
# the uploadable WASM fixture bundles, whose `manifest.toml` files are
|
|
# `include_str!`d by `ironclaw_extension_host`
|
|
# (`src/available_extension_import.rs`). The guest sources beside them are
|
|
# standalone cargo workspaces that no Reborn lane compiles; routing the
|
|
# whole tree to the embedding crate over-schedules those rather than
|
|
# letting a fixture change select nothing at all.
|
|
("test-tools/", "ironclaw_extension_host"),
|
|
# the bundled Reborn product skills. `ironclaw_extension_host`'s build
|
|
# script (`build.rs`, `embed_reborn_skills`) walks this repo-root tree,
|
|
# parses every `skills/<name>/SKILL.md`, and embeds each skill's complete
|
|
# file set byte-for-byte into the binary (`src/bundled_skills.rs`), with
|
|
# `cargo:rerun-if-changed` on every file. Unlike the two trees above,
|
|
# *every* file here is shipped product output — Markdown included, which
|
|
# is why `_is_shipped_asset_markdown` special-cases this prefix instead of
|
|
# keying on a `prompts/` segment. Unclassified until 2026-08-05, when the
|
|
# fail-closed arm rejected #7157 on `skills/delegation/SKILL.md`.
|
|
(BUNDLED_SKILLS_PREFIX, "ironclaw_extension_host"),
|
|
)
|
|
EMBEDDED_ASSET_PREFIXES: tuple[str, ...] = tuple(
|
|
prefix for prefix, _ in EMBEDDED_ASSET_OWNERS
|
|
)
|
|
# Everything the crate/asset arm owns: crate roots plus the asset trees above.
|
|
CRATE_OR_ASSET_PREFIXES = ("crates/",) + tuple(
|
|
prefix for prefix, _ in EMBEDDED_ASSET_OWNERS
|
|
)
|
|
|
|
|
|
def _is_shipped_asset_markdown(path: str) -> bool:
|
|
"""True for Markdown that is itself a shipped asset in a table-owned tree.
|
|
|
|
Two Markdown *asset* kinds exist, and everything else ending in `.md`
|
|
under the asset trees is documentation — `test-tools/README.md`, a package
|
|
`AGENTS.md` — and stays prose:
|
|
|
|
* **Prompts** in the package and fixture trees: the table owns "manifests,
|
|
prompts, schemas and built `wasm/*.wasm`", and of those kinds only a
|
|
prompt is Markdown (the other three are `.toml`, `.json` and `.wasm`).
|
|
Keyed on the directory segment so it holds at any depth
|
|
(`<pkg>/prompts/<tool>/<name>.md` today, deeper tomorrow).
|
|
* **Bundled skills**: under `skills/` every file is the shipped artifact —
|
|
`build.rs` parses each `SKILL.md` and embeds each skill's complete file
|
|
set — so all Markdown there is product output, keyed on the tree itself.
|
|
"""
|
|
if not path.startswith(EMBEDDED_ASSET_PREFIXES):
|
|
return False
|
|
if path.startswith(BUNDLED_SKILLS_PREFIX):
|
|
return True
|
|
return "prompts" in Path(path).parts[:-1]
|
|
INTEGRATION_SUPPORT_OWNERS = {
|
|
"tests/fixtures/extensions/acme-messenger/manifest.toml": (
|
|
"tests/integration/extension_runtime.rs"
|
|
),
|
|
"tests/support/hosted_mcp_registration_server.rs": (
|
|
"tests/integration/hosted_mcp_registration.rs"
|
|
),
|
|
}
|
|
INTEGRATION_SNAPSHOT_PREFIX_OWNERS = {
|
|
"tests/snapshots/golden_payload__": "tests/integration/golden_payload.rs",
|
|
}
|
|
# Production files whose changes alter the model-visible prompt or tool
|
|
# surface that `golden_payload` snapshot-pins — the surface digest, the
|
|
# instruction bundle, the communication-context renderer, and the shipped
|
|
# prompt assets of the crates the golden harness composes. A change here still
|
|
# classifies as a production-package change below (crate buckets, reverse
|
|
# dependents); this mapping ADDITIONALLY schedules the golden integration lane
|
|
# so a prompt-surface PR cannot land green and then bounce the merge queue on
|
|
# stale goldens (#7361's queue failure, 2026-08-07: `surface.rs` changed the
|
|
# surface digest, the PR lane never ran the golden bucket, the exhaustive
|
|
# queue gate caught it first).
|
|
#
|
|
# Curated, not derived — a new prompt-composition site must be added here by
|
|
# hand, and until it is, the merge queue remains the backstop exactly as it
|
|
# was for every path before this mapping existed. Self-tested by
|
|
# `test_reborn_pr_test_plan.py` (positive per entry + a negative control).
|
|
PROMPT_SURFACE_GOLDEN_OWNER = "tests/integration/golden_payload.rs"
|
|
PROMPT_SURFACE_PATHS = (
|
|
"crates/kernel/ironclaw_host_runtime/src/surface.rs",
|
|
"crates/contracts/ironclaw_loop_contracts/src/instruction_bundle.rs",
|
|
"crates/contracts/ironclaw_loop_contracts/src/runtime_context.rs",
|
|
)
|
|
PROMPT_SURFACE_PREFIXES = (
|
|
"crates/contracts/ironclaw_loop_contracts/prompts/",
|
|
"crates/contracts/ironclaw_host_api/prompts/",
|
|
"crates/loop/ironclaw_agent_loop/prompts/",
|
|
"crates/loop/ironclaw_loop_host/prompts/",
|
|
# The host-managed ports assemble the exact request the goldens pin:
|
|
# `prompt.rs` drives the InstructionBundleBuilder into the message list
|
|
# and `model.rs` shapes the model request around it.
|
|
"crates/kernel/ironclaw_turns/src/host_managed_ports/",
|
|
)
|
|
PR_STATIC_CONTROL_PATHS = {
|
|
"Cargo.toml",
|
|
"rust-toolchain",
|
|
"rust-toolchain.toml",
|
|
".cargo/config",
|
|
".cargo/config.toml",
|
|
"tests/integration/coverage-exemptions.toml",
|
|
"tests/integration/coverage-floor.toml",
|
|
# Release-smoke Python coverage is executed by Code Style's dedicated
|
|
# `Release smoke script tests` step. It drives the real
|
|
# `scripts/ci/smoke-release-binary.py`; no Tests (Reborn) Rust lane owns
|
|
# this unittest module.
|
|
"tests/test_smoke_release_binary.py",
|
|
# Repo-root `scripts/` is deliberately NOT prefix-classified — the
|
|
# `unmapped test or CI path` arm below exists to force a per-file decision.
|
|
# These are decided:
|
|
# * the panic baseline is enforced by Code Style
|
|
# (`check_no_panics.py --reborn-baseline`), which runs on every PR and
|
|
# owns the whole check; no Reborn lane reads it.
|
|
# * `reborn-e2e-rust.sh` is driven by the `Reborn E2E` workflow, which has
|
|
# its own scope detector (`Detect Reborn E2E scope`). This planner
|
|
# selects lanes for `Tests (Reborn)` only, and that workflow does not
|
|
# invoke the script.
|
|
# * `check-version-bumps.sh` is the WIT/package version-parity gate, run
|
|
# only by `platform-and-compat.yml` (`run: ./scripts/check-version-bumps.sh`,
|
|
# behind that workflow's own `has_direct_wasm_abi_risk` filter, which
|
|
# already names the script). No `Tests (Reborn)` lane invokes it.
|
|
# Decided 2026-08-04 (WS10): editing it previously raised
|
|
# `unmapped test or CI path`, failing `Detect Reborn test scope` and
|
|
# skipping every downstream Reborn lane.
|
|
# * `run-reborn-webui.sh` is a local developer launcher for the WebUI dev
|
|
# server. It is referenced by no workflow at all (a search over
|
|
# `.github/` finds nothing), so no lane can be selected for it.
|
|
# * `build-wasm-extensions.sh` is named in the same
|
|
# `has_direct_wasm_abi_risk` classifier, which both scopes and runs it,
|
|
# and additionally has a Code Style self-test
|
|
# (`scripts/ci/test-build-wasm-extensions.sh`). No Reborn Rust lane
|
|
# executes it.
|
|
# * `e2e-skill-self-creation.sh` drives the skill self-creation e2e
|
|
# against a live model, selected by `E2E_PROFILE`. Like
|
|
# `run-reborn-webui.sh` it is referenced by no workflow (a search over
|
|
# `.github/` finds nothing) and needs credentials no lane has, so no
|
|
# lane can be selected for it; it is run by hand per
|
|
# `docs/internal/skills/multi_tenant_enablement.md`.
|
|
"scripts/no_panics_reborn_baseline.txt",
|
|
"scripts/reborn-e2e-rust.sh",
|
|
"scripts/build-wasm-extensions.sh",
|
|
"scripts/check-version-bumps.sh",
|
|
"scripts/run-reborn-webui.sh",
|
|
"scripts/e2e-skill-self-creation.sh",
|
|
# `codebase-graph.sh` inspects agent-only graph metadata. It does not
|
|
# execute or select a Reborn product test surface. (Arrived with #7215.)
|
|
"scripts/codebase-graph.sh",
|
|
# Container build inputs. `platform-and-compat.yml` keys `has_docker_risk`
|
|
# off exactly this pair and owns the image build; Code Style additionally
|
|
# proves every `include_str!` target is inside each build context
|
|
# (`scripts/ci/check-include-str-paths.sh`, the #5603 outage class). A
|
|
# Reborn Rust lane never builds an image.
|
|
"Dockerfile",
|
|
".dockerignore",
|
|
# `.gitignore` is decided the same way, and belongs here rather than with
|
|
# the repo-root prose above precisely because something *does* read it:
|
|
# Code Style's `Reject tracked files that match .gitignore` guard
|
|
# (`git ls-files -ci --exclude-standard`) owns that check outright. It runs
|
|
# whenever `has_code` is true, and `has_code` names `.gitignore` itself, so
|
|
# it runs on every PR that edits one. No Reborn lane reads the file, so no
|
|
# lane here can exercise a change to it. It was unclassified until
|
|
# 2026-08-04 (#6965), so the fail-closed arm failed the whole
|
|
# `Tests (Reborn)` roll-up on any PR that added an ignore rule.
|
|
".gitignore",
|
|
# * `check_no_panics.py` is the enforcer of the baseline listed above and
|
|
# runs under Code Style on every PR; same owner, same reasoning.
|
|
"scripts/check_no_panics.py",
|
|
# * `dev_metrics.py` is a reporting tool; nothing gates on it.
|
|
"scripts/dev_metrics.py",
|
|
# * `check-type-duplicates.py` is a local dev analysis tool (type-dedup
|
|
# backlog reporting); no workflow or hook invokes it, so no lane can
|
|
# exercise a change to it. Surfaced 2026-08-05 (#7259) when the docs
|
|
# path sweep touched its docstring.
|
|
"scripts/check-type-duplicates.py",
|
|
# * Its self-test, `python3 scripts/test-check-type-duplicates.py`, runs
|
|
# under Code Style's "Static-check self-tests" step
|
|
# (`.github/workflows/code_style.yml`) — the same lane that runs
|
|
# `scripts/ci/test-check-guidance.py` and the other `scripts/ci/`
|
|
# self-tests covered by the `PR_STATIC_CONTROL_PREFIXES` rule below.
|
|
# Neither this file nor `scripts/check-type-duplicates.py` sits under
|
|
# `scripts/ci/`, so unlike its sibling neither tripped the workflow's
|
|
# `has_code` path filter on its own — a PR touching only this file had
|
|
# `fast-checks` (and thus the self-test step) skipped entirely (review
|
|
# on #7797). `has_code`'s scope list now names both scripts
|
|
# explicitly, pinned by the `has_code` filter's `in_scope` probes in
|
|
# `scripts/ci/ws12_workflow_contracts.py`. Static control here means
|
|
# "no Tests (Reborn) lane reads it", not "no workflow runs it": Code
|
|
# Style owns this self-test, not a Reborn Rust test lane, so it stays
|
|
# classified as static control.
|
|
"scripts/test-check-type-duplicates.py",
|
|
# * `render-architecture-video.sh` is a local one-command Remotion
|
|
# render for docs/internal/architecture-video; never invoked by a
|
|
# workflow (the `architecture-video` Claude skill that used to
|
|
# reference it was deleted as stale v1-architecture content — see
|
|
# `.claude/skills/`). No lane can exercise a change to it.
|
|
"scripts/render-architecture-video.sh",
|
|
# * `pre-commit-safety.sh` is a local git hook, not a CI lane.
|
|
"scripts/pre-commit-safety.sh",
|
|
# * `preflight-gates.sh` is the local pre-push gate gauntlet proposed by
|
|
# the 2026-08 gate audit (docs/internal/gate-audit-2026-08.md §4.3);
|
|
# referenced by no workflow, so no lane can be selected for it.
|
|
"scripts/preflight-gates.sh",
|
|
# * `check-boundaries.sh` was DELETED by the same audit (measured broken
|
|
# on a clean tree, run by nothing). The entry stays so the deletion
|
|
# diff — and any revert — classifies instead of tripping the
|
|
# fail-closed arm; the audit's own PR was the first to hit it.
|
|
"scripts/check-boundaries.sh",
|
|
# * `test-mutation-audit.sh` is the self-test for the mutation audit,
|
|
# driven by its own lane rather than by a crate/integration selection.
|
|
"scripts/test-mutation-audit.sh",
|
|
# * `mutation-audit.sh` is the audit those guardrails self-test, run by
|
|
# the same dedicated lane (`nightly-deep-ci.yml`'s mutation-frontier
|
|
# job) and by hand; no Reborn PR lane invokes it.
|
|
"scripts/mutation-audit.sh",
|
|
# * the rest of the repo-root metadata class, classified 2026-08-04 as a
|
|
# class rather than one file per red run. Every entry above this block
|
|
# was added the other way — a rename-shaped diff touches root files no
|
|
# feature PR normally touches, the planner fails closed on the first
|
|
# one, and the next only surfaces after that one is fixed (`Dockerfile`,
|
|
# then `clippy.toml`, then six more). The whole remaining class is
|
|
# enumerated here so the sequence stops.
|
|
#
|
|
# Membership rule: a file belongs here only if NO Reborn test lane
|
|
# reads it. Verified per file against `crates/**/*.rs` and `tests/**`
|
|
# before listing. `.dockerignore` and `.env.example` are absent from
|
|
# this block because they are decided elsewhere above — the former in
|
|
# the container-build pair, the latter in `IGNORED_ROOT_FILES`.
|
|
#
|
|
# Workspace lint/format/dependency/release policy, owned by the lanes
|
|
# that read them — `clippy.toml` by Code Style, `deny.toml` by
|
|
# cargo-deny, `release-plz.toml` by the release workflow:
|
|
"clippy.toml",
|
|
"deny.toml",
|
|
"release-plz.toml",
|
|
# VCS/repo metadata — read by git and by review tooling, never a lane:
|
|
".gitattributes",
|
|
".coderabbit.yaml",
|
|
".mcp.json",
|
|
# Toolchain pins and lint config for non-Rust lanes:
|
|
".node-version",
|
|
".nvmrc",
|
|
".sqlfluff",
|
|
# Container/deploy descriptors owned by their own workflows:
|
|
"Dockerfile.process-sandbox",
|
|
"docker-compose.yml",
|
|
"railway.toml",
|
|
"codecov.yml",
|
|
# Shipped artifacts with no test coverage of their own:
|
|
"ironclaw.bash",
|
|
"ironclaw.fish",
|
|
"ironclaw.zsh",
|
|
"ironclaw.png",
|
|
"LICENSE-APACHE",
|
|
"LICENSE-MIT",
|
|
# The Reborn container startup scripts are shell, not Rust: no Reborn test
|
|
# lane executes them. Code Style owns their fast self-tests, while
|
|
# `platform-and-compat.yml` classifies both as Docker risk and exercises
|
|
# them through the built runtime image. `docker/` stays per-file, never a
|
|
# prefix: it mixes classes, and the shipped runtime
|
|
# configs beside this script belong to a Rust lane instead — see
|
|
# `DOCKER_RUNTIME_CONFIG_OWNERS` below. `docker/process-sandbox-entrypoint.sh`
|
|
# has no owning lane and must keep refusing.)
|
|
"docker/reborn/entrypoint.sh",
|
|
"docker/reborn/start-sshd.sh",
|
|
}
|
|
# Shipped container configs a Reborn Rust test parses and asserts on, mapped to
|
|
# the test source that owns them. They are NOT static control: the membership
|
|
# rule for that set is "no Reborn test lane reads the file", and
|
|
# `ironclaw_cli`'s `smoke` test reads both of these — it parses each through
|
|
# `ironclaw_config::RebornConfigFile::parse_text` and pins the resulting
|
|
# profile, storage backend and policy (`docker_reborn_production_config_uses_postgres_storage`
|
|
# and its local-config sibling). Calling them prose would silently under-select
|
|
# the one lane that can catch a broken production config.
|
|
#
|
|
# Both were unclassified until 2026-08-11, when #7471's Postgres pool change
|
|
# edited `config.production.toml` and the fail-closed arm failed
|
|
# `Detect Reborn test scope`, cascading into the whole `Tests (Reborn)`
|
|
# roll-up. Classified as the pair they are, rather than one per red run —
|
|
# the same lesson the repo-root metadata block above records.
|
|
#
|
|
# The two `config.hosted-single-tenant*.toml` siblings were undecided until
|
|
# the docs/internal/reborn consolidation (2026-08-12) touched their reader,
|
|
# `tests/dockerfile_runtime_home.rs`, and hit this planner's fail-closed arm.
|
|
# That PR gave the reader a lane — `_root_test_partitions()` and
|
|
# `run-reborn-root-partition.sh` both inventory it alongside
|
|
# `support_unit_tests.rs` — so the configs now map to it: a root-test owner
|
|
# selects its root partition, a crate-test owner selects its exact crate
|
|
# target (both arms below).
|
|
DOCKER_RUNTIME_CONFIG_OWNERS = {
|
|
"docker/reborn/config.toml": "crates/app/ironclaw_cli/tests/smoke.rs",
|
|
"docker/reborn/config.production.toml": "crates/app/ironclaw_cli/tests/smoke.rs",
|
|
"docker/reborn/config.hosted-single-tenant.toml": "tests/dockerfile_runtime_home.rs",
|
|
"docker/reborn/config.hosted-single-tenant-volume.toml": "tests/dockerfile_runtime_home.rs",
|
|
}
|
|
# Repository configuration that a Reborn crate test reads as an asserted input.
|
|
# These paths are not static CI control: changing one must schedule the test that
|
|
# defines its product/security contract. The linked-device supply-chain test
|
|
# parses Dependabot's Cargo ignore rules so the exact grammers pin cannot be
|
|
# silently reopened by an automated dependency update.
|
|
REPO_CONFIG_TEST_OWNERS = {
|
|
".github/dependabot.yml": (
|
|
"crates/app/ironclaw_architecture_tests/tests/"
|
|
"reborn_linked_device_supply_chain_pin.rs"
|
|
),
|
|
}
|
|
# `.githooks/` is developer-local git hook plumbing: no Reborn lane executes a
|
|
# hook, while Code Style both triggers on the tree and lints its contents
|
|
# (`scripts/ci/test-ci-comm-locale-pin.sh` follows the symlinks and scans them).
|
|
PR_STATIC_CONTROL_PREFIXES = (".github/workflows/", "scripts/ci/", ".githooks/")
|
|
# cargo-dist's build-setup fragment. cargo-dist re-inlines it into
|
|
# `.github/workflows/ironclaw-release.yml` on every `dist generate`, so it is
|
|
# workflow source that happens to live outside `.github/workflows/` — the same
|
|
# static control as the file it is inlined into, and read by no Reborn lane.
|
|
# The planner fails closed on unclassified paths, so a `.github/` file with no
|
|
# rule takes the whole PR's plan step down rather than mis-scheduling it.
|
|
PR_STATIC_CONTROL_FRAGMENTS = (".github/dist-build-setup.yml",)
|
|
SHARED_REBORN_ACTION_PREFIXES = (
|
|
".github/actions/setup-sccache-dist/",
|
|
".github/actions/setup-rust/",
|
|
# The deliberate mapping the arm below asks for. `install-cargo-component`
|
|
# is consumed by `coverage.yml` and `platform-and-compat.yml`; a change to
|
|
# it can move what those lanes build, and no narrow lane exercises it
|
|
# safely, so it takes the exhaustive plan like its two siblings. Until this
|
|
# entry existed it hit the fail-closed arm, so a PR editing only that file
|
|
# took the whole plan step down instead of scheduling anything.
|
|
".github/actions/install-cargo-component/",
|
|
)
|
|
BUCKET_WEIGHTS = {
|
|
"reborn-core": 12,
|
|
"auth-security": 9,
|
|
"extension-operator": 8,
|
|
"product-workflow": 8,
|
|
"webui-ingress": 8,
|
|
"composition-core": 8,
|
|
"wasm-sandbox": 8,
|
|
"agent-runtime": 7,
|
|
"llm-mcp": 7,
|
|
"events-conversations": 7,
|
|
"host-runtime": 6,
|
|
"channel-adapters": 6,
|
|
"architecture-misc": 5,
|
|
"memory-skills": 5,
|
|
}
|
|
|
|
|
|
def _run(*argv: str) -> str:
|
|
return subprocess.run(
|
|
argv,
|
|
cwd=ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout.strip()
|
|
|
|
|
|
def _metadata() -> dict[str, Any]:
|
|
return json.loads(_run("cargo", "metadata", "--format-version", "1"))
|
|
|
|
|
|
def _lockfile_change_is_manifest_owned(
|
|
*,
|
|
current: dict[str, Any],
|
|
base: dict[str, Any],
|
|
changed_paths: set[str],
|
|
metadata: dict[str, Any],
|
|
) -> bool:
|
|
"""Return true only for lockfile edits confined to changed workspace manifests."""
|
|
if {key: value for key, value in current.items() if key != "package"} != {
|
|
key: value for key, value in base.items() if key != "package"
|
|
}:
|
|
return False
|
|
|
|
workspace_members = set(metadata["workspace_members"])
|
|
workspace_packages = {
|
|
package["name"]: str(
|
|
Path(package["manifest_path"]).resolve().relative_to(ROOT)
|
|
)
|
|
for package in metadata["packages"]
|
|
if package["id"] in workspace_members
|
|
}
|
|
changed_workspace_packages = {
|
|
name for name, manifest in workspace_packages.items() if manifest in changed_paths
|
|
}
|
|
if not changed_workspace_packages:
|
|
return False
|
|
|
|
def indexed(lockfile: dict[str, Any]) -> dict[tuple[str, str, str], dict[str, Any]]:
|
|
packages = lockfile.get("package", [])
|
|
if not isinstance(packages, list):
|
|
return {}
|
|
result = {
|
|
(
|
|
str(package.get("name", "")),
|
|
str(package.get("version", "")),
|
|
str(package.get("source", "")),
|
|
): package
|
|
for package in packages
|
|
if isinstance(package, dict)
|
|
}
|
|
return result if len(result) == len(packages) else {}
|
|
|
|
current_packages = indexed(current)
|
|
base_packages = indexed(base)
|
|
if not current_packages or current_packages.keys() != base_packages.keys():
|
|
return False
|
|
|
|
for key, current_package in current_packages.items():
|
|
base_package = base_packages[key]
|
|
if current_package == base_package:
|
|
continue
|
|
name, _version, source = key
|
|
if source or name not in changed_workspace_packages:
|
|
return False
|
|
current_without_dependencies = {
|
|
field: value
|
|
for field, value in current_package.items()
|
|
if field != "dependencies"
|
|
}
|
|
base_without_dependencies = {
|
|
field: value for field, value in base_package.items() if field != "dependencies"
|
|
}
|
|
if current_without_dependencies != base_without_dependencies:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _manifest_owned_lockfile_change(
|
|
*, base_sha: str, changed_paths: set[str], metadata: dict[str, Any]
|
|
) -> bool:
|
|
if not base_sha:
|
|
return False
|
|
return _lockfile_change_is_manifest_owned(
|
|
current=tomllib.loads((ROOT / "Cargo.lock").read_text(encoding="utf-8")),
|
|
base=tomllib.loads(_run("git", "show", f"{base_sha}:Cargo.lock")),
|
|
changed_paths=changed_paths,
|
|
metadata=metadata,
|
|
)
|
|
|
|
|
|
def _canonical_packages() -> list[str]:
|
|
return json.loads(_run("scripts/ci/discover-reborn-package-crates.sh"))
|
|
|
|
|
|
def _bucket_packages(packages: list[str]) -> list[dict[str, Any]]:
|
|
return json.loads(
|
|
_run("scripts/ci/reborn-crate-test-buckets.sh", json.dumps(packages))
|
|
)
|
|
|
|
|
|
def _bound_pr_buckets(
|
|
buckets: list[dict[str, Any]], max_buckets: int = MAX_PR_CRATE_BUCKETS
|
|
) -> list[dict[str, Any]]:
|
|
"""Pack canonical buckets into bounded PR jobs without splitting them."""
|
|
if len(buckets) <= max_buckets:
|
|
return buckets
|
|
|
|
bounded = [
|
|
{"name": f"affected-{index + 1}", "packages": []}
|
|
for index in range(min(max_buckets, len(buckets)))
|
|
]
|
|
weights = [0] * len(bounded)
|
|
ordered = sorted(
|
|
buckets,
|
|
key=lambda bucket: (
|
|
-BUCKET_WEIGHTS.get(
|
|
str(bucket.get("name")), len(bucket.get("packages", []))
|
|
),
|
|
str(bucket.get("name")),
|
|
),
|
|
)
|
|
for bucket in ordered:
|
|
target = min(range(len(bounded)), key=lambda index: (weights[index], index))
|
|
bounded[target]["packages"].extend(bucket["packages"])
|
|
weights[target] += BUCKET_WEIGHTS.get(
|
|
str(bucket.get("name")), len(bucket.get("packages", []))
|
|
)
|
|
return bounded
|
|
|
|
|
|
def _root_test_partitions() -> dict[str, int]:
|
|
extra_tests = [
|
|
name
|
|
for name in ("dockerfile_runtime_home", "support_unit_tests")
|
|
if (ROOT / f"tests/{name}.rs").is_file()
|
|
]
|
|
names = sorted(
|
|
[
|
|
path.stem
|
|
for path in (ROOT / "tests").glob("reborn_*.rs")
|
|
if path.is_file()
|
|
]
|
|
+ extra_tests
|
|
)
|
|
return {f"tests/{name}.rs": index % 4 for index, name in enumerate(names)}
|
|
|
|
|
|
def _integration_test_lanes() -> dict[str, str | int]:
|
|
return planner_test_lanes(ROOT)
|
|
|
|
|
|
def _workspace_packages(metadata: dict[str, Any]) -> tuple[dict[str, str], dict[str, set[str]]]:
|
|
members = set(metadata["workspace_members"])
|
|
packages_by_id = {
|
|
package["id"]: package
|
|
for package in metadata["packages"]
|
|
if package["id"] in members
|
|
}
|
|
directories = {
|
|
str(Path(package["manifest_path"]).resolve().parent.relative_to(ROOT)): package[
|
|
"name"
|
|
]
|
|
for package in packages_by_id.values()
|
|
if Path(package["manifest_path"]).resolve().parent != ROOT
|
|
}
|
|
reverse: dict[str, set[str]] = defaultdict(set)
|
|
for node in metadata["resolve"]["nodes"]:
|
|
if node["id"] not in packages_by_id:
|
|
continue
|
|
dependent = packages_by_id[node["id"]]["name"]
|
|
for dependency in node["deps"]:
|
|
if dependency["pkg"] in packages_by_id:
|
|
reverse[packages_by_id[dependency["pkg"]]["name"]].add(dependent)
|
|
return directories, reverse
|
|
|
|
|
|
def _affected_packages(changed: set[str], reverse: dict[str, set[str]]) -> set[str]:
|
|
affected = set(changed)
|
|
pending = list(changed)
|
|
while pending:
|
|
package = pending.pop()
|
|
for dependent in reverse.get(package, set()):
|
|
if dependent not in affected:
|
|
affected.add(dependent)
|
|
pending.append(dependent)
|
|
return affected
|
|
|
|
|
|
def _manifest_requires_cargo(package: dict[str, Any]) -> bool:
|
|
"""Whether a whole package needs Cargo's in-process test semantics."""
|
|
name = str(package.get("name", "<unknown>"))
|
|
manifest_path = Path(str(package.get("manifest_path", "")))
|
|
try:
|
|
manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
|
|
except (OSError, tomllib.TOMLDecodeError) as error:
|
|
raise ValueError(
|
|
f"cannot classify test runner for {name} at {manifest_path}: {error}"
|
|
) from error
|
|
|
|
manifest_targets: list[dict[str, Any]] = []
|
|
for table in ("lib", "bin", "test", "bench", "example"):
|
|
value = manifest.get(table, [])
|
|
if isinstance(value, dict):
|
|
manifest_targets.append(value)
|
|
elif isinstance(value, list):
|
|
manifest_targets.extend(
|
|
target for target in value if isinstance(target, dict)
|
|
)
|
|
if any(target.get("harness") is False for target in manifest_targets):
|
|
return True
|
|
|
|
for target in package.get("targets", []):
|
|
if not target.get("test", False):
|
|
continue
|
|
kinds = set(target.get("kind", []))
|
|
if not kinds or not kinds <= NEXTEST_LIBTEST_TARGET_KINDS:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _cargo_required_packages(
|
|
metadata: dict[str, Any], packages: set[str]
|
|
) -> set[str]:
|
|
by_name = {str(package["name"]): package for package in metadata["packages"]}
|
|
missing = sorted(packages - by_name.keys())
|
|
if missing:
|
|
raise ValueError(
|
|
"selected packages are missing from Cargo metadata: " + ", ".join(missing)
|
|
)
|
|
return {
|
|
package
|
|
for package in packages
|
|
if _manifest_requires_cargo(by_name[package])
|
|
}
|
|
|
|
|
|
def _annotate_cargo_packages(
|
|
buckets: list[dict[str, Any]], metadata: dict[str, Any]
|
|
) -> list[dict[str, Any]]:
|
|
"""Add the Cargo-only subset after every bucket's package list is final."""
|
|
full_package_names = {
|
|
str(package)
|
|
for bucket in buckets
|
|
if not bucket.get("exact_targets")
|
|
for package in bucket.get("packages", [])
|
|
}
|
|
cargo_required = _cargo_required_packages(metadata, full_package_names)
|
|
annotated: list[dict[str, Any]] = []
|
|
for bucket in buckets:
|
|
candidate = dict(bucket)
|
|
if not candidate.get("exact_targets"):
|
|
cargo_packages = sorted(set(candidate["packages"]) & cargo_required)
|
|
if cargo_packages:
|
|
candidate["cargo_packages"] = cargo_packages
|
|
annotated.append(candidate)
|
|
return annotated
|
|
|
|
|
|
def _full_plan(
|
|
reason: str,
|
|
canonical_packages: list[str],
|
|
metadata: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"mode": "full",
|
|
"reasons": [reason],
|
|
"changed_packages": [],
|
|
"affected_packages": canonical_packages,
|
|
"crate_buckets": _annotate_cargo_packages(
|
|
_bucket_packages(canonical_packages), metadata
|
|
),
|
|
"root_partitions": list(ALL_ROOT_PARTITIONS),
|
|
"integration_lanes": list(ALL_INTEGRATION_LANES),
|
|
"run_qa_replay": True,
|
|
"run_sandbox_docker": True,
|
|
"coverage_mode": "full",
|
|
}
|
|
|
|
|
|
def _merge_group_global_risk(paths: set[str]) -> str | None:
|
|
"""Return the first path whose queue impact cannot be narrowed safely."""
|
|
for path in sorted(paths):
|
|
if (
|
|
path in {"Cargo.lock", ".config/nextest.toml"}
|
|
or path in PR_STATIC_CONTROL_PATHS
|
|
or path in PR_STATIC_CONTROL_FRAGMENTS
|
|
or path.startswith(PR_STATIC_CONTROL_PREFIXES)
|
|
or path.startswith(SHARED_REBORN_ACTION_PREFIXES)
|
|
):
|
|
return path
|
|
return None
|
|
|
|
|
|
def _unclassified_path_plan(
|
|
*,
|
|
event: str,
|
|
reason: str,
|
|
canonical_packages: list[str],
|
|
metadata: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
"""Fail PR feedback loudly; widen an otherwise valid queue diff."""
|
|
if event == "merge_group":
|
|
return _full_plan(
|
|
f"merge-group scope could not classify {reason}; running the "
|
|
"exhaustive plan",
|
|
canonical_packages,
|
|
metadata,
|
|
)
|
|
raise ValueError(reason)
|
|
|
|
|
|
def build_plan(
|
|
*,
|
|
event: str,
|
|
changed_paths: list[str],
|
|
metadata: dict[str, Any],
|
|
canonical_packages: list[str],
|
|
lockfile_manifest_owned: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Build a deterministic test plan, rejecting or widening unknown inputs."""
|
|
if event in FULL_EVENTS:
|
|
return _full_plan(
|
|
f"{event} requires exhaustive coverage", canonical_packages, metadata
|
|
)
|
|
if event not in DIFF_EVENTS:
|
|
return _full_plan(f"unknown event {event!r}", canonical_packages, metadata)
|
|
|
|
paths = {path.strip().replace("\\", "/") for path in changed_paths if path.strip()}
|
|
if not paths:
|
|
raise ValueError(
|
|
f"empty {event.replace('_', '-')} diff cannot be classified; "
|
|
"refusing to launch "
|
|
"an unbounded test matrix"
|
|
)
|
|
|
|
if event == "merge_group":
|
|
global_risk = _merge_group_global_risk(paths)
|
|
if global_risk is not None:
|
|
return _full_plan(
|
|
f"merge-group global or topology input changed: {global_risk}; "
|
|
"running the exhaustive plan",
|
|
canonical_packages,
|
|
metadata,
|
|
)
|
|
|
|
package_directories, reverse = _workspace_packages(metadata)
|
|
if event == "merge_group":
|
|
changed_manifest = next(
|
|
(
|
|
f"{directory}/Cargo.toml"
|
|
for directory in sorted(package_directories)
|
|
if f"{directory}/Cargo.toml" in paths
|
|
),
|
|
None,
|
|
)
|
|
if changed_manifest is not None:
|
|
return _full_plan(
|
|
"merge-group workspace topology input changed: "
|
|
f"{changed_manifest}; running the exhaustive plan",
|
|
canonical_packages,
|
|
metadata,
|
|
)
|
|
production_packages: set[str] = set()
|
|
direct_test_packages: set[str] = set()
|
|
exact_test_targets: dict[str, set[tuple[str, str]]] = defaultdict(set)
|
|
packages_requiring_all_targets: set[str] = set()
|
|
root_partitions: set[int] = set()
|
|
integration_lanes: set[str | int] = set()
|
|
# Recorded replay is a repository-wide ordering and integration sentinel,
|
|
# not affected-area coverage. Keep it on for every pull request even when
|
|
# no changed path maps to another Reborn lane.
|
|
run_qa_replay = True
|
|
run_sandbox_docker = False
|
|
qa_evidence_changed = False
|
|
nextest_config_changed = False
|
|
shared_reborn_action_changed = False
|
|
reasons: list[str] = []
|
|
root_inventory = _root_test_partitions()
|
|
integration_inventory = _integration_test_lanes()
|
|
group_integration_prefixes = {
|
|
f"{Path(owner).parent.as_posix()}/"
|
|
for owner, lane in integration_inventory.items()
|
|
if lane == "groups"
|
|
}
|
|
sandbox_crate_prefixes = _sandbox_docker_prefixes()
|
|
sandbox_docker_prefixes = SANDBOX_DOCKER_PREFIXES + sandbox_crate_prefixes
|
|
sandbox_docker_exact_paths = set(SANDBOX_DOCKER_EXACT_PATHS)
|
|
if sandbox_crate_prefixes:
|
|
sandbox_crate_directory = _sandbox_crate_directory()
|
|
sandbox_docker_exact_paths.update(
|
|
{
|
|
f"{sandbox_crate_directory}/Cargo.toml",
|
|
f"{sandbox_crate_directory}/src/lib.rs",
|
|
f"{sandbox_crate_directory}/tests/support/user_sandbox_live.rs",
|
|
f"{sandbox_crate_directory}/tests/user_sandbox_docker_live.rs",
|
|
}
|
|
)
|
|
|
|
for path in sorted(paths):
|
|
path_parts = PurePosixPath(path).parts
|
|
if path == "Cargo.lock":
|
|
if lockfile_manifest_owned:
|
|
reasons.append(
|
|
"Cargo.lock change is owned by a changed crate manifest"
|
|
)
|
|
else:
|
|
reasons.append(
|
|
"workspace lockfile breadth is deferred to the exhaustive merge-queue gate"
|
|
)
|
|
continue
|
|
if path == ".config/nextest.toml":
|
|
# Test-runner config: every `Tests (Reborn)` lane executes cargo
|
|
# nextest with these profiles, so a change to it cannot be
|
|
# exercised by any narrow lane. It is deliberately NOT static
|
|
# control — the membership rule for that set is "no Reborn test
|
|
# lane reads the file", and these lanes read it. Widening to the
|
|
# exhaustive plan is the safe resolution (a superset can never
|
|
# under-select). Unclassified until 2026-08-10, when deleting the
|
|
# dead `live_tests::zizmor_scan*` overrides failed the whole
|
|
# `Tests (Reborn)` roll-up on the provider-matrix retirement PR.
|
|
nextest_config_changed = True
|
|
continue
|
|
if path.startswith(SHARED_REBORN_ACTION_PREFIXES):
|
|
# Every `Tests (Reborn)` job installs the compiler cache
|
|
# (setup-sccache-dist) or the Rust toolchain itself
|
|
# (setup-rust) through one of these local actions. No narrow
|
|
# lane can exercise a change to either safely, so use the
|
|
# exhaustive plan just as we do for shared nextest
|
|
# configuration. Keep other `.github/actions/**` paths
|
|
# fail-closed until their consumers are mapped deliberately.
|
|
shared_reborn_action_changed = True
|
|
continue
|
|
if path in REPO_CONFIG_TEST_OWNERS:
|
|
owner = REPO_CONFIG_TEST_OWNERS[path]
|
|
package = next(
|
|
(
|
|
name
|
|
for directory, name in package_directories.items()
|
|
if owner.startswith(f"{directory}/")
|
|
),
|
|
None,
|
|
)
|
|
if package is None:
|
|
raise ValueError(
|
|
f"repository config owner is in no workspace package: {owner}"
|
|
)
|
|
direct_test_packages.add(package)
|
|
exact_test_targets[package].add(("test", Path(owner).stem))
|
|
reasons.append(f"repository config parsed by {owner}: {path}")
|
|
continue
|
|
if path in PR_STATIC_CONTROL_FRAGMENTS or path in PR_STATIC_CONTROL_PATHS or path.startswith(
|
|
PR_STATIC_CONTROL_PREFIXES
|
|
):
|
|
reasons.append(f"static CI or workspace-policy checks own: {path}")
|
|
continue
|
|
if path in DOCKER_RUNTIME_CONFIG_OWNERS:
|
|
owner = DOCKER_RUNTIME_CONFIG_OWNERS[path]
|
|
if owner in root_inventory:
|
|
root_partitions.add(root_inventory[owner])
|
|
reasons.append(f"shipped container config parsed by {owner}: {path}")
|
|
continue
|
|
package = next(
|
|
(
|
|
name
|
|
for directory, name in package_directories.items()
|
|
if owner.startswith(f"{directory}/")
|
|
),
|
|
None,
|
|
)
|
|
if package is None:
|
|
raise ValueError(
|
|
f"container config owner is in no workspace package: {owner}"
|
|
)
|
|
direct_test_packages.add(package)
|
|
exact_test_targets[package].add(("test", Path(owner).stem))
|
|
reasons.append(f"shipped container config parsed by {owner}: {path}")
|
|
continue
|
|
if path.startswith(DEDICATED_WORKFLOW_PREFIXES):
|
|
reasons.append(f"dedicated workflow owns: {path}")
|
|
continue
|
|
if path.startswith(WORKSPACE_EXCLUDED_PREFIXES):
|
|
reasons.append(f"workspace-excluded project, built by no lane: {path}")
|
|
continue
|
|
if path.startswith(DEDICATED_E2E_PREFIX):
|
|
reasons.append(f"dedicated Reborn E2E workflow owns: {path}")
|
|
continue
|
|
if path.startswith(QA_HARNESS_PREFIXES):
|
|
qa_evidence_changed = True
|
|
reasons.append(f"live QA harness changed: {path}")
|
|
continue
|
|
if path == CHANGED_COVERAGE_MANIFEST:
|
|
reasons.append("changed-coverage policy is statically validated")
|
|
continue
|
|
doc_fact = _doc_fact_selections(path)
|
|
if doc_fact:
|
|
for package, target in doc_fact:
|
|
direct_test_packages.add(package)
|
|
exact_test_targets[package].add(("test", target))
|
|
reasons.append(f"doc-fact contract tests read: {path}")
|
|
continue
|
|
if (
|
|
path in IGNORED_GUIDANCE_PATHS
|
|
or path.startswith(IGNORED_PREFIXES)
|
|
or path in IGNORED_ROOT_FILES
|
|
or (path.endswith(".md") and "/" not in path)
|
|
):
|
|
continue
|
|
if path in sandbox_docker_exact_paths or path.startswith(
|
|
sandbox_docker_prefixes
|
|
):
|
|
run_sandbox_docker = True
|
|
reasons.append(f"sandbox Docker surface changed: {path}")
|
|
if (
|
|
not path.startswith("crates/")
|
|
and path not in root_inventory
|
|
and path not in integration_inventory
|
|
):
|
|
continue
|
|
if path.startswith(_webui_frontend_prefix()):
|
|
reasons.append("Code Style owns WebUI lint, tests, and production build")
|
|
continue
|
|
if path in root_inventory:
|
|
root_partitions.add(root_inventory[path])
|
|
reasons.append(f"root test changed: {path}")
|
|
continue
|
|
if (
|
|
path.startswith("tests/support/reborn_parity_qa/")
|
|
or path == "tests/support_unit_tests.rs"
|
|
):
|
|
if event == "merge_group":
|
|
root_partitions.update(ALL_ROOT_PARTITIONS)
|
|
reasons.append(
|
|
"shared root-test support changed; merge group runs every root partition"
|
|
)
|
|
else:
|
|
root_partitions.add(0)
|
|
reasons.append(
|
|
"shared root-test support changed; PR runs a representative partition"
|
|
)
|
|
continue
|
|
if path.startswith("tests/support/") and path not in INTEGRATION_SUPPORT_OWNERS:
|
|
# Direct shared root-test support (tests/support/mod.rs and the
|
|
# modules it declares). The integration group targets also compile
|
|
# this tree via `#[path = "../../support/mod.rs"]`, so schedule a
|
|
# representative lane of each tier.
|
|
if event == "merge_group":
|
|
root_partitions.update(ALL_ROOT_PARTITIONS)
|
|
integration_lanes.update(ALL_INTEGRATION_LANES)
|
|
reasons.append(
|
|
"shared root-test support changed; merge group runs every "
|
|
"root and integration consumer lane"
|
|
)
|
|
else:
|
|
root_partitions.add(0)
|
|
integration_lanes.add(0)
|
|
reasons.append(
|
|
"shared root-test support changed; PR runs a representative "
|
|
"partition and integration lane"
|
|
)
|
|
continue
|
|
if path in integration_inventory:
|
|
integration_lanes.add(integration_inventory[path])
|
|
reasons.append(f"integration test changed: {path}")
|
|
continue
|
|
if any(path.startswith(prefix) for prefix in group_integration_prefixes):
|
|
integration_lanes.add("groups")
|
|
reasons.append(f"integration group scenario changed: {path}")
|
|
continue
|
|
if (
|
|
len(path_parts) >= 4
|
|
and path_parts[:2] == ("tests", "integration")
|
|
and path_parts[2].startswith("group_")
|
|
):
|
|
integration_lanes.add("groups")
|
|
reasons.append(f"unregistered integration group path changed: {path}")
|
|
continue
|
|
if path in INTEGRATION_SUPPORT_OWNERS:
|
|
owner = INTEGRATION_SUPPORT_OWNERS[path]
|
|
integration_lanes.add(integration_inventory[owner])
|
|
reasons.append(f"integration test support changed: {path}")
|
|
continue
|
|
snapshot_owner = next(
|
|
(
|
|
owner
|
|
for prefix, owner in INTEGRATION_SNAPSHOT_PREFIX_OWNERS.items()
|
|
if path.startswith(prefix)
|
|
),
|
|
None,
|
|
)
|
|
if snapshot_owner is not None:
|
|
integration_lanes.add(integration_inventory[snapshot_owner])
|
|
reasons.append(f"integration test snapshot changed: {path}")
|
|
continue
|
|
if path.startswith("tests/integration/support/"):
|
|
if event == "merge_group":
|
|
# Root parity/QA binaries and every flat/group integration
|
|
# target compile this support tree, so the queue must run all
|
|
# of those consumers together.
|
|
root_partitions.update(ALL_ROOT_PARTITIONS)
|
|
integration_lanes.update(ALL_INTEGRATION_LANES)
|
|
reasons.append(
|
|
"shared integration support changed; merge group runs every "
|
|
"root and integration consumer lane"
|
|
)
|
|
else:
|
|
integration_lanes.add(0)
|
|
reasons.append(
|
|
"shared integration support changed; PR runs a representative lane"
|
|
)
|
|
continue
|
|
if path.startswith("tests/integration/"):
|
|
integration_lanes.add(0)
|
|
reasons.append(
|
|
"shared integration support changed; PR runs a representative lane"
|
|
)
|
|
continue
|
|
if path.startswith("tests/fixtures/llm_traces/reborn_qa/") or path in {
|
|
"scripts/ci/check-reborn-qa-fixtures.sh",
|
|
"scripts/ci/test-check-reborn-qa-fixtures.sh",
|
|
"scripts/ci/test-check-regression-promotions.py",
|
|
}:
|
|
qa_evidence_changed = True
|
|
reasons.append("recorded QA evidence changed")
|
|
continue
|
|
if path.startswith("tests/fixtures/") and not path.startswith(
|
|
"tests/fixtures/llm_traces/"
|
|
):
|
|
# Document/binary fixtures (docx, xlsx, pptx, pdf) are consumed by
|
|
# integration tests through `include_bytes!`, so changing one
|
|
# changes what those tests assert. Recorded LLM traces under
|
|
# `reborn_qa` are handled by the QA-evidence arm above;
|
|
# other trace families require an explicit owner rather than
|
|
# silently becoming generic integration fixtures.
|
|
integration_lanes.add(0)
|
|
reasons.append(f"integration fixture changed: {path}")
|
|
continue
|
|
if path.startswith(("tests/reborn_", "tests/e2e/reborn_", "scripts/ci/reborn-")):
|
|
return _unclassified_path_plan(
|
|
event=event,
|
|
reason=f"unmapped Reborn test path: {path}",
|
|
canonical_packages=canonical_packages,
|
|
metadata=metadata,
|
|
)
|
|
if path.startswith("tests/e2e/"):
|
|
# E2E scenarios and support live in the dedicated
|
|
# `reborn-e2e.yml` workflow, which runs its own changed-path
|
|
# filter and provider/shard matrix. They are not part of the
|
|
# crate-bucket / root-partition / integration-lane plan emitted
|
|
# here, so skip them instead of failing closed on an unmapped
|
|
# path.
|
|
reasons.append(f"Reborn E2E workflow owns: {path}")
|
|
continue
|
|
if path in PROMPT_SURFACE_PATHS or path.startswith(PROMPT_SURFACE_PREFIXES):
|
|
# Deliberately no `continue`: the path still classifies as a
|
|
# production-package change below. This arm only ADDS the golden
|
|
# lane so the prompt-surface snapshots run on the PR instead of
|
|
# first failing in the merge queue.
|
|
integration_lanes.add(
|
|
integration_inventory[PROMPT_SURFACE_GOLDEN_OWNER]
|
|
)
|
|
reasons.append(f"model-visible prompt surface changed: {path}")
|
|
if path.startswith(CRATE_OR_ASSET_PREFIXES):
|
|
# Family-level prose that belongs to no package: `crates/AGENTS.md`,
|
|
# `crates/README.md`, `crates/Architecture.md`, and the family
|
|
# `AGENTS.md` files the target-architecture tree move adds. Matched
|
|
# by "Markdown that no package directory owns", so a genuinely
|
|
# unmapped *crate* path still reaches the arms checked below.
|
|
# Depth-independent by construction, so it keeps holding for
|
|
# `crates/AGENTS.md` and for a future `crates/<family>/AGENTS.md`
|
|
# after the WS7 family move. A crate-resident doc still resolves to
|
|
# its package below and keeps selecting that package's lane.
|
|
#
|
|
# The carve-out yields to shipped Markdown *assets*, and must.
|
|
# Prompts: the asset table declares it owns "manifests, prompts,
|
|
# schemas and built `wasm/*.wasm`"; of those kinds only a prompt is
|
|
# Markdown (manifests are `.toml`, schemas `.json`, wasm `.wasm`),
|
|
# so `.md` under a `prompts/` directory is an asset. Bundled
|
|
# skills: under `skills/` every file is embedded product output,
|
|
# `SKILL.md` first among them, so all its Markdown is an asset.
|
|
# Every other `.md` is prose. Without this clause the prose arm
|
|
# fired first and a change to a shipped prompt — production output
|
|
# that `ironclaw_extension_support` compiles in — planned
|
|
# `mode=none`, selecting no lane at all, while its sibling
|
|
# `manifest.toml` in the same package correctly selected two. That
|
|
# is exactly the "silent under-schedule of a change to production
|
|
# output" the comment above `EMBEDDED_ASSET_OWNERS` forbids.
|
|
#
|
|
# Keyed on the `prompts/` segment (or the `skills/` tree) rather
|
|
# than on the asset prefixes themselves, because the package and
|
|
# fixture prefixes also cover genuine prose: `test-tools/README.md`
|
|
# is documentation of the fixture bundles and stays prose, as its
|
|
# own test pins.
|
|
if Path(path).suffix == ".md" and not _is_shipped_asset_markdown(path):
|
|
if not any(
|
|
path.startswith(f"{directory}/")
|
|
for directory in package_directories
|
|
):
|
|
reasons.append(f"crate-tree guidance changed: {path}")
|
|
continue
|
|
package = next(
|
|
(
|
|
name
|
|
for directory, name in package_directories.items()
|
|
if path == directory or path.startswith(f"{directory}/")
|
|
),
|
|
None,
|
|
)
|
|
if package is None:
|
|
# A shipped asset tree that no package *directory* owns but a
|
|
# package *compiles*: `crates/extensions/packages/*/wasm/*.wasm`
|
|
# is `include_bytes!`d by `ironclaw_extension_support` and
|
|
# `test-tools/*/manifest.toml` is `include_str!`d by
|
|
# `ironclaw_extension_host`. This arm is checked before the
|
|
# widening below so the change schedules the crate that
|
|
# compiles it, with that crate named in the reason — calling
|
|
# either tree prose would turn a change to production output
|
|
# into a silent under-schedule.
|
|
owner = next(
|
|
(
|
|
owner
|
|
for prefix, owner in EMBEDDED_ASSET_OWNERS
|
|
if path.startswith(prefix)
|
|
),
|
|
None,
|
|
)
|
|
if owner is not None:
|
|
production_packages.add(owner)
|
|
reasons.append(f"asset compiled into {owner} changed: {path}")
|
|
continue
|
|
# The path is under `crates/` but belongs to no workspace
|
|
# package. The normal cause is a crate this PR **deletes or
|
|
# renames** — `git diff` reports its old paths, and the merge
|
|
# or rename is exactly the change shape the target-architecture
|
|
# restructure ships (PROPOSAL §2 plans six crate deletions).
|
|
#
|
|
# Refusing to plan is the wrong resolution: it blocks the PR
|
|
# outright. Widening is the safe one — the exhaustive plan is a
|
|
# superset of any narrowing, so an unattributable path can never
|
|
# cause under-selection. Malformed input is still rejected: a
|
|
# path outside every classified prefix raises below.
|
|
return _full_plan(
|
|
"a crate path maps to no workspace package (deletion or "
|
|
f"rename): {path}; this PR runs the exhaustive plan",
|
|
canonical_packages,
|
|
metadata,
|
|
)
|
|
directory = next(
|
|
directory
|
|
for directory, name in package_directories.items()
|
|
if name == package
|
|
)
|
|
relative = path.removeprefix(f"{directory}/")
|
|
if relative.startswith(("tests/", "benches/", "examples/")):
|
|
direct_test_packages.add(package)
|
|
reasons.append(f"package-owned test surface changed: {package}")
|
|
parts = Path(relative).parts
|
|
target_kinds = {
|
|
"tests": "test",
|
|
"benches": "bench",
|
|
"examples": "example",
|
|
}
|
|
if len(parts) == 2 and Path(parts[1]).suffix == ".rs":
|
|
exact_test_targets[package].add(
|
|
(target_kinds[parts[0]], Path(parts[1]).stem)
|
|
)
|
|
else:
|
|
packages_requiring_all_targets.add(package)
|
|
else:
|
|
production_packages.add(package)
|
|
reasons.append(f"production package changed: {package}")
|
|
continue
|
|
if path.startswith(("tests/reborn_", "tests/e2e/reborn_", "scripts/ci/reborn-")):
|
|
return _unclassified_path_plan(
|
|
event=event,
|
|
reason=f"unmapped Reborn test path: {path}",
|
|
canonical_packages=canonical_packages,
|
|
metadata=metadata,
|
|
)
|
|
if path.startswith("tests/e2e/"):
|
|
# The browser/E2E suite has its own workflow (`reborn-e2e.yml`,
|
|
# `paths: tests/e2e/**`) with its own scope detection, so this
|
|
# planner must not also schedule Rust lanes for it. The
|
|
# `tests/e2e/reborn_*` harnesses above stay a deliberate hard
|
|
# error — they are shared fixtures, not one scenario.
|
|
reasons.append(f"dedicated Reborn E2E workflow owns: {path}")
|
|
continue
|
|
if path.startswith(("scripts/", "tests/", ".github/actions/")):
|
|
return _unclassified_path_plan(
|
|
event=event,
|
|
reason=f"unmapped test or CI path: {path}",
|
|
canonical_packages=canonical_packages,
|
|
metadata=metadata,
|
|
)
|
|
return _unclassified_path_plan(
|
|
event=event,
|
|
reason=f"unclassified pull-request path: {path}",
|
|
canonical_packages=canonical_packages,
|
|
metadata=metadata,
|
|
)
|
|
|
|
if nextest_config_changed:
|
|
return _full_plan(
|
|
"nextest runner config changed; this PR runs the exhaustive plan",
|
|
canonical_packages,
|
|
metadata,
|
|
)
|
|
if shared_reborn_action_changed:
|
|
return _full_plan(
|
|
"shared reborn action changed; this PR runs the exhaustive plan",
|
|
canonical_packages,
|
|
metadata,
|
|
)
|
|
|
|
canonical_set = set(canonical_packages)
|
|
changed_packages = production_packages | direct_test_packages
|
|
affected = (
|
|
_affected_packages(production_packages, reverse) | direct_test_packages
|
|
) & canonical_set
|
|
if changed_packages and not affected:
|
|
raise ValueError(
|
|
"changed packages are outside the canonical Reborn package set: "
|
|
f"{', '.join(sorted(changed_packages))}"
|
|
)
|
|
|
|
buckets = _bucket_packages(sorted(affected)) if affected else []
|
|
full_target_packages = (
|
|
_affected_packages(production_packages, reverse)
|
|
| packages_requiring_all_targets
|
|
) & canonical_set
|
|
for bucket in buckets:
|
|
bucket_packages = set(bucket["packages"])
|
|
if bucket_packages & full_target_packages:
|
|
continue
|
|
if not all(package in exact_test_targets for package in bucket_packages):
|
|
continue
|
|
bucket["exact_targets"] = [
|
|
{"package": package, "kind": kind, "name": name}
|
|
for package in sorted(bucket_packages)
|
|
for kind, name in sorted(exact_test_targets[package])
|
|
]
|
|
# PR feedback is latency-bounded; merge-group coverage stays exhaustive
|
|
# and preserves every canonical bucket boundary.
|
|
if event == "pull_request" and len(buckets) > MAX_PR_CRATE_BUCKETS:
|
|
original_bucket_count = len(buckets)
|
|
buckets = _bound_pr_buckets(buckets)
|
|
reasons.append(
|
|
f"coalesced {original_bucket_count} affected crate buckets into "
|
|
f"{len(buckets)} PR jobs without omitting packages"
|
|
)
|
|
buckets = _annotate_cargo_packages(buckets, metadata)
|
|
active = bool(
|
|
buckets
|
|
or root_partitions
|
|
or integration_lanes
|
|
or qa_evidence_changed
|
|
or run_sandbox_docker
|
|
)
|
|
return {
|
|
"mode": "selected" if active else "none",
|
|
"reasons": reasons or ["no Reborn test surface changed"],
|
|
"changed_packages": sorted(changed_packages),
|
|
"affected_packages": sorted(affected),
|
|
"crate_buckets": buckets,
|
|
"root_partitions": sorted(root_partitions),
|
|
"integration_lanes": sorted(
|
|
integration_lanes, key=lambda value: (isinstance(value, str), str(value))
|
|
),
|
|
"run_qa_replay": run_qa_replay,
|
|
"run_sandbox_docker": run_sandbox_docker,
|
|
"coverage_mode": "none",
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--event", required=True)
|
|
parser.add_argument(
|
|
"--changed-files",
|
|
type=Path,
|
|
help="newline-delimited changed paths; required for diff events",
|
|
)
|
|
parser.add_argument(
|
|
"--canonical-packages",
|
|
type=Path,
|
|
help="JSON package array produced by discover-reborn-package-crates.sh",
|
|
)
|
|
parser.add_argument(
|
|
"--base-sha",
|
|
help="pull-request base commit used to verify manifest-owned lockfile edits",
|
|
)
|
|
args = parser.parse_args()
|
|
try:
|
|
changed_paths = (
|
|
args.changed_files.read_text(encoding="utf-8").splitlines()
|
|
if args.changed_files
|
|
else []
|
|
)
|
|
canonical_packages = (
|
|
json.loads(args.canonical_packages.read_text(encoding="utf-8"))
|
|
if args.canonical_packages
|
|
else _canonical_packages()
|
|
)
|
|
normalized_paths = {
|
|
path.strip().replace("\\", "/") for path in changed_paths if path.strip()
|
|
}
|
|
metadata = _metadata()
|
|
lockfile_manifest_owned = (
|
|
_manifest_owned_lockfile_change(
|
|
base_sha=args.base_sha or "",
|
|
changed_paths=normalized_paths,
|
|
metadata=metadata,
|
|
)
|
|
if "Cargo.lock" in normalized_paths
|
|
else False
|
|
)
|
|
plan = build_plan(
|
|
event=args.event,
|
|
changed_paths=changed_paths,
|
|
metadata=metadata,
|
|
canonical_packages=canonical_packages,
|
|
lockfile_manifest_owned=lockfile_manifest_owned,
|
|
)
|
|
except (
|
|
OSError,
|
|
KeyError,
|
|
ValueError,
|
|
RuntimeError,
|
|
subprocess.CalledProcessError,
|
|
) as error:
|
|
print(f"Reborn PR test planner failed: {error}", file=sys.stderr)
|
|
return 1
|
|
print(json.dumps(plan, separators=(",", ":"), sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|