File size: 6,121 Bytes
380a7fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a4176f
b026565
884b5f8
8f4b341
 
 
 
 
8a4176f
 
 
 
 
 
 
 
 
b026565
884b5f8
8f4b341
380a7fc
 
8a4176f
380a7fc
 
 
 
8f4b341
 
 
 
 
380a7fc
8f4b341
 
 
380a7fc
 
 
 
8a4176f
 
380a7fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554220c
380a7fc
 
 
 
 
 
 
 
 
 
554220c
 
 
380a7fc
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""Construit une version livrable du Space : manifest.json à jour, puis un zip des fichiers de la version.

    python scripts/build_release.py                      # manifest + dist/unique-v3.9.zip
    python scripts/build_release.py --check              # vérifie seulement que l'arbre correspond au manifest

Le manifest liste les fichiers d'exécution (ceux que le panneau du Space vérifie au démarrage, et que
`unique_ext.updater` contrôle avant d'installer un zip) : ceux de la v3.8 plus tout `unique_ext/`.
Le zip contient le manifest, ces fichiers, et les tests et scripts associés. Il s'envoie tel quel dans le Space,
à la main (dossiers dans Files → Upload files) ou, dès la v3.9 en place, en un seul fichier avec
UNIQUE_EXT_INSTALL=<chemin du zip>.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
import zipfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = "mouh/learning/manifest.json"
CORE = ["app.py", "requirements.txt", "mouh/service.py", "mouh/verification/loop.py", "mouh/learning/__init__.py",
        "mouh/learning/prompts.py", "mouh/learning/records.py", "mouh/learning/recorder.py", "mouh/learning/memory.py",
        "mouh/learning/unique_generator.py", "jobs/train_unique.py",
        "mouh/learning/local_store.py", "mouh/learning/local_ingest.py", "mouh/learning/local_trainer.py",
        "mouh/learning/dashboard.py",
        "mouh/geometry/__init__.py", "mouh/geometry/mesh.py", "mouh/geometry/api.py", "mouh/geometry/sync.py"]
# Frontend VORTEX / SYMBIOZA 3D : tout l'arbre web/symbioza en fichiers texte (page, CSS, modules, three.js vendorisé).
# Seuls les suffixes texte sont hachés : l'empreinte est calculée en UTF-8, un binaire n'y entrerait pas.
WEB_DIR = "web/symbioza"
WEB_SUFFIXES = (".html", ".css", ".js", ".svg", ".json", ".txt")
# v4.1 : fichiers retirés (mode Jobs, maître, alimentation, export GGUF, installation à la volée, v3.14) ; l'updater
# les supprime du Space au moment de l'installation, et les rétablit en cas de retour arrière.
REMOVED = ["mouh/learning/autopilot.py", "jobs/teacher_tasks.py", "jobs/ingest_tasks.py", "unique_ext/autopilot.py",
           "unique_ext/install.py", "unique_ext/backends/gguf.py", "unique_ext/jobs/__init__.py",
           "unique_ext/jobs/export_gguf.py", "unique_ext/jobs/eval_generators.py",
           "unique_ext/remote/__init__.py", "unique_ext/remote/vllm.py", "unique_ext/remote/factory.py",
           "unique_ext/remote/replay.py", "unique_ext/control/__init__.py", "unique_ext/control/loop.py",
           "unique_ext/control/sources.py", "unique_ext/control/app.py", "tests/test_trispace.py",
           "docs/ADR-003-TRI-SPACE.md"]
EXTRA = ["tests/test_learning.py", "tests/test_ext.py", "tests/test_local.py", "scripts/build_release.py",
         "docs/ADR-004-LOCAL.md", "docs/SYMBIOZA.md", "tests/test_geometry.py", "tests/test_symbioza.py",
         "tests/test_selfint.py", "tests/test_sky_sync.py", "tests/test_frontend.py"]


def normalized_hash(text: str) -> str:   # identique à unique_ext.updater.normalized_hash
    text = text.replace("\r\n", "\n").lstrip(" \t")
    return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]


def _tree(rel_dir: str, suffixes: tuple[str, ...]) -> list[str]:
    return sorted(str(p.relative_to(ROOT)) for p in (ROOT / rel_dir).rglob("*")
                  if p.is_file() and "__pycache__" not in p.parts and p.suffix in suffixes)


def runtime_files() -> list[str]:
    ext = _tree("unique_ext", (".py", ".md"))
    web = _tree(WEB_DIR, WEB_SUFFIXES)
    return CORE + ext + web


def build_manifest(version: str) -> dict:
    return {"version": version, "files": {rel: normalized_hash((ROOT / rel).read_text(encoding="utf-8"))
                                          for rel in runtime_files()},
            "removed": [r for r in REMOVED if not (ROOT / r).exists()]}


def check() -> list[str]:
    man = json.loads((ROOT / MANIFEST).read_text(encoding="utf-8"))
    problems = [f"{rel} absent du manifest" for rel in runtime_files() if rel not in man["files"]]
    for rel, h in sorted(man["files"].items()):
        p = ROOT / rel
        if not p.exists():
            problems.append(f"{rel} manquant")
        elif normalized_hash(p.read_text(encoding="utf-8")) != h:
            problems.append(f"{rel} différent du manifest {man['version']}")
    return problems


def main(argv: list[str] | None = None) -> int:
    sys.path.insert(0, str(ROOT))
    import unique_ext

    major_minor = ".".join(unique_ext.__version__.split(".")[:2])
    ap = argparse.ArgumentParser()
    ap.add_argument("--version", default=f"v{major_minor}")
    ap.add_argument("--out", default=None, help="défaut : dist/unique-<version>.zip")
    ap.add_argument("--check", action="store_true")
    ap.add_argument("--since", default=None, help="manifest.json de la version en place : zip des seuls fichiers changés")
    a = ap.parse_args(argv)
    if a.check:
        problems = check()
        print("\n".join(problems) or "arbre conforme au manifest")
        return 1 if problems else 0
    man = build_manifest(a.version)
    (ROOT / MANIFEST).write_text(json.dumps(man, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    out = Path(a.out) if a.out else ROOT / "dist" / f"unique-{a.version}.zip"
    out.parent.mkdir(parents=True, exist_ok=True)
    names = sorted(set(runtime_files() + [MANIFEST] + [e for e in EXTRA if (ROOT / e).exists()]))
    if a.since:  # mise à jour : seulement ce qui a changé (l'updater vérifie que le reste est déjà en place)
        before = json.loads(Path(a.since).read_text(encoding="utf-8")).get("files", {})
        names = [n for n in names if n == MANIFEST or n in EXTRA or before.get(n) != man["files"].get(n)]
    with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
        for rel in names:
            z.write(ROOT / rel, rel)
    print(f"{out} : {len(names)} fichiers, version {a.version}")
    for rel in names:
        print(f"  {rel}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())