File size: 6,406 Bytes
29f25be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
116
117
118
"""Archive Mac project snapshots, experiment evidence and Vime client for local handoff."""
import argparse
import hashlib
import json
import subprocess
import zipfile
from pathlib import Path
from datetime import date


SKIP_PARTS = {'.git', '__pycache__', '.pytest_cache', '.build', '.swiftpm', 'xcuserdata'}
SKIP_NAMES = {'.DS_Store'}


def digest(source):
    if isinstance(source, bytes):
        return hashlib.sha256(source).hexdigest()
    with source.open('rb') as stream:
        return hashlib.file_digest(stream, 'sha256').hexdigest()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--project', type=Path, required=True)
    parser.add_argument('--client', type=Path, required=True)
    parser.add_argument('--output', type=Path, required=True)
    args = parser.parse_args()
    root, client, out = args.project.resolve(), args.client.resolve(), args.output.resolve()
    if out.exists():
        raise SystemExit('Output already exists; use a new handoff version.')
    out.mkdir(parents=True)
    manifest = {'format': 'vimeml_mac_handoff_v1', 'date': date.today().isoformat(),
                'vime_git_head': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=client, text=True).strip(),
                'vime_git_status': subprocess.check_output(['git', 'status', '--short'], cwd=client, text=True),
                'archives': {}, 'excluded': [],
                'policy': 'Local transfer only; snapshots are not automatic merge instructions or Git commits.'}

    def entries(base, roots, prefix):
        files = []
        for name in roots:
            source = base / name
            if not source.exists():
                continue
            paths = [source] if source.is_file() else sorted(source.rglob('*'))
            for path in paths:
                relative = path.relative_to(base)
                if not path.is_file():
                    continue
                if (SKIP_PARTS.intersection(relative.parts) or path.name in SKIP_NAMES or path.suffix == '.pyc'
                        or path.name == '.env' or path.name.startswith('.env.')
                        or path.name in {'.pypirc', '.envrc'}):
                    manifest['excluded'].append(str(Path(prefix) / relative))
                    continue
                if path.is_symlink():
                    raise SystemExit(f'Unexpected symlink requires review: {path}')
                files.append(((Path(prefix) / relative).as_posix(), path))
        return files

    def archive(name, files):
        indexed = []
        with zipfile.ZipFile(out / name, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=3) as zip_file:
            for relative, data in files:
                value = digest(data)
                if isinstance(data, bytes):
                    zip_file.writestr(relative, data)
                    size = len(data)
                else:
                    zip_file.write(data, relative)
                    size = data.stat().st_size
                indexed.append({'path': relative, 'bytes': size, 'sha256': value})
        # Verify every entry against its source hash, including models and raw trace files.
        with zipfile.ZipFile(out / name) as zip_file:
            for entry in indexed:
                with zip_file.open(entry['path']) as stream:
                    value = hashlib.file_digest(stream, 'sha256').hexdigest()
                if value != entry['sha256']:
                    raise SystemExit(f'Archive verification failed: {entry["path"]}')
        manifest['archives'][name] = {'file_count': len(indexed),
                                     'uncompressed_bytes': sum(e['bytes'] for e in indexed),
                                     'archive_bytes': (out / name).stat().st_size,
                                     'entries': indexed}
        print(f'{name}: {len(indexed)} files, {(out / name).stat().st_size / 1048576:.1f} MiB; SHA256 verified', flush=True)

    source_roots = ['annotations', 'configs', 'docs', 'examples', 'scripts', 'src', 'tests']
    source_roots += [p.name for p in root.iterdir() if p.is_file() and p.name not in SKIP_NAMES]
    archive('VimeML-source.zip', entries(root, source_roots, 'VimeML'))
    results_roots = ['outputs', 'runs'] + [str(p.relative_to(root)) for p in (root / 'artifacts').iterdir()
                                          if p.name not in {'models', 'token-data'}]
    archive('VimeML-results.zip', entries(root, results_roots, 'VimeML'))
    archive('VimeML-training-inputs.zip', entries(root, ['artifacts/models', 'artifacts/token-data'], 'VimeML'))

    client_paths = subprocess.check_output(['git', 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd=client).decode().split('\0')
    client_entries = entries(client, sorted(set(p for p in client_paths if p)), 'Vime')
    patch = subprocess.check_output(['git', 'diff', '--binary', 'HEAD', '--', '.', ':(exclude)**/xcuserdata/**'], cwd=client)
    client_entries += [('CLIENT_TRACKED_CHANGES.patch', patch),
                       ('CLIENT_GIT_STATUS.txt', manifest['vime_git_status'].encode()),
                       ('CLIENT_GIT_HEAD.txt', (manifest['vime_git_head'] + '\n').encode()),
                       ('Vime/Docs/MacStageHandoff.md', (root / 'docs/reports/mac-20261006/handoff.md').read_bytes()),
                       ('Vime/Docs/iPhoneKeyboardMemory.md', (root / 'docs/reports/mac-20261006/iphone-keyboard-memory.md').read_bytes())]
    archive('Vime-client.zip', client_entries)
    manifest['not_archived_roots'] = ['VimeML/venv (rebuild from requirements and environment snapshot)',
                                      'VimeML/handoff (this generated output)',
                                      'Vime/.git (base commit and patch preserved instead)']
    (out / 'README.md').write_bytes((root / 'docs/reports/mac-20261006/handoff.md').read_bytes())
    (out / 'MANIFEST.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n')
    sums = []
    for path in sorted(out.iterdir()):
        if path.is_file():
            with path.open('rb') as stream:
                value = hashlib.file_digest(stream, 'sha256').hexdigest()
            sums.append(f'{value}  {path.name}')
    (out / 'SHA256SUMS.txt').write_text('\n'.join(sums) + '\n')
    print(f'Handoff ready: {out}', flush=True)


if __name__ == '__main__':
    main()