k0880k commited on
Commit
d76e42d
·
1 Parent(s): d9b10e5

feat(migration): add generic legacy extension support pipeline

Browse files
Files changed (1) hide show
  1. core/extension_support_pipeline.py +185 -0
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"))}