"""Stdlib/Git composition primitives; no runtime or acceptance execution. Re-expressed from P23 checker blob 8a62d1e1070688ced445e48d49b831fc7e9cfea3, SHA256 e41479ae51bccdfab3f31bd348794b441c2f341179bbe33222e2aacdf88f987f. Source filter matches run_all_tests.source_refs (reference blob 502ad8c1f6ba371446ecfd8f37f0f9194cdb10ea); no imports from Main-only modules. """ import contextlib import hashlib import json import os from pathlib import Path import re import subprocess import tempfile CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' SPEC = 'docs/spec/acceptance/P24-02-code-r01.json' RECONCILED_MAIN = '54cade4181f6c3c4f5701ba54ac5fcbeca695945' MAIN_INPUTS = { 'scenes/main/main.gd': 'fff37f082d750bb91c71f2aa54d168c9e2cb8fe7', 'scenes/main/world_preview.gd': 'fa32fe0344d20565b6d542da1256a94e8dee1845', CONTRACT: '2eecb803f5eb6774ff38cd353de1f02847aeb0f9', 'tools/regression/suite-policy.json': 'ad194ae425fb8123fa191ed3635f404c098f692d', } KNOWN_CONFLICTS = set(MAIN_INPUTS) OVERRIDES = {'GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_COMMON_DIR'} def require(ok, message): if not ok: raise ValueError(message) def clean_env(): # Config injection can also redirect Git. Do not inherit any Git overrides. return {k: v for k, v in os.environ.items() if not k.startswith('GIT_')} def git_process(root, *args, private=None, input=None): env = clean_env() if private: env.update(private) return subprocess.run(['git', '-C', str(root), *args], input=input, capture_output=True, env=env, timeout=120) def git_bytes(root, *args, private=None): p = git_process(root, *args, private=private) require(p.returncode == 0, 'Git failed: ' + ' '.join(args) + ': ' + p.stderr.decode(errors='replace')) return p.stdout def root_identity(script_root, cwd): root = Path(git_bytes(script_root, 'rev-parse', '--show-toplevel').decode().strip()).resolve() require(Path(git_bytes(cwd, 'rev-parse', '--show-toplevel').decode().strip()).resolve() == root, 'cwd differs from ROOT') head = git_bytes(root, 'rev-parse', 'HEAD').strip() if any(k in OVERRIDES or k.startswith('GIT_CEILING') for k in os.environ): inherited = subprocess.run(['git', '-C', str(root), 'rev-parse', 'HEAD'], capture_output=True, timeout=10) require(inherited.returncode == 0 and inherited.stdout.strip() == head, 'inherited Git override changes HEAD') for name in ('CANDIDATE_COMMIT', 'TARGET_HEAD'): require(name not in os.environ or os.environ[name].strip().encode() == head, name + ' mismatches HEAD') return root def strict_json(raw): def pairs(rows): d = {} for k, v in rows: require(k not in d, 'duplicate JSON key: ' + k) d[k] = v return d return json.loads(raw, object_pairs_hook=pairs) def safe_path(root, name): p = Path(name) require(not p.is_absolute() and '..' not in p.parts and p.as_posix() == name, 'unsafe relative path') target = root / p require(target.resolve().is_relative_to(root.resolve()), 'path escapes root') require(not any((root / Path(*p.parts[:i])).is_symlink() for i in range(1, len(p.parts)+1)), 'source/artifact symlink') return target def root_context(directory, index): path = safe_path(directory, 'root-composition.json') require(path.is_file(), 'missing regular root-composition.json') raw = path.read_bytes() require(hashlib.sha256(raw).hexdigest() == index.get('root_composition_sha256'), 'composition SHA mismatch') return strict_json(raw) def blob_oid(raw): return hashlib.sha1(b'blob %d\0' % len(raw) + raw).hexdigest() def git_blob_stream(root, oids): process = subprocess.Popen(['git', '-C', str(root), 'cat-file', '--batch'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=clean_env()) try: for oid in oids: require(re.fullmatch('[0-9a-f]{40}', oid), 'invalid blob OID') process.stdin.write((oid + '\n').encode()); process.stdin.flush() match = re.fullmatch(rb'([0-9a-f]{40}) blob (0|[1-9][0-9]*)\n', process.stdout.readline(128)) require(match is not None and match[1].decode() == oid, 'invalid blob header') raw = process.stdout.read(int(match[2])) require(len(raw) == int(match[2]) and process.stdout.read(1) == b'\n', 'truncated blob') require(blob_oid(raw) == oid, 'blob SHA1 mismatch') yield oid, raw process.stdin.close() require(process.stdout.read(1) == b'' and process.wait() == 0, 'extra blob output/Git failure') finally: if process.poll() is None: process.kill() process.wait() process.stdin.close(); process.stdout.close() def tree(root, revision): result = {} for row in git_bytes(root, 'ls-tree', '-r', '-z', revision).split(b'\0'): if not row: continue meta, path = row.split(b'\t', 1) path = path.decode() mode, kind, oid = meta.decode().split() require(path not in result, 'duplicate tree path') require(not Path(path).is_absolute() and '..' not in Path(path).parts, 'unsafe tree path') result[path] = (mode, kind, oid) return result def source_paths(manifest): folders = {'autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools'} fixed = {'project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md'} require(fixed <= set(manifest), 'missing fixed source settings') return {p for p in manifest if (Path(p).parts[0] in folders and Path(p).name != '.DS_Store' and '__pycache__' not in Path(p).parts)} | fixed @contextlib.contextmanager def tested_checkout(root, t): # Only a private writable allocation; never fall back to global temp. temporary = os.environ.get('TMPDIR') require(temporary and Path(temporary).is_dir(), 'existing task TMPDIR required') with tempfile.TemporaryDirectory(prefix='p24-view-', dir=temporary) as name: view = Path(name) manifest = tree(root, t) paths = sorted(manifest) for path, (_, raw) in zip(paths, git_blob_stream(root, [manifest[p][2] for p in paths])): require(manifest[path][0] in ('100644', '100755') and manifest[path][1] == 'blob', 'non-regular tested source') target = safe_path(view, path) target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(raw) yield view def deterministic_tree(root, m, s, main_inputs=MAIN_INPUTS): main, submitted = tree(root, m), tree(root, s) require(set(main_inputs) == KNOWN_CONFLICTS, 'reconciliation must bind all four inputs') for p, oid in main_inputs.items(): require(main.get(p, (None, None, None))[2] == oid, 'Main conflict input changed; new reconciliation required: ' + p) temporary = os.environ.get('TMPDIR') require(temporary and Path(temporary).is_dir(), 'existing task TMPDIR required') objects = Path(git_bytes(root, 'rev-parse', '--git-path', 'objects').decode().strip()) if not objects.is_absolute(): objects = root / objects with tempfile.TemporaryDirectory(prefix='p24-compose-', dir=temporary) as name: private = Path(name) (private / 'objects').mkdir() env = {'GIT_OBJECT_DIRECTORY': str(private / 'objects'), 'GIT_ALTERNATE_OBJECT_DIRECTORIES': str(objects.resolve()), 'GIT_INDEX_FILE': str(private / 'index')} result = git_process(root, 'merge-tree', '--write-tree', '-z', '--name-only', m, s, private=env) require(result.returncode == 1, 'expected exactly four content conflicts') fields = result.stdout.split(b'\0') merged = fields.pop(0).decode() require(re.fullmatch('[0-9a-f]{40}', merged), 'missing merge tree') conflict_paths = [] while fields and fields[0]: conflict_paths.append(fields.pop(0).decode()) require(fields and fields.pop(0) == b'', 'missing conflict separator') require(len(conflict_paths) == 4 and set(conflict_paths) == KNOWN_CONFLICTS, 'unexpected conflict set') # Structured merge-tree messages: path count, paths, type, message. seen = set() while fields and fields[0]: count = int(fields.pop(0)) paths = [fields.pop(0).decode() for _ in range(count)] kind = fields.pop(0).decode() message = fields.pop(0).decode() if kind == 'Auto-merging': continue require(kind == 'CONFLICT (contents)' and count == 1 and paths[0] in KNOWN_CONFLICTS and 'binary' not in message.lower(), 'unsupported conflict type: ' + kind) seen.update(paths) require(seen == KNOWN_CONFLICTS, 'missing content conflict messages') for p in KNOWN_CONFLICTS: require(main[p][0] == submitted[p][0] == '100644' and main[p][1] == submitted[p][1] == 'blob', 'mode/type conflict') git_bytes(root, 'read-tree', merged, private=env) for p in sorted(KNOWN_CONFLICTS): mode, _, oid = submitted[p] git_bytes(root, 'update-index', '--add', '--cacheinfo', mode, oid, p, private=env) return git_bytes(root, 'write-tree', private=env).decode().strip() def verify_composition(root, roles, source_map, inputs, base, allowed_paths, main_inputs=MAIN_INPUTS): require(isinstance(roles, dict), 'missing composition roles') for k in ('S', 'M', 'Q', 'T'): require(isinstance(roles.get(k), str) and re.fullmatch('[0-9a-f]{40}', roles[k]), 'invalid role ' + k) s, m, q, t = (roles[k] for k in ('S', 'M', 'Q', 'T')) for c in (base, s, m, q): require(git_bytes(root, 'cat-file', '-t', c).strip() == b'commit', 'role is not commit') require(git_bytes(root, 'rev-list', '--parents', '-n', '1', q).decode().split() == [q, m, s], 'Q parents must be exactly current [M,S]') require(git_bytes(root, 'rev-parse', q+'^{tree}').decode().strip() == t, 'T differs from Q tree') head = git_bytes(root, 'rev-parse', 'HEAD').decode().strip() if roles.get('mode') == 'normal_validation': require(head == s, 'normal validation HEAD must be S') elif roles.get('mode') == 'integration_final': require(roles.get('C') == head, 'final HEAD must be C') require(git_bytes(root, 'rev-list', '--parents', '-n', '1', head).decode().split() == [head, m, s], 'C parents must be exactly [M,S]') require(git_bytes(root, 'rev-parse', head+'^{tree}').decode().strip() == t, 'C tree differs') require(not git_bytes(root, 'status', '--porcelain'), 'final checkout dirty') else: raise ValueError('invalid composition mode') require(deterministic_tree(root, m, s, main_inputs) == t, 'Q/T differs from deterministic composition') b, st, mt, tt = (tree(root, rev) for rev in (base, s, m, t)) changed_s = {p for p in b.keys() | st.keys() if b.get(p) != st.get(p)} changed_m = {p for p in b.keys() | mt.keys() if b.get(p) != mt.get(p)} def allowed(p): return any(p.startswith(a[:-2]) if a.endswith('/**') else p == a for a in allowed_paths) require(all(allowed(p) for p in changed_s), 'S changed outside allowed_paths') for paths, authority in ((changed_s-changed_m, st), (changed_m-changed_s, mt), (KNOWN_CONFLICTS, st)): require(all(tt.get(p) == authority.get(p) for p in paths), 'S/M-only or conflict provenance mismatch') require(isinstance(source_map, dict) and set(source_map) == source_paths(tt), 'incomplete source_refs map') require(isinstance(inputs, dict) and {SPEC, CONTRACT, 'project.godot', 'tools/regression/suite-policy.json'} <= set(inputs), 'missing settings/spec/marker/policy input pins') required = set(source_map) | set(inputs) bindings = roles.get('blob_bindings') require(isinstance(bindings, dict) and set(bindings) == required, 'missing/extra blob_bindings') paths = sorted(required) for p in paths: require(p in tt and tt[p][0] in ('100644', '100755') and tt[p][1] == 'blob', 'not regular bound blob') require(bindings[p] == tt[p][2], 'changed blob binding: ' + p) for p, (oid, raw) in zip(paths, git_blob_stream(root, [tt[p][2] for p in paths])): digest = hashlib.sha256(raw).hexdigest() require(blob_oid(raw) == bindings[p] == oid, 'SHA1 binding mismatch') for mapping in (source_map, inputs): require(p not in mapping or mapping[p] == digest, 'SHA256 binding mismatch: ' + p) if head != s or p in changed_s-changed_m or p in KNOWN_CONFLICTS: require(safe_path(root, p).read_bytes() == raw, 'checkout bytes mismatch: ' + p) return roles