Spaces:
Running
Running
k0880k commited on
Commit ·
d76e42d
1
Parent(s): d9b10e5
feat(migration): add generic legacy extension support pipeline
Browse files
core/extension_support_pipeline.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forward-engineered support pipeline for legacy Joomla extensions.
|
| 2 |
+
|
| 3 |
+
Unsupported component/plugin/module/package families are advanced through a
|
| 4 |
+
bounded adapter pipeline:
|
| 5 |
+
|
| 6 |
+
observe -> normalize -> derive -> install -> native-upgrade -> prove -> hydrate
|
| 7 |
+
|
| 8 |
+
Vendor archives remain immutable. Derived candidates are never called compatible
|
| 9 |
+
until the selected target proves the required functional contract. Operator
|
| 10 |
+
policy controls how far automation may proceed.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from dataclasses import dataclass, asdict
|
| 15 |
+
from typing import Any
|
| 16 |
+
import re
|
| 17 |
+
|
| 18 |
+
from . import compat_bridge_forge as bridge
|
| 19 |
+
from . import unified_migration as um
|
| 20 |
+
|
| 21 |
+
MODES = {"strict", "assisted", "auto"}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass(frozen=True)
|
| 25 |
+
class SupportProfile:
|
| 26 |
+
element: str
|
| 27 |
+
name: str
|
| 28 |
+
family_stems: tuple[str, ...] = ()
|
| 29 |
+
required_intents: tuple[str, ...] = ("http.route.dispatch",)
|
| 30 |
+
notes: tuple[str, ...] = ()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
PROFILES: dict[str, SupportProfile] = {
|
| 34 |
+
"com_easyblog": SupportProfile(
|
| 35 |
+
element="com_easyblog",
|
| 36 |
+
name="EasyBlog",
|
| 37 |
+
family_stems=("easyblog",),
|
| 38 |
+
required_intents=("http.route.dispatch", "administrator.route.dispatch"),
|
| 39 |
+
notes=(
|
| 40 |
+
"Treat the captured vendor package as immutable input.",
|
| 41 |
+
"On a Joomla 6 target, prefer a derived compatibility candidate plus runtime proof over raw table hydration.",
|
| 42 |
+
"EasyBlog data remains quarantined until target code/schema is proven ready.",
|
| 43 |
+
),
|
| 44 |
+
),
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def normalize_mode(value: Any) -> str:
|
| 49 |
+
mode = str(value or "assisted").strip().lower()
|
| 50 |
+
return mode if mode in MODES else "assisted"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _version_tuple(raw: str) -> tuple[int, ...]:
|
| 54 |
+
nums = re.findall(r"\d+", str(raw or ""))
|
| 55 |
+
return tuple(int(x) for x in nums[:4]) or (0,)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _major(raw: str) -> int:
|
| 59 |
+
return _version_tuple(raw)[0]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _extension_row(site: str, element: str) -> dict | None:
|
| 63 |
+
rows, _ = um._extension_rows(site)
|
| 64 |
+
lowered = str(element or "").lower()
|
| 65 |
+
for row in rows:
|
| 66 |
+
if str(row.get("element") or "").lower() == lowered:
|
| 67 |
+
manifest = um._manifest(row.get("manifest_cache"))
|
| 68 |
+
return {
|
| 69 |
+
"element": row.get("element") or "",
|
| 70 |
+
"name": row.get("name") or row.get("element") or "",
|
| 71 |
+
"type": row.get("type") or "",
|
| 72 |
+
"folder": row.get("folder") or "",
|
| 73 |
+
"version": str(manifest.get("version") or ""),
|
| 74 |
+
"author": str(manifest.get("author") or ""),
|
| 75 |
+
}
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def coordinate(source: str, target: str, element: str) -> dict:
|
| 80 |
+
src_rows, _ = um._extension_rows(source)
|
| 81 |
+
dst_rows, _ = um._extension_rows(target)
|
| 82 |
+
return {
|
| 83 |
+
"source": source,
|
| 84 |
+
"target": target,
|
| 85 |
+
"element": element,
|
| 86 |
+
"source_joomla": um._joomla_version(src_rows),
|
| 87 |
+
"target_joomla": um._joomla_version(dst_rows),
|
| 88 |
+
"source_extension": _extension_row(source, element),
|
| 89 |
+
"target_extension": _extension_row(target, element),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def profile_for(element: str) -> dict:
|
| 94 |
+
profile = PROFILES.get(str(element or "").lower())
|
| 95 |
+
if profile:
|
| 96 |
+
return asdict(profile)
|
| 97 |
+
stem = re.sub(r"^(?:com_|plg_|mod_|pkg_|file_)", "", str(element or "").lower())
|
| 98 |
+
return asdict(SupportProfile(element=str(element or ""), name=str(element or "extension"), family_stems=(stem,) if stem else ()))
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def plan(source: str, target: str, element: str, *, mode: str = "assisted") -> dict:
|
| 102 |
+
mode = normalize_mode(mode)
|
| 103 |
+
coord = coordinate(source, target, element)
|
| 104 |
+
profile = profile_for(element)
|
| 105 |
+
src = coord.get("source_extension") or {}
|
| 106 |
+
dst = coord.get("target_extension") or {}
|
| 107 |
+
target_major = _major(coord.get("target_joomla") or "")
|
| 108 |
+
try:
|
| 109 |
+
forge_plan = bridge.plan(source, target, element)
|
| 110 |
+
except Exception as exc:
|
| 111 |
+
forge_plan = {"can_forge": False, "verified": False, "known_breaks": 0, "automatic_candidates": [], "semantic_ports": [], "error": f"{type(exc).__name__}: {str(exc)[:320]}"}
|
| 112 |
+
|
| 113 |
+
target_present = bool(dst)
|
| 114 |
+
source_present = bool(src)
|
| 115 |
+
verified = bool(forge_plan.get("verified"))
|
| 116 |
+
automatic = list(forge_plan.get("automatic_candidates") or [])
|
| 117 |
+
semantic = list(forge_plan.get("semantic_ports") or [])
|
| 118 |
+
captured = bool((forge_plan.get("package") or {}).get("available"))
|
| 119 |
+
|
| 120 |
+
if verified:
|
| 121 |
+
state, next_action = "verified", "release_data_family"
|
| 122 |
+
elif target_present and not forge_plan.get("known_breaks"):
|
| 123 |
+
state, next_action = "target_present_unproven", "runtime_verify"
|
| 124 |
+
elif semantic:
|
| 125 |
+
state, next_action = "semantic_port_required", "operator_or_adapter_port"
|
| 126 |
+
elif captured and automatic:
|
| 127 |
+
state, next_action = "bridge_ready", "forge_candidate"
|
| 128 |
+
elif captured and target_major >= 6:
|
| 129 |
+
state, next_action = "runtime_proof_required", "scan_and_probe"
|
| 130 |
+
elif not captured:
|
| 131 |
+
state, next_action = "package_required", "capture_authorized_package"
|
| 132 |
+
else:
|
| 133 |
+
state, next_action = "inspect", "inspect_runtime"
|
| 134 |
+
|
| 135 |
+
if str(element).lower() == "com_easyblog" and target_major >= 6 and captured and not semantic and not verified:
|
| 136 |
+
next_action = "forge_install_upgrade_verify"
|
| 137 |
+
|
| 138 |
+
allowed = {
|
| 139 |
+
"scan": True,
|
| 140 |
+
"deterministic_transform": mode in {"assisted", "auto"},
|
| 141 |
+
"install_derived_candidate": mode == "auto",
|
| 142 |
+
"native_upgrade_hooks": mode == "auto",
|
| 143 |
+
"release_data_family": verified,
|
| 144 |
+
}
|
| 145 |
+
approvals = []
|
| 146 |
+
if mode == "strict" and next_action in {"forge_candidate", "forge_install_upgrade_verify"}:
|
| 147 |
+
approvals.append("allow deterministic compatibility transforms")
|
| 148 |
+
if mode in {"strict", "assisted"} and next_action in {"forge_candidate", "runtime_verify", "forge_install_upgrade_verify", "scan_and_probe"}:
|
| 149 |
+
approvals.append("approve target installation/runtime verification")
|
| 150 |
+
if semantic:
|
| 151 |
+
approvals.append("semantic port required; no automatic source rewrite")
|
| 152 |
+
|
| 153 |
+
stages = [
|
| 154 |
+
{"id": "observe", "status": "done" if source_present else "blocked", "detail": "source extension/runtime inventory"},
|
| 155 |
+
{"id": "normalize", "status": "ready" if source_present else "blocked", "detail": "normalize extension family + migration intent"},
|
| 156 |
+
{"id": "derive", "status": "done" if verified else ("ready" if captured else "blocked"), "detail": "preserve vendor archive; derive compatibility candidate only"},
|
| 157 |
+
{"id": "install", "status": "done" if verified else ("ready" if allowed["install_derived_candidate"] else "approval"), "detail": "install through Joomla administrator installer"},
|
| 158 |
+
{"id": "upgrade", "status": "done" if verified else ("ready" if allowed["native_upgrade_hooks"] else "approval"), "detail": "allow extension/Joomla installer schema and upgrade hooks to establish target-owned structure"},
|
| 159 |
+
{"id": "prove", "status": "done" if verified else "pending", "detail": "frontend/admin runtime probes on selected target"},
|
| 160 |
+
{"id": "hydrate", "status": "ready" if verified else "blocked", "detail": "release extension data only after target support proof"},
|
| 161 |
+
]
|
| 162 |
+
return {"coordinate": coord, "profile": profile, "mode": mode, "state": state, "next_action": next_action, "allowed": allowed, "approvals_required": approvals, "stages": stages, "forge": forge_plan, "contract": "observe -> normalize -> derive -> install -> native-upgrade -> prove -> hydrate"}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def execute(source: str, target: str, element: str, *, mode: str = "assisted", source_username: str = "", source_password: str = "", target_username: str = "", target_password: str = "") -> dict:
|
| 166 |
+
p = plan(source, target, element, mode=mode)
|
| 167 |
+
mode = p["mode"]
|
| 168 |
+
if p["state"] == "verified":
|
| 169 |
+
return {**p, "executed": [], "ready_for_data": True}
|
| 170 |
+
if p.get("forge", {}).get("semantic_ports"):
|
| 171 |
+
return {**p, "executed": [], "ready_for_data": False, "stop_reason": "semantic_port_required"}
|
| 172 |
+
if not p.get("forge", {}).get("package", {}).get("available"):
|
| 173 |
+
return {**p, "executed": [], "ready_for_data": False, "stop_reason": "authorized_source_package_required"}
|
| 174 |
+
executed = []
|
| 175 |
+
if p["allowed"]["deterministic_transform"]:
|
| 176 |
+
candidate = bridge.forge(source, target, element)
|
| 177 |
+
executed.append({"stage": "derive", "result": candidate})
|
| 178 |
+
else:
|
| 179 |
+
return {**p, "executed": executed, "ready_for_data": False, "stop_reason": "operator_approval_required"}
|
| 180 |
+
if not p["allowed"]["install_derived_candidate"]:
|
| 181 |
+
return {**plan(source, target, element, mode=mode), "executed": executed, "ready_for_data": False, "stop_reason": "target_install_approval_required"}
|
| 182 |
+
proof = bridge.verify(source, target, element, source_username=source_username, source_password=source_password, target_username=target_username, target_password=target_password)
|
| 183 |
+
executed.append({"stage": "install_upgrade_prove", "result": proof})
|
| 184 |
+
final = plan(source, target, element, mode=mode)
|
| 185 |
+
return {**final, "executed": executed, "ready_for_data": bool(proof.get("verified"))}
|