## Summary
- restore release-preparation context on `main` pushes only when typed
validation scope reports an exact `CHANGELOG.md` change
- make primary cold launch converge on the shared control-center
presenter and retain fronting until the SwiftUI window is registered
- add control-plane, lifecycle, and race-regression coverage for both
release blockers
## Validation
- `python3 script/tests/test_validation_tooling.py` (53 passed)
- `python3 script/development_domains.py validate`
- `./script/validate_build_scripts_safety.sh --static-only`
- `./script/validate_app_lifecycle_contract.sh` (12/12 passed)
- `./script/validate_swift_tests.sh --scope all` (745 passed)
- `./script/validate_pre_push.sh`
- `./script/changelog_fragments.py policy --base-ref origin/main
--release-preparation true`
## Release note
This amends the still-unpublished v0.4.3 release preparation. The exact
merged `main` commit will receive the mandatory host UI acceptance
before release dispatch.
## Development claim / 开发声明
- Domain: `release-control-plane`
- Claim: `control-plane`
- Base commit: `7c57bbdb16b24074df16dd64ec51d3fc28782a7f`
- Release preparation: `yes`
- Focused validation:
- `python3 script/tests/test_validation_tooling.py`
- `./script/validate_build_scripts_safety.sh --static-only`
<!-- apc-engineering-coordination-v1
{"claim_overlap_rejections":0,"merge_tree_conflicts":0,"schema_version":"apc.engineering-coordination.v1","task_created_at":"2026-08-12T16:56:32Z","train_conflict_resolutions":0}
-->
---------
Co-authored-by: gd-ops <gd-ops@xmfunny.com>
281 lines
8.9 KiB
Python
Executable File
281 lines
8.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Classify changed paths for local, CI, and UI-validation routing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import dataclasses
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
OVERLAY_SWIFT_PREFIXES = (
|
|
"apps/macos/Sources/AgentPetCompanion/Overlay/",
|
|
)
|
|
OVERLAY_SWIFT_FILES = {
|
|
"apps/macos/Sources/AgentPetCompanionCore/FrameScheduler.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/AppStoreOverlaySnapshotTests.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/FrameTimelineTests.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/OverlayDisplayWidthTests.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/OverlayGeometryTests.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/OverlayInteractionTelemetryTests.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/OverlayPlacementAuthorityTests.swift",
|
|
"apps/macos/Tests/AgentPetCompanionTests/PetFramePipelineTests.swift",
|
|
}
|
|
LOCALIZATION_PREFIX = (
|
|
"apps/macos/Sources/AgentPetCompanion/Resources/Localization/",
|
|
)
|
|
BUNDLE_SCRIPT_PATHS = {
|
|
"script/build_and_run.sh",
|
|
"script/build_app_bundle.sh",
|
|
"script/prepare_interaction_attestation.sh",
|
|
"script/validate_app_bundle.sh",
|
|
"script/validate_interaction_attestation.py",
|
|
"script/validate_macos_build_contract.py",
|
|
"script/validate_overlay_interaction.sh",
|
|
"script/validate_overlay_offline.sh",
|
|
"script/validation_helpers.sh",
|
|
}
|
|
|
|
|
|
def has_prefix(path: str, prefixes: tuple[str, ...]) -> bool:
|
|
return any(path == prefix.rstrip("/") or path.startswith(prefix) for prefix in prefixes)
|
|
|
|
|
|
def is_localization(path: str) -> bool:
|
|
return has_prefix(path, LOCALIZATION_PREFIX) or path.endswith(
|
|
"LocalizationKeys.swift"
|
|
)
|
|
|
|
|
|
def is_overlay_swift(path: str) -> bool:
|
|
return path in OVERLAY_SWIFT_FILES or has_prefix(path, OVERLAY_SWIFT_PREFIXES)
|
|
|
|
|
|
def recommends_computer_use(path: str) -> bool:
|
|
if not path.endswith(".swift"):
|
|
return False
|
|
if "/Tests/" in path:
|
|
return False
|
|
return has_prefix(
|
|
path,
|
|
(
|
|
"apps/macos/Sources/AgentPetCompanion/Overlay/",
|
|
"apps/macos/Sources/AgentPetCompanion/Views/",
|
|
"apps/macos/Sources/AgentPetCompanion/App/",
|
|
),
|
|
)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class ValidationScope:
|
|
changed_count: int
|
|
docs_only: bool
|
|
changelog: bool
|
|
localization: bool
|
|
scripts: bool
|
|
producer: bool
|
|
connectors: bool
|
|
schemas: bool
|
|
plugin_version: bool
|
|
rust_mode: str
|
|
swift_mode: str
|
|
bundle: bool
|
|
computer_use: str
|
|
|
|
def as_dict(self) -> dict[str, bool | int | str]:
|
|
return dataclasses.asdict(self)
|
|
|
|
|
|
def classify(paths: list[str]) -> ValidationScope:
|
|
normalized = sorted({path.strip("/") for path in paths if path.strip("/")})
|
|
path_set = set(normalized)
|
|
|
|
root_rust = bool(path_set & {"Cargo.toml", "Cargo.lock", "rust-toolchain.toml"})
|
|
petcore_types = any(path.startswith("crates/petcore-types/") for path in normalized)
|
|
petcore = any(path.startswith("crates/petcore/") for path in normalized)
|
|
petcore_cli = any(path.startswith("crates/petcore-cli/") for path in normalized)
|
|
schemas = any(path.startswith(("schemas/", "fixtures/")) for path in normalized)
|
|
|
|
if root_rust or petcore_types or schemas:
|
|
rust_mode = "workspace"
|
|
elif petcore:
|
|
rust_mode = "petcore"
|
|
elif petcore_cli:
|
|
rust_mode = "cli"
|
|
else:
|
|
rust_mode = "none"
|
|
|
|
swift_inputs = [
|
|
path
|
|
for path in normalized
|
|
if (path.endswith(".swift") and not is_localization(path))
|
|
or path in {"apps/macos/Package.swift", "apps/macos/Package.resolved"}
|
|
]
|
|
if not swift_inputs:
|
|
swift_mode = "none"
|
|
elif all(path.endswith(".swift") and is_overlay_swift(path) for path in swift_inputs):
|
|
swift_mode = "overlay"
|
|
else:
|
|
swift_mode = "full"
|
|
|
|
localization = any(is_localization(path) for path in normalized)
|
|
changelog = "CHANGELOG.md" in path_set
|
|
scripts = any(path.startswith(("script/", ".github/")) for path in normalized)
|
|
producer = any(
|
|
path.startswith(("skills/agent-pet-maker/", "skills/agent-pet-studio/"))
|
|
or path.startswith(("schemas/petpack", "fixtures/petpack"))
|
|
or path.startswith(
|
|
"apps/macos/Sources/AgentPetCompanion/Resources/BuiltInPets/"
|
|
)
|
|
for path in normalized
|
|
)
|
|
connectors = any(
|
|
path.startswith("plugins/")
|
|
or path == "docs/integrations/agent-connectors.md"
|
|
for path in normalized
|
|
)
|
|
connector_code = any(path.startswith("plugins/") for path in normalized)
|
|
plugin_version = any(
|
|
path.startswith(
|
|
("plugins/codex/", "skills/agent-pet-maker/", "skills/agent-pet-studio/")
|
|
)
|
|
for path in normalized
|
|
)
|
|
|
|
non_localization_app_resource = any(
|
|
path.startswith("apps/macos/Sources/AgentPetCompanion/Resources/")
|
|
and not is_localization(path)
|
|
for path in normalized
|
|
)
|
|
bundle_script = bool(path_set & BUNDLE_SCRIPT_PATHS)
|
|
bundle = any(
|
|
(
|
|
rust_mode != "none",
|
|
swift_mode != "none",
|
|
schemas,
|
|
producer,
|
|
connector_code,
|
|
non_localization_app_resource,
|
|
bundle_script,
|
|
)
|
|
)
|
|
|
|
documentation_paths = (
|
|
"README.md",
|
|
"README.zh-CN.md",
|
|
"CHANGELOG.md",
|
|
"AGENTS.md",
|
|
)
|
|
docs_only = bool(normalized) and all(
|
|
path.startswith("docs/")
|
|
or path.startswith("changes/")
|
|
or path in documentation_paths
|
|
for path in normalized
|
|
) and not connectors
|
|
|
|
computer_use = (
|
|
"recommended"
|
|
if any(recommends_computer_use(path) for path in normalized)
|
|
else "not_required"
|
|
)
|
|
|
|
return ValidationScope(
|
|
changed_count=len(normalized),
|
|
docs_only=docs_only,
|
|
changelog=changelog,
|
|
localization=localization,
|
|
scripts=scripts,
|
|
producer=producer,
|
|
connectors=connectors,
|
|
schemas=schemas,
|
|
plugin_version=plugin_version,
|
|
rust_mode=rust_mode,
|
|
swift_mode=swift_mode,
|
|
bundle=bundle,
|
|
computer_use=computer_use,
|
|
)
|
|
|
|
|
|
def paths_from_git(root: pathlib.Path, base_ref: str) -> list[str]:
|
|
base = base_ref
|
|
if base == "0" * 40:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--verify", "HEAD^"],
|
|
cwd=root,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
base = result.stdout.strip() if result.returncode == 0 else "HEAD"
|
|
result = subprocess.run(
|
|
["git", "diff", "--name-only", "-z", base, "HEAD", "--"],
|
|
cwd=root,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return [os.fsdecode(item) for item in result.stdout.split(b"\0") if item]
|
|
|
|
|
|
def paths_from_file(path: pathlib.Path) -> list[str]:
|
|
return [os.fsdecode(item) for item in path.read_bytes().split(b"\0") if item]
|
|
|
|
|
|
def emit(scope: ValidationScope, output_format: str) -> None:
|
|
values = scope.as_dict()
|
|
if output_format == "json":
|
|
print(json.dumps(values, sort_keys=True, separators=(",", ":")))
|
|
return
|
|
|
|
names = {
|
|
"changed_count": "APC_CHANGED_COUNT",
|
|
"docs_only": "APC_CHANGED_DOCS_ONLY",
|
|
"changelog": "APC_CHANGED_CHANGELOG",
|
|
"localization": "APC_CHANGED_LOCALIZATION",
|
|
"scripts": "APC_CHANGED_SCRIPTS",
|
|
"producer": "APC_CHANGED_PRODUCER",
|
|
"connectors": "APC_CHANGED_CONNECTORS",
|
|
"schemas": "APC_CHANGED_SCHEMAS",
|
|
"plugin_version": "APC_CHANGED_PLUGIN_VERSION",
|
|
"rust_mode": "APC_RUST_MODE",
|
|
"swift_mode": "APC_SWIFT_MODE",
|
|
"bundle": "APC_BUILD_BUNDLE",
|
|
"computer_use": "APC_COMPUTER_USE",
|
|
}
|
|
for key, value in values.items():
|
|
rendered = "1" if value is True else "0" if value is False else str(value)
|
|
if output_format == "shell":
|
|
print(f"{names[key]}={shlex.quote(rendered)}")
|
|
else:
|
|
print(f"{key}={rendered}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path.cwd())
|
|
source = parser.add_mutually_exclusive_group(required=True)
|
|
source.add_argument("--base-ref")
|
|
source.add_argument("--paths-file", type=pathlib.Path)
|
|
parser.add_argument("--format", choices=("github", "json", "shell"), default="json")
|
|
arguments = parser.parse_args()
|
|
root = arguments.root.resolve()
|
|
try:
|
|
paths = (
|
|
paths_from_git(root, arguments.base_ref)
|
|
if arguments.base_ref is not None
|
|
else paths_from_file(arguments.paths_file)
|
|
)
|
|
emit(classify(paths), arguments.format)
|
|
except (OSError, subprocess.CalledProcessError, ValueError) as error:
|
|
print(f"could not classify validation scope: {error}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|