Files
TheTree_mock/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py
T

263 lines
13 KiB
Python

"""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