Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
Monthly issue metrics / issue metrics (push) Has been cancelled
348 lines
23 KiB
Python
348 lines
23 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, actionable_confirmed_gap
|
|
from record_runtime_issue import ACTIVE_STATUSES
|
|
from failure_reconciliation import write as write_json
|
|
from scheduler_progress import cooling_ids, retry_details
|
|
from policy_io import read_policy
|
|
|
|
|
|
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:
|
|
if path.name == "execution-policy.json":
|
|
return read_policy(path)
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def gap_reuse_context(row: dict, repairs: list[dict]) -> dict:
|
|
"""Pass existing implementation notes, not another acceptance audit."""
|
|
shared_hosts = {"web/src/project-shell.tsx", "web/src/workbench-shell.tsx"}
|
|
anchors = set(row.get("sourceAnchors", [])) - shared_hosts
|
|
completed = [candidate for candidate in repairs
|
|
if candidate.get("key") != row.get("key")
|
|
and candidate.get("status") in {"implemented_unvalidated", "implemented_pending_validation", "resolved"}
|
|
and (candidate.get("module") == row.get("module")
|
|
or anchors.intersection(candidate.get("sourceAnchors", [])))]
|
|
# Keyed entries are append-oriented. Related components take priority over
|
|
# general module history; at most three summaries enter the next prompt.
|
|
completed = sorted(reversed(completed),
|
|
key=lambda candidate: bool(anchors.intersection(candidate.get("sourceAnchors", []))),
|
|
reverse=True)[:3]
|
|
return {
|
|
"instruction": "Reuse these existing implementation notes and named artifacts; this is not a request to recheck completed work. Fix only the selected source-confirmed gap. Do not duplicate its document owner, controls, geometry implementation or build pipeline. On completion update this exact key INSIDE docs/current/execution-policy.json confirmedGapRepairs with status=implemented_unvalidated, record, result, implementedAt and remainingFunctionalLimitations. A separate implementation note alone does not dequeue the task. If your note already records completion but this keyed entry is still open, copy the existing implementation bookkeeping into the keyed entry without repeating builds or verification.",
|
|
"relatedCompleted": [{"key": candidate.get("key"), "status": candidate.get("status"),
|
|
"record": candidate.get("record"), "result": str(candidate.get("result", ""))[:900],
|
|
"sourceAnchors": candidate.get("sourceAnchors", [])[:8]}
|
|
for candidate in completed],
|
|
"continuationRecord": row.get("record"),
|
|
"sourceAccess": "The user already authorizes bounded source reads of actual native document owners and dependencies needed by this functional package. The source reader accepts exact CoreProbe owner/helper files and direct relative frontend imports from named packet anchors for confirmed packages. Missing packet metadata is not a need for permission. Follow already located include/import/call paths, retain the file/line checkpoint, and continue implementation without asking to add those existing owners to an allowlist. Keep source-window budgets and avoid broad rescans; when the budget is exhausted, save the next exact file/line for a fresh bounded turn instead of repeating completed reads.",
|
|
"publishedBuildTree": "Resolve each target's existing artifact source in web/tools/sync-assets.mjs before building. A target may exist in several configured trees; use the one actually published. AssemblyProbe shares the common configure tree; AssemblyKernelProbe retains assembly-configure. Group targets only when they share that published tree. Do not compile an alternate tree and then sync stale artifacts from another.",
|
|
"buildRecordScope": "Necessary builds are authorized by the implementation task, not by a pipeline flag. packet.build is a default hint, NOT an exhaustive allowlist. Ignore another batch/target's pipeline report; never wait for packet authorization. Trace changed sources/headers/resources to their actual document-owner targets and build only those targets in the existing trees under tools/web_build_lock.py, -j8, grouping targets in the same tree. CMake/Ninja owns dependency regeneration. Reuse completed unchanged builds; dependencyDirty=false alone does not cover changed headers. Explicitly cd to web before Vite; a shell cwd error is not missing dependencies.",
|
|
"policyWrite": "Write receipt fields to a separate JSON patch file, then run python3 tools/policy_io.py --key <existing-key> --patch-file <receipt-patch.json>. This helper locks, reads fresh policy, changes only that key and atomically publishes with a durable backup. Never open execution-policy.json in write mode, loop json.dump over live policy, or overwrite the entire policy from a receipt. Missing or malformed policy never enables validation.",
|
|
"frontendBuild": {
|
|
"command": "python3 /home/mes123456/FreeCADWasmDev/working/FreeCAD/tools/build_web_frontend.py",
|
|
"reason": "Use the existing Vite configuration and normal web/dist output directly. npm run build invokes prebuild geometry audit and tsc --noEmit, which are not authorized in the current no-validation phase. Do not run npm run build, its prebuild hook, or replace the config with a temporary build. Keep the repository audit scripts unchanged for a future explicitly authorized validation phase.",
|
|
"reuse": "If a later completed configured frontend build already includes this task's last source edit, reuse that delivery record instead of rebuilding just to refresh a task note. Do not run evidence verification.",
|
|
},
|
|
}
|
|
|
|
|
|
def related_gap_package(ready: list[dict], policy: dict) -> list[dict]:
|
|
"""Group concrete work sharing an editor/owner, retaining partial packages."""
|
|
config = policy.get("functionalPackages", {})
|
|
if not config.get("enabled"):
|
|
return ready[:1]
|
|
limit = max(1, min(8, int(config.get("maxMembers", 6))))
|
|
previous = load(BATCH)
|
|
prior_keys = {row.get("key") for row in previous.get("confirmedGaps", [])}
|
|
carried = [row for row in ready if row.get("key") in prior_keys]
|
|
lead = carried[0] if carried else ready[0]
|
|
# UI packages keep command families on one mounted host/build ownership.
|
|
# Broad topic membership alone must never merge unrelated runtime owners.
|
|
ui = policy.get("desktopUiParity", {})
|
|
if ui.get("enabled"):
|
|
order = ui.get("packageOrder", [])
|
|
ready = sorted(ready, key=lambda row: order.index(row.get("desktopUiPackage"))
|
|
if row.get("desktopUiPackage") in order else len(order))
|
|
requested = [row for row in ready if row.get("desktopUiPackage") == ui.get("nextPackage")]
|
|
lead = requested[0] if requested else carried[0] if carried else ready[0]
|
|
generic = {"web/src/project-shell.tsx", "web/src/workbench-shell.tsx",
|
|
"web/src/desktop-command-dispatcher.ts", "web/src/module-router.ts",
|
|
"web/src/module-registry.generated.ts"}
|
|
|
|
def anchors(row):
|
|
return {path for path in [row.get("source"), *row.get("sourceAnchors", [])]
|
|
if isinstance(path, str) and path.startswith("web/src/") and path not in generic}
|
|
|
|
group = lead.get("functionalPackage")
|
|
shared = anchors(lead)
|
|
selected = [lead]
|
|
for row in [*carried, *ready]:
|
|
if row in selected or len(selected) >= limit:
|
|
continue
|
|
same_group = bool(group) and row.get("functionalPackage") == group and row.get("module") == lead.get("module")
|
|
same_owner = row.get("module") == lead.get("module") and bool(shared & anchors(row))
|
|
ui_owner = (ui.get("enabled") and lead.get("desktopUiPackage")
|
|
and row.get("desktopUiPackage") == lead.get("desktopUiPackage")
|
|
and bool(lead.get("uiBuildGroup")) and row.get("uiBuildGroup") == lead.get("uiBuildGroup"))
|
|
if same_group or same_owner or ui_owner:
|
|
selected.append(row)
|
|
return selected
|
|
|
|
|
|
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 actionable_confirmed_gap(row)]
|
|
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
|
|
deferred = cooling_ids(candidates={row["id"] for row in repairs})
|
|
ready = [row for row in repairs if row["id"] not in deferred]
|
|
if not ready:
|
|
details = retry_details({row["id"] for row in repairs})
|
|
write_json(BATCH, {"schema": "freecad-web-module-function-batch/v1", "status": "waiting_for_retry",
|
|
"schedulingPolicy": "confirmed-gap-repair-only", "records": [], "queue": [], **details})
|
|
print(json.dumps({"status": "waiting_for_retry", **details}))
|
|
return 0
|
|
members = related_gap_package(ready, policy)
|
|
row = dict(members[0])
|
|
row["reuseContext"] = gap_reuse_context(row, policy.get("confirmedGapRepairs", []))
|
|
if policy.get("desktopUiParity", {}).get("enabled"):
|
|
row["reuseContext"]["desktopUiParity"] = policy["desktopUiParity"]
|
|
row["reuseContext"]["instruction"] += (
|
|
" Current mainline: desktop UI parity packages. Reuse existing workbench/task/adapter/native owners. "
|
|
"Implement command registration -> desktop menu/toolbar -> task/property/dialog -> native call -> status feedback "
|
|
"as one coherent family, including desktop labels, icons, order, shortcuts, enabled state, spacing and cancel semantics. "
|
|
"The complete UI scope includes menus, buttons, property pages, dialogs, operation interfaces, control/task panels, context menus and status feedback. "
|
|
"Use docs/current/desktop-ui-parity-inventory.json as a source locator, including Python InitGui/task panels and shared src/Gui, never as proof of parity. "
|
|
"Do not drop remaining original UI items as unrelated or close an entire package after a subset. Record precise unfinished commands/surfaces and the next edit on the same original key. "
|
|
"No JSON/Python console required for ordinary operators. Geometry remains native OCCT WASM; Three.js displays only. "
|
|
"Report: module | UI items | source | route | OCCT boundary | status | conclusion. "
|
|
"Use tools/policy_io.py --key KEY --patch-file PATH to save receipts; never truncate live policy.")
|
|
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"]]
|
|
if len(members) > 1:
|
|
parts.extend(member["id"] for member in members[1:])
|
|
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": len(members)},
|
|
"records": [[member["id"], member.get("command", member["id"]), "implementation_required", member["source"], member.get("line", 1)] for member in members],
|
|
"confirmedGaps": members,
|
|
"functionalPackage": {"name": row.get("functionalPackage") or row["module"] + " shared workflow",
|
|
"keys": [member["key"] for member in members],
|
|
"rule": "Implement related members together, share source reading and integration, then build each affected target once and one configured frontend bundle. Record each member truthfully; preserve unfinished members without repeating completed work."},
|
|
"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())
|