Files
FreeCADWasmDev/tools/prepare_implementation_business_batch.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

172 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Select the next implementation-first business batch across phase boundaries."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
from implementation_first import implementation_ready_ids, user_validation_deferred
from record_runtime_issue import ACTIVE_STATUSES
from failure_reconciliation import write as write_json
ROOT = Path(__file__).resolve().parents[1]
CAPABILITIES = ROOT / "docs/current/feature-capability-registry.json"
ISSUES = ROOT / "docs/current/runtime-issue-ledger.json"
BATCH = ROOT / "docs/current/module-batch-manifest.json"
POLICY = "delivery-workflow-first-v5"
def coverage_development_due(history: list[dict]) -> bool:
"""Reserve case authoring after three model rounds without new delivery."""
attempts = 0
for row in reversed(history):
if row.get("outcome") == "provider_wait":
continue
if row.get("businessLane") == "development" or row.get("firstBusinessPassIds"):
return False
# Local evidence refresh and historical reconciliation do not reset
# delivery starvation, but neither spends a model opportunity here.
if row.get("tier") not in {"feature-workflow", "business-acceptance"}:
continue
if row.get("outcome") in {"no_progress", "source_edits_pending_outcome"} or row.get("fileChanges"):
attempts += 1
if attempts >= 3:
return True
return False
def recovery_budget_spent(history: list[dict]) -> bool:
failed_turns = 0
for row in reversed(history):
if row.get("outcome") == "provider_wait":
continue
if row.get("businessLane") == "development" or row.get("tier") != "business-acceptance":
return False
if row.get("outcome") == "progress":
if not row.get("fileChanges"):
continue # A local evidence refresh must not reset failed model recovery work.
return False
if row.get("outcome") in {"no_progress", "source_edits_pending_outcome"}:
failed_turns += 1
if failed_turns >= 2:
return True
return False
def load(path: Path) -> dict:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
return value if isinstance(value, dict) else {}
def feature_phase_remaining() -> int:
rows = [row for row in load(CAPABILITIES).get("records", []) if isinstance(row, dict) and row.get("id")]
ready = implementation_ready_ids()
active_issues = {
str(row.get("capabilityId"))
for row in load(ISSUES).get("issues", [])
if isinstance(row, dict) and str(row.get("status", "open")) in ACTIVE_STATUSES
}
return len(({str(row["id"]) for row in rows} - ready) | active_issues)
def phase_order(previous: dict, remaining: int) -> list[tuple[str, list[str]]]:
if user_validation_deferred():
return [
("P0-reported-functional-repair", ["tools/prepare_feature_capability_batch.py", "--runtime-only"]),
("P1-connect-delivery-workflows", ["tools/prepare_feature_capability_batch.py", "--delivery-only"]),
("P2-connect-all-features", ["tools/prepare_feature_capability_batch.py"]),
("P3-connect-product-workflows", ["tools/prepare_product_business_batch.py"]),
("P4-connect-format-workflows", ["tools/prepare_format_business_batch.py"]),
]
history = load(ROOT / "docs/current/scheduler-progress.json").get("history", [])
last_tier = previous.get("selected", {}).get("tier")
if last_tier == "failure-reconciliation":
last_tier = next((row.get("tier") for row in reversed(load(ROOT / "docs/current/scheduler-progress.json").get("history", []))
if row.get("tier") and row["tier"] != "failure-reconciliation"), None)
implementation = [("P2-delivery-implementation", ["tools/prepare_feature_capability_batch.py", "--delivery-only"])]
acceptance = [("P2-delivery-acceptance", ["tools/prepare_business_acceptance_batch.py", "--delivery-only"])]
development = []
if previous.get("businessLane") != "development" and (recovery_budget_spent(history) or coverage_development_due(history)):
command = ["tools/prepare_business_acceptance_batch.py", "--uncovered-only"]
module = previous.get("selected", {}).get("module")
if module:
command.extend(["--exclude-module", module])
development = [("P1-uncovered-business-development", command)]
return [
*development,
("P0-observed-runtime-repair", ["tools/prepare_feature_capability_batch.py", "--runtime-only"]),
("P0-executable-business-acceptance", ["tools/prepare_business_acceptance_batch.py", "--covered-only"]),
*(implementation + acceptance if last_tier == "business-acceptance" else acceptance + implementation),
("P3-react-product-integration", ["tools/prepare_product_business_batch.py"]),
("P4-module-import-export-workflows", ["tools/prepare_format_business_batch.py"]),
*([("P5-remaining-feature-implementation", ["tools/prepare_feature_capability_batch.py"])] if remaining else []),
("P6-remaining-business-acceptance", ["tools/prepare_business_acceptance_batch.py"]),
("P5-failure-reconciliation", ["tools/prepare_failure_reconciliation_batch.py"]),
]
def acceptance_preempts(batch: dict) -> bool:
if user_validation_deferred():
return False
if batch.get("selected", {}).get("tier") != "feature-workflow":
return False
from prepare_business_acceptance_batch import candidates, inventory
state = inventory()
if candidates(state, covered_only=True):
return True
history = load(ROOT / "docs/current/scheduler-progress.json").get("history", [])
if coverage_development_due(history) and candidates(state, uncovered_only=True):
return True
if candidates(state, delivery_only=True) and batch.get("selectionScope") != "delivery":
return True
if batch.get("schedulingPolicy") != POLICY:
return True
history = [row for row in load(ROOT / "docs/current/scheduler-progress.json").get("history", [])
if row.get("tier") and row["tier"] != "failure-reconciliation" and row.get("outcome") != "provider_wait"]
# Bound a preserved implementation turn even when its selected IDs change.
return bool(len(history) >= 2 and all(row["tier"] == "feature-workflow" for row in history[-2:])
and candidates(state))
def main() -> int:
previous = load(BATCH)
remaining = feature_phase_remaining()
waits = []
for phase, command in phase_order(previous, remaining):
result = subprocess.run([sys.executable, *command], cwd=ROOT, text=True, capture_output=True, check=False)
if result.stdout:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, end="", file=sys.stderr)
if result.returncode:
return result.returncode
batch = load(BATCH)
if batch.get("status") == "ready":
batch["schedulingPolicy"] = POLICY
write_json(BATCH, batch)
# Runtime issues have their own priority phase. Historical review
# must not replace an executable business batch at module handoff.
print(json.dumps({"implementationPhase": phase, "featureRemaining": remaining}, ensure_ascii=False))
return 0
if batch.get("status") == "waiting_for_retry":
waits.append(batch)
elif batch.get("status") != "no_ready_batch":
print(json.dumps({"status": "error", "reason": "planner returned an unknown batch state", "phase": phase}))
return 2
if waits:
batch = min(waits, key=lambda value: float(value["retryAt"]))
batch["deferredIds"] = sorted({record_id for value in waits for record_id in value.get("deferredIds", [])})
batch["deferredRecords"] = len(batch["deferredIds"])
BATCH.write_text(json.dumps(batch, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())