Continue FreeCAD Web UI parity repairs
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
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
This commit is contained in:
47
tools/build_web_frontend.py
Normal file
47
tools/build_web_frontend.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the configured operator frontend from its fixed working directory.
|
||||
|
||||
No npm lifecycle hooks, standalone typechecks, tests or native builds. The
|
||||
caller groups related frontend edits before invoking this delivery command.
|
||||
"""
|
||||
import os
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
web = Path(__file__).resolve().parents[1] / "web"
|
||||
vite = web / "node_modules/vite/bin/vite.js"
|
||||
node = shutil.which("node")
|
||||
if not node or not vite.is_file():
|
||||
print(f"Frontend dependency unavailable: node={node}, Vite={vite}", file=sys.stderr)
|
||||
return 2
|
||||
env = {**os.environ, "FREECAD_WEB_FAST_LINK": "ON"}
|
||||
# The foreground session and the sole scheduler share web/dist. Keep the
|
||||
# lock outside dist: Vite clears that directory before writing a bundle.
|
||||
with (web / ".freecad-web-frontend-build.lock").open("a") as lock:
|
||||
started = time.monotonic()
|
||||
waiting = False
|
||||
while True:
|
||||
try:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
break
|
||||
except BlockingIOError:
|
||||
if not waiting:
|
||||
print("Waiting for the existing frontend delivery to release web/dist", flush=True)
|
||||
waiting = True
|
||||
if time.monotonic() - started >= 1800:
|
||||
print("Frontend delivery lock timed out; retain edits and resume after the active build", file=sys.stderr)
|
||||
return 124
|
||||
time.sleep(0.25)
|
||||
# Vite retains the descriptor if the outer runner exits mid-delivery.
|
||||
return subprocess.run([node, str(vite), "build"], cwd=web, env=env,
|
||||
pass_fds=(lock.fileno(),), check=False).returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
347
tools/prepare_implementation_business_batch.py
Normal file
347
tools/prepare_implementation_business_batch.py
Normal file
@@ -0,0 +1,347 @@
|
||||
#!/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())
|
||||
235
tools/read_implementation_source.py
Normal file
235
tools/read_implementation_source.py
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read a bounded missing patch anchor from the current implementation packet."""
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PACKET = ROOT / "docs/current/module-implementation-packet.json"
|
||||
MAX_WINDOWS = 4
|
||||
CONTINUOUS_MAX_WINDOWS = 16
|
||||
MAX_TOTAL_WINDOW_BYTES = 48000
|
||||
MAX_WINDOW_BYTES = 12000
|
||||
|
||||
|
||||
def window_limit():
|
||||
try:
|
||||
policy = json.loads((ROOT / "docs/current/execution-policy.json").read_text())
|
||||
except (OSError, ValueError):
|
||||
return MAX_WINDOWS
|
||||
return CONTINUOUS_MAX_WINDOWS if policy.get("continuousDevelopment") is True else MAX_WINDOWS
|
||||
|
||||
|
||||
def source_limits(packet):
|
||||
limit = window_limit()
|
||||
count = len({row.get("apiId") for row in packet.get("records", [])
|
||||
if isinstance(row, dict) and isinstance(row.get("apiId"), str) and row["apiId"]})
|
||||
scale = min(3, max(1, count)) if limit == CONTINUOUS_MAX_WINDOWS else 1
|
||||
return limit * scale, MAX_TOTAL_WINDOW_BYTES * scale
|
||||
|
||||
|
||||
def packet_paths(packet):
|
||||
names = {row.get("path") for key in ("supportFiles", "editableExcerpts") for row in packet.get(key, [])}
|
||||
gap = packet.get("confirmedGap") or {}
|
||||
names.add(gap.get("source"))
|
||||
names.update(name for name in gap.get("sourceAnchors", []) if isinstance(name, str))
|
||||
for member in packet.get("confirmedGaps", []):
|
||||
names.add(member.get("source"))
|
||||
names.update(name for name in member.get("sourceAnchors", []) if isinstance(name, str))
|
||||
contract_name = packet.get("acceptanceContract")
|
||||
if isinstance(contract_name, str):
|
||||
contract_path = (ROOT / contract_name).resolve()
|
||||
if contract_path.is_relative_to(ROOT) and contract_path.is_relative_to((ROOT / "docs/business-acceptance").resolve()) and contract_path.is_file():
|
||||
try:
|
||||
contract = json.loads(contract_path.read_text(encoding="utf-8"))
|
||||
if isinstance(contract, dict):
|
||||
names.add(contract_name)
|
||||
command = contract.get("command", [])
|
||||
if isinstance(command, list) and len(command) > 1 and isinstance(command[1], str):
|
||||
names.add(command[1])
|
||||
for key in ("dependencies", "caseDependencies"):
|
||||
paths = contract.get(key, [])
|
||||
if isinstance(paths, list):
|
||||
names.update(name for name in paths if isinstance(name, str))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
for row in packet.get("records", []):
|
||||
source = row.get("source")
|
||||
if source:
|
||||
names.add(source if (ROOT / source).is_file() else str(Path("freecad-source") / source))
|
||||
names.update(match.get("path") for match in row.get("nativeMatches", []))
|
||||
for gap in row.get("implementationGaps", []):
|
||||
names.update(match.get("path") for match in gap.get("sourceCandidates", []))
|
||||
return {(ROOT / name).resolve() for name in names if isinstance(name, str) and name}
|
||||
|
||||
|
||||
def module_source_path(path, packet):
|
||||
"""Permit named in-module implementation anchors before case declaration."""
|
||||
module = packet.get("module")
|
||||
if window_limit() != CONTINUOUS_MAX_WINDOWS or not isinstance(module, str) or not module.isalnum():
|
||||
return False
|
||||
if not path.is_relative_to(ROOT):
|
||||
return False
|
||||
relative = path.relative_to(ROOT)
|
||||
if packet.get("confirmedGaps") and relative.parent == Path("freecad-source/src/WebAssembly/FreeCADCoreProbe"):
|
||||
# Functional packages legitimately cross module boundaries through
|
||||
# existing document owners. Keep exact-file/window budgets, but don't
|
||||
# turn missing packet metadata into a user-authorization blocker.
|
||||
return path.suffix.lower() in {".cpp", ".h", ".hpp", ".py"} or path.name == "CMakeLists.txt"
|
||||
if relative.is_relative_to(Path("freecad-source/src/Mod") / module):
|
||||
return path.suffix.lower() in {".cpp", ".cxx", ".h", ".hpp", ".py", ".ui", ".xml", ".txt"}
|
||||
slug = module.lower()
|
||||
if relative.parent == Path("tools"):
|
||||
return path.name.startswith(slug + "-") and path.name.endswith("-business-cases.mjs")
|
||||
return (relative.parent == Path("web/src") and path.name.startswith(slug + "-")
|
||||
and path.suffix in {".ts", ".tsx", ".json"})
|
||||
|
||||
|
||||
def frontend_dependency_path(path, packet):
|
||||
"""Allow one exact relative import from a confirmed package's named sources.
|
||||
|
||||
Workbench UI packages cross module boundaries through document facades and
|
||||
shared task components. Resolve only direct imports; this is not a recursive
|
||||
module crawl and it does not change the existing excerpt/turn budgets.
|
||||
"""
|
||||
if window_limit() != CONTINUOUS_MAX_WINDOWS or not (packet.get("confirmedGaps") or packet.get("confirmedGap")):
|
||||
return False
|
||||
frontend = (ROOT / "web/src").resolve()
|
||||
if not path.is_relative_to(frontend) or path.suffix not in {".ts", ".tsx", ".json"}:
|
||||
return False
|
||||
for anchor in sorted(packet_paths(packet)):
|
||||
if (not anchor.is_relative_to(frontend) or anchor.suffix not in {".ts", ".tsx"}
|
||||
or not anchor.is_file() or anchor.stat().st_size > 512 * 1024):
|
||||
continue
|
||||
source = anchor.read_text(encoding="utf-8")
|
||||
for specifier in re.findall(r'''(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)["'](\.[^"'\n]+)["']''', source):
|
||||
dependency = (anchor.parent / specifier).resolve()
|
||||
candidates = {dependency}
|
||||
if dependency.suffix == ".js":
|
||||
candidates.update(dependency.with_suffix(suffix) for suffix in (".ts", ".tsx"))
|
||||
elif not dependency.suffix:
|
||||
candidates.update(Path(str(dependency) + suffix) for suffix in (".ts", ".tsx", ".json"))
|
||||
if path in candidates:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def packet_summary(packet):
|
||||
# Task metadata is structured JSON, not a source file to slice by lines.
|
||||
result = {"batchId": packet.get("batchId"), "records": [], "supportPaths": [],
|
||||
"instruction": "This is task metadata, not source lines. Use sourceReadCommand below; changing --start on the packet does not read C++ or Python."}
|
||||
retained = packet.get("retainedBusinessResults", [])
|
||||
result["retainedBusinessResults"] = []
|
||||
for row in retained:
|
||||
result["retainedBusinessResults"].append(row)
|
||||
if len(json.dumps(result).encode("utf-8")) > MAX_WINDOW_BYTES // 2:
|
||||
result["retainedBusinessResults"].pop()
|
||||
break
|
||||
for row in packet.get("records", []):
|
||||
entry = {key: row[key] for key in ("apiId", "name", "source", "excerptLine") if key in row}
|
||||
if isinstance(row.get("source"), str) and row.get("excerptLine"):
|
||||
source = row["source"]
|
||||
if not (ROOT / source).is_file(): source = str(Path("freecad-source") / source)
|
||||
entry["sourceReadCommand"] = shlex.join([
|
||||
"python3", "tools/read_implementation_source.py", source,
|
||||
"--start", str(max(1, int(row["excerptLine"]))), "--lines", "60",
|
||||
"--reason", "Read selected command native source anchor",
|
||||
])
|
||||
entry["gaps"] = [{key: gap[key] for key in ("stage", "classification", "reason") if key in gap}
|
||||
for gap in row.get("implementationGaps", [])]
|
||||
result["records"].append(entry)
|
||||
if len(json.dumps(result).encode("utf-8")) > MAX_WINDOW_BYTES - 1000:
|
||||
result["records"].pop()
|
||||
break
|
||||
result["omittedRecords"] = len(packet.get("records", [])) - len(result["records"])
|
||||
for path in sorted(packet_paths(packet)):
|
||||
if not path.is_relative_to(ROOT) or not path.is_file():
|
||||
continue
|
||||
result["supportPaths"].append(str(path.relative_to(ROOT)))
|
||||
if len(json.dumps(result).encode("utf-8")) > MAX_WINDOW_BYTES - 200:
|
||||
result["supportPaths"].pop()
|
||||
break
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("path")
|
||||
parser.add_argument("--start", required=True, type=int)
|
||||
parser.add_argument("--lines", type=int, default=60)
|
||||
parser.add_argument("--reason", required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
marker_value = os.environ.get("FREECAD_SOURCE_BUDGET_MARKER")
|
||||
if not marker_value:
|
||||
raise ValueError("This reader requires a scheduler-owned turn budget")
|
||||
if not 1 <= args.lines <= 100 or args.start < 1:
|
||||
raise ValueError("Use a positive --start and 1..100 --lines")
|
||||
if not args.reason.strip() or len(args.reason) > 240:
|
||||
raise ValueError("Explain the missing anchor in --reason (1..240 characters)")
|
||||
packet = json.loads(PACKET.read_text(encoding="utf-8"))
|
||||
path = (ROOT / args.path).resolve()
|
||||
metadata = path == PACKET.resolve()
|
||||
if path.is_relative_to(ROOT) and not path.is_file():
|
||||
raise ValueError("Requested source file does not exist or is not a file; locate the exact filename with a scoped file search")
|
||||
if not path.is_relative_to(ROOT) or (not metadata and path not in packet_paths(packet)
|
||||
and not module_source_path(path, packet)
|
||||
and not frontend_dependency_path(path, packet)) or not path.is_file():
|
||||
raise ValueError("Use an exact packet dependency, its direct frontend import, or current-module source/case path; no directory scans")
|
||||
if path.stat().st_size > 16 * 1024 * 1024:
|
||||
raise ValueError("This file exceeds the bounded source reader size limit")
|
||||
if metadata:
|
||||
excerpt = packet_summary(packet)
|
||||
args.start = 1
|
||||
else:
|
||||
with path.open(encoding="utf-8") as source:
|
||||
excerpt = "".join(itertools.islice(source, args.start - 1, args.start - 1 + args.lines))
|
||||
if not excerpt:
|
||||
raise ValueError("The requested anchor is beyond EOF; use the packet's named line")
|
||||
size = len(excerpt.encode("utf-8"))
|
||||
if size > MAX_WINDOW_BYTES:
|
||||
raise ValueError("Excerpt exceeds 12000 bytes; reduce --lines, do not dump a minified file")
|
||||
relative = str(path.relative_to(ROOT))
|
||||
fingerprint = hashlib.sha256(f"{relative}:{args.start}:{excerpt}".encode("utf-8")).hexdigest()
|
||||
# Per-turn file locking prevents parallel requests from exceeding the
|
||||
# bounded allowance. Repeated unchanged windows add no context.
|
||||
budget_path = Path(marker_value + ".windows.json")
|
||||
with budget_path.open("a+", encoding="utf-8") as budget:
|
||||
fcntl.flock(budget, fcntl.LOCK_EX)
|
||||
budget.seek(0)
|
||||
data = budget.read()
|
||||
state = json.loads(data) if data else {"batchId": packet["batchId"], "windows": [], "bytes": 0}
|
||||
if state["batchId"] != packet["batchId"]:
|
||||
raise ValueError("Batch changed during this turn; retain the current implementation checkpoint")
|
||||
if any(row["fingerprint"] == fingerprint for row in state["windows"]):
|
||||
print(json.dumps({"status": "already_in_context", "path": relative, "start": args.start,
|
||||
"instruction": "Reuse the unchanged excerpt already returned in this turn"}))
|
||||
return 0
|
||||
limit, byte_limit = source_limits(packet)
|
||||
if len(state["windows"]) >= limit or state["bytes"] + size > byte_limit:
|
||||
raise ValueError(f"Supplemental source budget reached ({limit} windows / {byte_limit} bytes); reuse retained excerpts and continue the identified repair")
|
||||
state["windows"].append({"path": relative, "start": args.start, "lines": args.lines,
|
||||
"fingerprint": fingerprint, "bytes": size, "reason": args.reason})
|
||||
state["bytes"] += size
|
||||
budget.seek(0)
|
||||
budget.truncate()
|
||||
budget.write(json.dumps(state, separators=(",", ":")))
|
||||
budget.flush()
|
||||
print(json.dumps({"status": "packet_summary" if metadata else "source_window", "batchId": packet["batchId"], "path": relative,
|
||||
"start": args.start, "bytes": size, "remainingWindows": limit - len(state["windows"]),
|
||||
"excerpt": excerpt}, ensure_ascii=False))
|
||||
return 0
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(f"IMPLEMENTATION_SOURCE_WINDOW_REJECTED: {error}", file=sys.stderr)
|
||||
return 64
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user