Ctrl K
ComunidadGuías…

scripts/check-version-consistency.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Verify FlintTrade release version consistency across publishable surfaces."""

from __future__ import annotations

import json
import re
import sys
import tomllib
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]

PACKAGE_JSONS = [
    "package.json",
    "packages/core/design-system/package.json",
    "packages/apps/terminal/package.json",
    "packages/apps/site/package.json",
    "packages/apps/desktop/package.json",
]

CARGO_MANIFESTS = [
    "packages/core/ticks/Cargo.toml",
]

CURRENT_TAG_FILES = [
    "readme.md",
    "disclaimer.md",
    "CLAUDE.md",
    "PLAN.md",
    "docs/ARCHITECTURE.md",
    "docs/DESKTOP.md",
    "docs/INVENTORY.md",
    "docs/USER_GUIDE.md",
    "docs/setup/macos.md",
    "docs/setup/QUICKSTART.md",
    "docs/setup/windows.md",
    "packages/apps/site/src/app/page.tsx",
    "templates/agent-context/AGENTS.md.template",
    "templates/agent-context/CLAUDE.md.template",
    "templates/agent-context/PLAN.md.template",
]

INSTALLER_POLICY_FILES = [
    "readme.md",
    "docs/DESKTOP.md",
    "docs/setup/macos.md",
    "docs/setup/windows.md",
    "packages/apps/site/src/app/page.tsx",
    "Makefile",
    ".github/workflows/desktop-release.yml",
]

# Subsystems that no longer exist; docs describing the CURRENT system must not
# name them (LM Studio → managed Ollama; OpenClaw → agent_backends).
RETIRED_NAMES = ["OpenClaw", "LM Studio"]
RETIRED_NAME_FILES = [
    "security.md",
    "readme.md",
    "docs/DESKTOP.md",
    "docs/USER_GUIDE.md",
    "docs/ARCHITECTURE.md",
]

STALE_INSTALLER_PHRASES = [
    "`.dmg`, `.app`",
    "`.dmg` or `.app`",
    ".dmg or .app",
    ".dmg + .app",
    ".dmg/.app",
    "`.msi`, `.exe`",
    "`.msi` or `.exe`",
    ".exe or .msi",
    ".msi/.exe",
]

def _stale_current_pattern(version_tag: str) -> re.Pattern[str] | None:
    """Pattern flagging sibling pre-releases of the current tag (e.g. an older beta).

    Derived from VERSION so a release bump cannot leave this check pinned to a
    previous tag. Stable (non-prerelease) versions have no sibling family.
    """
    match = re.match(r"^v(\d+\.\d+\.\d+-[A-Za-z]+)(?:\.(\d+))?$", version_tag)
    if not match:
        return None
    family = re.escape(match.group(1))
    suffix = match.group(2)
    if suffix is None:
        return re.compile(rf"\bv?{family}\.\d+\b")
    return re.compile(rf"\bv?{family}(?!\.{re.escape(suffix)}\b)\b")
SEMVER_IDENTIFIER = r"(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
RELEASE_VERSION_PATTERN = rf"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-{SEMVER_IDENTIFIER}(?:\.{SEMVER_IDENTIFIER})*)?"
SEMVER_WITH_PRERELEASE = re.compile(rf"^v?{RELEASE_VERSION_PATTERN}$")


def _read_json(path: str) -> object:
    return json.loads((ROOT / path).read_text(encoding="utf-8"))


def _read_toml(path: str) -> dict[str, object]:
    with (ROOT / path).open("rb") as handle:
        return tomllib.load(handle)


def _fail(failures: list[str], label: str, actual: object, expected: object) -> None:
    if actual != expected:
        failures.append(f"{label}: expected {expected!r}, got {actual!r}")


def main() -> int:
    failures: list[str] = []

    version_tag = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
    if not SEMVER_WITH_PRERELEASE.match(version_tag):
        failures.append(f"VERSION is not SemVer-like: {version_tag!r}")

    if not version_tag.startswith("v"):
        failures.append(f"VERSION must be a git tag with leading v: {version_tag!r}")

    bare_version = version_tag.removeprefix("v")
    release_note = f"docs/releases/{version_tag}.md"

    _fail(failures, "flint.toml project.version", _read_toml("flint.toml")["project"]["version"], version_tag)

    for path in PACKAGE_JSONS:
        package = _read_json(path)
        assert isinstance(package, dict)
        _fail(failures, f"{path} version", package.get("version"), bare_version)

    desktop_package = _read_json("packages/apps/desktop/package.json")
    assert isinstance(desktop_package, dict)
    desktop_dependencies = desktop_package.get("devDependencies")
    desktop_build = desktop_package.get("build")
    assert isinstance(desktop_dependencies, dict)
    assert isinstance(desktop_build, dict)
    electron_version = desktop_dependencies.get("electron")
    if not isinstance(electron_version, str) or re.fullmatch(r"\d+\.\d+\.\d+", electron_version) is None:
        failures.append(
            "packages/apps/desktop/package.json devDependencies.electron must be an exact version"
        )
    else:
        _fail(
            failures,
            "packages/apps/desktop/package.json build.electronVersion",
            desktop_build.get("electronVersion"),
            electron_version,
        )

    for path in sorted((ROOT / "packages").glob("*/*/pyproject.toml")):
        data = _read_toml(str(path.relative_to(ROOT)))
        project = data.get("project")
        if isinstance(project, dict):
            _fail(failures, f"{path.relative_to(ROOT)} project.version", project.get("version"), bare_version)

    for path in CARGO_MANIFESTS:
        data = _read_toml(path)
        package = data.get("package")
        if isinstance(package, dict):
            _fail(failures, f"{path} package.version", package.get("version"), bare_version)

    tick_init = (ROOT / "packages/core/ticks/python/tick_engine/__init__.py").read_text(encoding="utf-8")
    match = re.search(r'^__version__ = "([^"]+)"$', tick_init, re.MULTILINE)
    _fail(failures, "tick_engine __version__", match.group(1) if match else None, bare_version)

    if not (ROOT / release_note).exists():
        failures.append(f"missing current release note: {release_note}")

    site_generator = (ROOT / "packages/apps/site/scripts/generate-content.mjs").read_text(encoding="utf-8")
    if release_note not in site_generator:
        failures.append(f"site docs generator does not include {release_note}")
    if "const releasePages = releaseDocs.map" not in site_generator:
        failures.append("site docs generator must derive release sidebar pages from releaseDocs")

    for path in CURRENT_TAG_FILES:
        text = (ROOT / path).read_text(encoding="utf-8")
        if version_tag not in text:
            failures.append(f"{path} does not mention current VERSION {version_tag}")
        stale_pattern = _stale_current_pattern(version_tag)
        stale = stale_pattern.search(text) if stale_pattern else None
        if stale:
            failures.append(f"{path} still mentions stale current beta tag {stale.group(0)!r}")

    for path in INSTALLER_POLICY_FILES:
        text = (ROOT / path).read_text(encoding="utf-8")
        for phrase in STALE_INSTALLER_PHRASES:
            if phrase in text:
                failures.append(f"{path} still advertises unsupported beta installer artefact phrase {phrase!r}")

    # Retired subsystem names must not resurface as current-state claims in the
    # policy/docs surfaces (migration code, credits, and changelog history are
    # exempt by not being listed here).
    for path in RETIRED_NAME_FILES:
        text = (ROOT / path).read_text(encoding="utf-8")
        for name in RETIRED_NAMES:
            if name in text:
                failures.append(f"{path} still names retired subsystem {name!r}")

    if failures:
        print("Version consistency check failed:")
        for failure in failures:
            print(f"  - {failure}")
        return 1

    print(f"Version consistency check passed for {version_tag}")
    return 0


if __name__ == "__main__":
    sys.exit(main())