scripts/compatibility_matrix.py
Rama main · réplica de lectura del repositorio original.
"""
compatibility_matrix.py
Deduces SDK <-> Server compatibility using the relay model:
Each SDK version is compatible with the server versions that existed
during its active lifetime — from the server current when it was released,
up to (but not including) the server version that the next SDK release
was built against.
USAGE:
python compatibility_matrix.py \
--core C:/Users/franc/PycharmProjects/projectdavid-core \
--sdk C:/Users/franc/PycharmProjects/entitites_sdk \
--out compatibility.json \
--md COMPATIBILITY.md
OUTPUT:
compatibility.json — machine-readable matrix
COMPATIBILITY.md — human-readable two-column table
"""
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
# ─── DATA STRUCTURES ────────────────────────────────────────────────────────
@dataclass
class ReleasePin:
tag: str
timestamp: datetime
common_pin: str | None = None
orm_pin: str | None = None
raw_deps: list[str] = field(default_factory=list)
# ─── GIT HELPERS ────────────────────────────────────────────────────────────
def git(repo: Path, *args: str) -> str:
result = subprocess.run(
["git", "-C", str(repo), *args],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"git error in {repo}: {result.stderr.strip()}")
return result.stdout.strip()
def get_tags(repo: Path) -> list[str]:
raw = git(repo, "tag", "--sort=version:refname")
return [t for t in raw.splitlines() if t.strip()]
def get_tag_timestamp(repo: Path, tag: str) -> datetime:
raw = git(repo, "log", "-1", "--format=%ct", tag)
try:
return datetime.fromtimestamp(int(raw), tz=timezone.utc)
except (ValueError, OSError):
return datetime.min.replace(tzinfo=timezone.utc)
def get_file_at_tag(repo: Path, tag: str, filepath: str) -> str | None:
try:
return git(repo, "show", f"{tag}:{filepath}")
except RuntimeError:
return None
PYPROJECT_CANDIDATES = [
"pyproject.toml",
"src/api/entities_api/pyproject.toml",
"src/projectdavid/pyproject.toml",
"src/pyproject.toml",
"entities_api/pyproject.toml",
]
def get_pyproject_at_tag(repo: Path, tag: str) -> str | None:
for candidate in PYPROJECT_CANDIDATES:
content = get_file_at_tag(repo, tag, candidate)
if content:
return content
return None
# ─── PYPROJECT PARSING ──────────────────────────────────────────────────────
def extract_pins(pyproject_content: str) -> tuple[str | None, str | None, list[str]]:
common_pin = None
orm_pin = None
raw_deps = []
dep_block_match = re.search(
r"dependencies\s*=\s*\[(.*?)\]",
pyproject_content,
re.DOTALL,
)
if not dep_block_match:
return None, None, []
block = dep_block_match.group(1)
deps = re.findall(r'"([^"]+)"', block)
raw_deps = deps
for dep in deps:
normalized = dep.replace("_", "-").lower()
if normalized.startswith("projectdavid-common"):
common_pin = dep
elif normalized.startswith("projectdavid-orm"):
orm_pin = dep
return common_pin, orm_pin, raw_deps
# ─── RELEASE EXTRACTION ─────────────────────────────────────────────────────
def extract_releases(repo: Path, label: str) -> list[ReleasePin]:
tags = get_tags(repo)
releases = []
print(f"\n[{label}] Found {len(tags)} tags — extracting...")
for tag in tags:
content = get_pyproject_at_tag(repo, tag)
ts = get_tag_timestamp(repo, tag)
if content:
common_pin, orm_pin, raw_deps = extract_pins(content)
else:
common_pin, orm_pin, raw_deps = None, None, []
releases.append(
ReleasePin(
tag=tag,
timestamp=ts,
common_pin=common_pin,
orm_pin=orm_pin,
raw_deps=raw_deps,
)
)
print(f" [{label}] {len(releases)} releases parsed")
return releases
# ─── RELAY MODEL ────────────────────────────────────────────────────────────
def version_sort_key(release: ReleasePin) -> tuple[int, ...]:
"""Sort by version number — handles retroactive tagging correctly."""
tag = release.tag.lstrip("v")
parts = []
for p in re.split(r"[\.\-]", tag):
try:
parts.append(int(p))
except ValueError:
parts.append(0)
return tuple(parts)
def nearest_server_at_or_before(
ts: datetime,
server_timeline: list[ReleasePin],
) -> ReleasePin | None:
"""Return the most recent server release at or before the given timestamp."""
result = None
for s in server_timeline:
if s.timestamp <= ts:
result = s
return result
def server_index(tag: str, server_timeline: list[ReleasePin]) -> int:
for i, s in enumerate(server_timeline):
if s.tag == tag:
return i
return -1
def build_matrix(
sdk_releases: list[ReleasePin],
server_releases: list[ReleasePin],
) -> list[dict]:
"""
Relay model: each SDK version is compatible with server versions
that existed during its active lifetime — from the server current
when it was released, up to the server that the next SDK targets.
"""
matrix = []
# Exclude pre-release server tags (alpha/beta/rc) from the compatibility timeline
server_timeline = sorted(
[
s
for s in server_releases
if not any(x in s.tag.lower() for x in ["alpha", "beta", "rc"])
],
key=version_sort_key,
)
sdk_sorted = sorted(sdk_releases, key=version_sort_key)
for i, sdk in enumerate(sdk_sorted):
lower = nearest_server_at_or_before(sdk.timestamp, server_timeline)
if i + 1 < len(sdk_sorted):
next_sdk = sdk_sorted[i + 1]
upper = nearest_server_at_or_before(next_sdk.timestamp, server_timeline)
else:
upper = server_timeline[-1] if server_timeline else None
if not lower:
compatible_versions = []
compatible_range = "unknown"
elif lower.tag == (upper.tag if upper else None):
compatible_versions = [lower.tag]
compatible_range = lower.tag
else:
lo_idx = server_index(lower.tag, server_timeline)
hi_idx = server_index(
upper.tag if upper else server_timeline[-1].tag, server_timeline
)
if lo_idx > hi_idx:
lo_idx, hi_idx = hi_idx, lo_idx
compatible_versions = [s.tag for s in server_timeline[lo_idx : hi_idx + 1]]
compatible_range = _summarize_range(compatible_versions) or "unknown"
# Skip pre-release SDK tags from output
if sdk.tag.startswith("test-") or "alpha" in sdk.tag or "beta" in sdk.tag:
continue
matrix.append(
{
"sdk_version": sdk.tag,
"compatible_server_versions": compatible_versions,
"compatible_server_range": compatible_range,
}
)
# Restore original tag order
tag_order = {s.tag: i for i, s in enumerate(sdk_releases)}
matrix.sort(key=lambda r: tag_order.get(r["sdk_version"], 9999))
return matrix
def _summarize_range(versions: list[str]) -> str | None:
if not versions:
return None
if len(versions) == 1:
return versions[0]
return f">={versions[0]},<={versions[-1]}"
# ─── OUTPUT ──────────────────────────────────────────────────────────────────
def write_json(matrix: list[dict], path: Path) -> None:
path.write_text(json.dumps(matrix, indent=2))
print(f"\n✅ JSON written → {path}")
def write_markdown(matrix: list[dict], path: Path) -> None:
lines = [
"# Project David — SDK ↔ Server Compatibility Matrix",
"",
"Auto-generated by `compatibility_matrix.py`.",
"Compatibility deduced from chronological co-release history.",
"",
"| SDK Version | Compatible Server Versions |",
"|-------------|---------------------------|",
]
for row in matrix:
servers = row["compatible_server_range"] or "unknown"
lines.append(f"| `{row['sdk_version']}` | `{servers}` |")
lines += [
"",
"## Notes",
"",
"- Compatibility is deduced from chronological co-release history, not tested.",
"- Each SDK version is compatible with server versions that existed during its active lifetime.",
"- Rows marked `unknown` are early tags that predate parseable dependency metadata.",
"- When in doubt, always use the latest SDK with the latest server.",
"",
]
path.write_text("\n".join(lines))
print(f"✅ Markdown written → {path}")
# ─── CLI ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Build SDK <-> Server compatibility matrix"
)
parser.add_argument("--core", required=True, help="Path to projectdavid-core repo")
parser.add_argument("--sdk", required=True, help="Path to projectdavid SDK repo")
parser.add_argument("--out", default="compatibility.json", help="Output JSON path")
parser.add_argument("--md", default="COMPATIBILITY.md", help="Output markdown path")
args = parser.parse_args()
core_path = Path(args.core)
sdk_path = Path(args.sdk)
out_path = Path(args.out)
md_path = Path(args.md)
for p, label in [(core_path, "--core"), (sdk_path, "--sdk")]:
if not (p / ".git").exists():
print(f"ERROR: {label} path '{p}' is not a git repo")
sys.exit(1)
print("=" * 60)
print("Project David — Compatibility Matrix Builder")
print("=" * 60)
server_releases = extract_releases(core_path, "SERVER")
sdk_releases = extract_releases(sdk_path, "SDK")
print(
f"\n[MATRIX] Building: {len(sdk_releases)} SDK × {len(server_releases)} server releases"
)
matrix = build_matrix(sdk_releases, server_releases)
write_json(matrix, out_path)
write_markdown(matrix, md_path)
unmatched = [r for r in matrix if not r["compatible_server_versions"]]
print(
f"\nSummary: {len(matrix)} SDK releases, {len(unmatched)} with no server match"
)
if __name__ == "__main__":
main()