241 lines
13 KiB
Python
241 lines
13 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 continuous_coverage_due(history: list[dict]) -> bool:
|
|
refreshes = 0
|
|
for row in reversed(history):
|
|
if row.get("outcome") == "provider_wait":
|
|
continue
|
|
if row.get("businessLane") == "development" or row.get("firstBusinessPassIds"):
|
|
return False
|
|
if row.get("tier") == "business-acceptance":
|
|
refreshes += 1
|
|
if refreshes >= 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]]]:
|
|
policy = load(ROOT / "docs/current/execution-policy.json")
|
|
if policy.get("continuousDevelopment") is True and policy.get("completionFocus") == "business-acceptance":
|
|
history = load(ROOT / "docs/current/scheduler-progress.json").get("history", [])
|
|
development = ("P3-uncovered-business-development", ["tools/prepare_business_acceptance_batch.py", "--uncovered-only"])
|
|
refresh = ("P2-current-executable-acceptance", ["tools/prepare_business_acceptance_batch.py", "--covered-only"])
|
|
if policy.get("prioritizeCoverageGaps") is True:
|
|
return [
|
|
("P0-uncovered-business-development", development[1]),
|
|
("P1-first-acceptance-of-completed-gaps", refresh[1]),
|
|
("P2-remaining-implementation", ["tools/prepare_feature_capability_batch.py"]),
|
|
("P3-product-integration", ["tools/prepare_product_business_batch.py"]),
|
|
("P3-format-integration", ["tools/prepare_format_business_batch.py"]),
|
|
]
|
|
return [
|
|
("P0-authorized-priority-acceptance", ["tools/prepare_business_acceptance_batch.py", "--priority-only"]),
|
|
("P1-current-business-repair", ["tools/prepare_feature_capability_batch.py", "--runtime-only"]),
|
|
*([development, refresh] if continuous_coverage_due(history) else [refresh, development]),
|
|
("P4-remaining-implementation", ["tools/prepare_feature_capability_batch.py"]),
|
|
("P4-product-integration", ["tools/prepare_product_business_batch.py"]),
|
|
("P4-format-integration", ["tools/prepare_format_business_batch.py"]),
|
|
("P5-failure-reconciliation", ["tools/prepare_failure_reconciliation_batch.py"]),
|
|
]
|
|
if user_validation_deferred():
|
|
return [
|
|
("P1-primary-workspace-missing-integration", ["tools/prepare_product_business_batch.py"]),
|
|
("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"]),
|
|
("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
|
|
policy = load(ROOT / "docs/current/execution-policy.json")
|
|
if policy.get("continuousDevelopment") is True and policy.get("completionFocus") == "business-acceptance":
|
|
return batch.get("selected", {}).get("family") != "runtime-issue"
|
|
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:
|
|
policy = load(ROOT / "docs/current/execution-policy.json")
|
|
if policy.get("workScope") == "gap-repair-only":
|
|
# The migration is already delivered. Old inventories and missing test
|
|
# receipts must not manufacture another full-port development queue.
|
|
repairs = [row for row in policy.get("confirmedGapRepairs", []) if isinstance(row, dict)
|
|
and row.get("status") == "open" and all(row.get(key) for key in ("id", "module", "source", "problem"))]
|
|
if not repairs:
|
|
write_json(BATCH, {"schema": "freecad-web-module-function-batch/v1", "status": "no_ready_batch",
|
|
"schedulingPolicy": "confirmed-gap-repair-only", "selectionScope": "confirmed-gaps",
|
|
"records": [], "queue": [],
|
|
"reason": "No confirmed gap queued. Reuse completed migration; do not restart historical backlog or verification."})
|
|
print(json.dumps({"status": "no_ready_batch", "reason": "confirmed gaps only; completed migration preserved"}))
|
|
return 0
|
|
row = repairs[0]
|
|
import hashlib
|
|
product = row["module"] == "Web" and row["id"].startswith("product:")
|
|
tier = "product-integration" if product else "feature-workflow"
|
|
family = "gap-repair-" + str(row.get("key") or row["id"].replace(":", "-"))
|
|
strategy = "module-business-first-deferred-validation" if product else "feature-first-deferred-validation"
|
|
parts = [row["module"], tier, family, row["id"]] if product else [row["module"], family, row["id"]]
|
|
identity = hashlib.sha256("|".join(parts).encode()).hexdigest()[:24]
|
|
write_json(BATCH, {"schema": "freecad-web-module-function-batch/v1", "status": "ready",
|
|
"batchId": identity, "strategy": strategy, "schedulingPolicy": "confirmed-gap-repair-only",
|
|
"minimumRecords": 1, "singletonFallback": True,
|
|
"selectionScope": "confirmed-gaps", "taskId": load(ROOT / "docs/current/task-state.json").get("active_task"),
|
|
"selected": {"module": row["module"], "tier": tier, "family": family, "recordCount": 1},
|
|
"records": [[row["id"], row.get("command", row["id"]), "implementation_required", row["source"], row.get("line", 1)]],
|
|
"confirmedGap": row, "businessWorkflow": "repair-existing-function"})
|
|
print(json.dumps({"status": "ready", "selectedGap": row["id"]}))
|
|
return 0
|
|
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())
|