172 lines
8.1 KiB
Python
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())
|