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()
|