From 6172bd737c4b83d99f69a04348dee7543c81cdac Mon Sep 17 00:00:00 2001 From: roger-sato Date: Sun, 18 Oct 2026 20:43:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20head=E5=BD=A2=E5=BC=8F=E3=81=AE?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=82=B9=E5=9B=BA=E5=AE=9A=E3=81=A7=E6=94=B9?= =?UTF-8?q?=E8=A1=8C=E5=85=A5=E3=82=8A=E3=83=91=E3=82=B9=E3=82=92=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/research-package-guide.md | 4 +- scripts/research_package.py | 3 +- tests/test_research_head_pin_paths.py | 140 ++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 tests/test_research_head_pin_paths.py diff --git a/docs/research/research-package-guide.md b/docs/research/research-package-guide.md index 1a70e63c75..1fb34ac02c 100644 --- a/docs/research/research-package-guide.md +++ b/docs/research/research-package-guide.md @@ -48,7 +48,9 @@ path can still be declared), never fewer. A row without an explicit `pin` resolves as `base_commit:`. A file vendored from a pull-request head can instead carry `"pin": "head:<40-hex sha>:"`, whose bytes resolve at that commit rather than at the package base. Any other -pin form fails closed. The originating task catalogue +pin form fails closed. Both forms preserve the exact path, including any LF or +CRLF characters in a Git filename; encode them as JSON escapes in the manifest. +The originating task catalogue `docs/research/agent-task-catalog.json` must stay pinned at `base_commit`, so a later catalogue revision cannot replace the card a package answers. Pins resolve only against Git objects reachable in this repository (the base commit or an diff --git a/scripts/research_package.py b/scripts/research_package.py index 404823640a..7ad9a3dcfc 100644 --- a/scripts/research_package.py +++ b/scripts/research_package.py @@ -259,7 +259,8 @@ def source_pin(row, base): return base + ":" + name if pin == "base_commit:" + name: return base + ":" + name - match = re.fullmatch(r"head:([0-9a-f]{40}):(.+)", pin) if isinstance(pin, str) else None + # Git filenames can contain LF; it is part of the exact path, not a delimiter. + match = re.fullmatch(r"head:([0-9a-f]{40}):(.+)", pin, re.DOTALL) if isinstance(pin, str) else None if match is not None and match.group(2) == name: return match.group(1) + ":" + name raise ValueError("SOURCES.json pin for " + name + " is not a supported pin form: " + brief(pin) diff --git a/tests/test_research_head_pin_paths.py b/tests/test_research_head_pin_paths.py new file mode 100644 index 0000000000..6287d6d017 --- /dev/null +++ b/tests/test_research_head_pin_paths.py @@ -0,0 +1,140 @@ +"""Exact head-pin filenames through the real metadata and publication CLIs. + +Builds on Aaron Wang's PR2737 head pins and Douglas Colkitt's package runner. +Prepared for roger-sato with substantial OpenAI Codex assistance. Apache-2.0. +""" +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'scripts')) +from research_package import source_pin +sys.path.pop(0) + + +class HeadPinPathTests(unittest.TestCase): + def fixture(self, name): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.package = 'research/fixture' + self.name = name + (self.root / 'scripts').mkdir() + for script in ('research_package.py', 'check_research_commit.py'): + shutil.copyfile(ROOT / 'scripts' / script, self.root / 'scripts' / script) + self.card = {'id': 'fixture', 'question': 'Check exact source paths'} + self.catalog = 'docs/research/agent-task-catalog.json' + self.save(self.catalog, {'tracks': [{'tasks': [self.card]}]}) + (self.root / name).parent.mkdir(parents=True, exist_ok=True) + (self.root / name).write_bytes(b'base input\n') + self.git('init', '-q') + self.git('config', 'user.name', 'Fixture') + self.git('config', 'user.email', 'fixture@example.invalid') + self.git('config', 'core.autocrlf', 'false') + self.commit('base') + self.base = self.git('rev-parse', 'HEAD').strip() + self.sources = {'schema_version': 1, 'base_commit': self.base, + 'files': [self.row(self.catalog, 'provenance'), self.row(name, 'input')]} + self.save(self.package + '/TASK.json', { + 'schema_version': 1, 'base_commit': self.base, 'task_id': 'fixture', + 'scope_status': 'unchanged', 'original_card': self.card, + 'actual_scope': self.card['question'], 'bottleneck_link': 'Tooling fixture', + 'stop_rule': 'Complete bounded fixture', 'evidence': 'Exact source pins', + 'author': 'Fixture', 'ai_assistance': 'Test fixture', 'outcome': 'partial', + 'open_questions': [], 'no_open_questions_reason': 'Bounded regression fixture'}) + self.save(self.package + '/VERIFY.json', { + 'schema_version': 1, 'status': 'ready', 'python': '3.12', 'inputs': [name], + 'commands': [{'argv': ['{python}', '-c', 'pass'], + 'checks': 'Fixture only', 'timeout_seconds': 1}]}) + (self.root / self.package / 'README.md').write_text('## Open next questions\n') + + def save(self, name, value): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value), encoding='utf-8') + + def git(self, *args): + return subprocess.check_output(['git', *args], cwd=self.root, + stderr=subprocess.PIPE, timeout=30).decode('utf-8') + + def commit(self, message): + self.git('add', '.') + self.git('commit', '-qm', message) + + def row(self, name, usage, commit=None): + commit = self.base if commit is None else commit + return {'path': name, 'usage': usage, + 'git_blob': self.git('rev-parse', commit + ':' + name).strip(), + 'sha256': hashlib.sha256((self.root / name).read_bytes()).hexdigest()} + + def cli(self, publication=False): + self.save(self.package + '/SOURCES.json', self.sources) + script = 'check_research_commit.py' if publication else 'research_package.py' + if publication: + self.commit('publication') + flags = ['-O'] if sys.flags.optimize else [] + return subprocess.run([sys.executable, *flags, '-B', str(self.root / 'scripts' / script), + '--package', self.package, + '--check-source-history' if publication else '--check-only'], + cwd=self.root, capture_output=True, text=True, timeout=30) + + @unittest.skipIf(os.name == 'nt', 'fixture creates POSIX filenames containing LF') + def test_base_and_head_pins_preserve_multiline_paths_in_both_clis(self): + for name in ('inputs/line\nbreak.txt', 'inputs/line\r\nbreak.txt', 'inputs/trailing\n'): + with self.subTest(name=name): + self.fixture(name) + for prefix in ('base_commit:', 'head:' + self.base + ':'): + self.sources['files'][1]['pin'] = prefix + name + for publication in (False, True): + result = self.cli(publication) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('PASS_RESEARCH_COMMIT_BYTES' if publication else + 'PASS_PACKAGE_METADATA_AND_PINS', result.stdout) + + @unittest.skipIf(os.name == 'nt', 'fixture creates POSIX filenames containing LF') + def test_later_head_is_resolved_and_all_source_checks_remain_active(self): + self.fixture('inputs/later\nhead.txt') + (self.root / self.name).write_bytes(b'later head input\n') + self.commit('later source') + head = self.git('rev-parse', 'HEAD').strip() + row = self.row(self.name, 'input', head) + row['pin'] = 'head:' + head + ':' + self.name + self.sources['files'][1] = row + for publication in (False, True): + result = self.cli(publication) + self.assertEqual(result.returncode, 0, result.stderr) + for field, value, message in ( + ('pin', 'base_commit:' + self.name, 'source blob provenance mismatch'), + ('git_blob', '0' * 40, 'source blob provenance mismatch'), + ('sha256', '0' * 64, 'source SHA256 provenance mismatch')): + with self.subTest(field=field): + self.sources['files'][1] = {**row, field: value} + result = self.cli() + self.assertNotEqual(result.returncode, 0) + self.assertIn(message, result.stderr) + self.sources['files'][1] = row + (self.root / self.name).write_bytes(b'working drift\n') + result = self.cli() + self.assertNotEqual(result.returncode, 0) + self.assertIn('working source drift', result.stderr) + + def test_malformed_head_pins_and_path_aliases_still_reject(self): + base, name = 'a' * 40, 'inputs/line\nbreak.txt' + for pin in ('head:' + base[:-1] + ':' + name, 'head:' + base + 'a:' + name, + 'head:' + base.upper() + ':' + name, 'head:\n' + base + ':' + name, + 'head:' + base + ':', 'head:' + base + ':' + name + '\n', + 'head:' + base + ':' + name.replace('\n', '\r\n'), + 'tag:' + base + ':' + name, 1, {}): + with self.subTest(pin=pin), self.assertRaisesRegex(ValueError, 'supported pin form'): + source_pin({'path': name, 'pin': pin}, base) + + +if __name__ == '__main__': + unittest.main() From cff9aac252b4bd9968a39c7cd0d12fce19fefab2 Mon Sep 17 00:00:00 2001 From: roger-sato Date: Sun, 18 Oct 2026 20:57:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=B1=A5=E6=AD=B4=E3=83=98=E3=83=AB?= =?UTF-8?q?=E3=83=91=E3=83=BC=E3=81=AE=E5=87=BA=E5=85=B8=E5=8C=BA=E5=88=86?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E9=9A=9B=E3=81=AE=E5=86=8D=E7=8F=BE=E7=B5=8C?= =?UTF-8?q?=E8=B7=AF=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../isolated-descendant-bytecode/README.md | 14 ++ .../isolated-descendant-bytecode/SOURCES.json | 2 +- .../test_research_descendant_source_roles.py | 149 ++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 tests/test_research_descendant_source_roles.py diff --git a/research/isolated-descendant-bytecode/README.md b/research/isolated-descendant-bytecode/README.md index d3d83682de..f6a84ab99d 100644 --- a/research/isolated-descendant-bytecode/README.md +++ b/research/isolated-descendant-bytecode/README.md @@ -79,6 +79,20 @@ pins are in [SOURCES.json](SOURCES.json). Canonical scientific base is runner is provenance; the manifest also declares the evolving live runner and regressions for replay selection. +The `research_package.py` source row is historical provenance: `verify.py` +loads that helper from the exact base-commit Git blob and checks its original +SHA256 before copying it into the differential fixture. Its base, blob and +SHA256 are unchanged. The original manifest is retained at commit +`20ecf2891d80b5acf18d980b2164fb44ea7faa90` (blob +`5e2d5560769ca754175472d123829a8586bb4ddc`). Classifying this row as a live +byte pin incorrectly prevented compatible changes to the current helper. +`VERIFY.json` still declares the current helper as an input for replay +selection, and its unchanged third command runs the eight current-helper +bytecode regressions. The sixteen-run historical differential, all three +registered commands, verifier and `RESULT.json` are retained unchanged. +This metadata correction was prepared for roger-sato with substantial OpenAI +Codex assistance; Arham Tahir's original work and source credit are retained. + ```sh python3.12 -B scripts/research_package.py --package research/isolated-descendant-bytecode --check-only python3.12 -B scripts/research_package.py --package research/isolated-descendant-bytecode diff --git a/research/isolated-descendant-bytecode/SOURCES.json b/research/isolated-descendant-bytecode/SOURCES.json index 5e2d556076..089b03a95a 100644 --- a/research/isolated-descendant-bytecode/SOURCES.json +++ b/research/isolated-descendant-bytecode/SOURCES.json @@ -22,7 +22,7 @@ }, { "path": "scripts/research_package.py", - "usage": "input", + "usage": "provenance", "git_blob": "404823640a4506cc363d5e999b724553a41a39a2", "sha256": "8e1fe8bebe7f045c82a672a103ed87a35cd9dfdbab5848b97969350449d0c8b4" }, diff --git a/tests/test_research_descendant_source_roles.py b/tests/test_research_descendant_source_roles.py new file mode 100644 index 0000000000..61378d20ee --- /dev/null +++ b/tests/test_research_descendant_source_roles.py @@ -0,0 +1,149 @@ +"""Historical helper authentication and current-helper replay remain separate. + +Arham Tahir's source-bound differential and registered commands are unchanged. +Prepared for roger-sato with substantial OpenAI Codex assistance. Apache-2.0. +""" +import copy +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +ORIGINAL = '20ecf2891d80b5acf18d980b2164fb44ea7faa90' +PACKAGE = 'research/isolated-descendant-bytecode' +HELPER = 'scripts/research_package.py' + + +def git(root, *args): + return subprocess.check_output(['git', '--no-replace-objects', *args], cwd=root, + stderr=subprocess.PIPE, timeout=30) + + +class DescendantSourceRoleTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.original = json.loads(git(ROOT, 'show', ORIGINAL + ':' + PACKAGE + '/SOURCES.json')) + self.manifest = json.loads((ROOT / PACKAGE / 'VERIFY.json').read_bytes()) + names = set(self.manifest['inputs']) | { + PACKAGE + '/' + name for name in + ('SOURCES.json', 'VERIFY.json', 'TASK.json', 'README.md', 'verify.py', 'RESULT.json')} + for name in names: + target = self.root / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / name, target) + git(self.root, 'init', '-q') + objects = Path(git(ROOT, 'rev-parse', '--git-path', 'objects').decode().strip()) + if not objects.is_absolute(): + objects = ROOT / objects + (self.root / '.git/objects/info/alternates').write_text(str(objects.resolve()) + '\n') + git(self.root, 'config', 'user.name', 'Fixture') + git(self.root, 'config', 'user.email', 'fixture@example.invalid') + git(self.root, 'config', 'core.autocrlf', 'false') + self.commit() + + def commit(self): + git(self.root, 'add', '.') + git(self.root, 'commit', '-qm', 'fixture') + + def write_sources(self, value): + (self.root / PACKAGE / 'SOURCES.json').write_text(json.dumps(value), encoding='utf-8') + + def cli(self, script='research_package.py', arguments=None): + if arguments is None: + arguments = ['--package', PACKAGE, '--check-only'] + flags = ['-O'] if sys.flags.optimize else [] + return subprocess.run([sys.executable, *flags, '-B', str(self.root / 'scripts' / script), + *arguments], cwd=self.root, capture_output=True, text=True, timeout=60) + + def test_only_source_role_changes_and_frozen_evidence_is_retained(self): + expected = copy.deepcopy(self.original) + row = next(row for row in expected['files'] if row['path'] == HELPER) + self.assertEqual(row['usage'], 'input') + row['usage'] = 'provenance' + self.assertEqual(json.loads((ROOT / PACKAGE / 'SOURCES.json').read_bytes()), expected) + for name in ('VERIFY.json', 'TASK.json', 'verify.py', 'RESULT.json'): + with self.subTest(name=name): + self.assertEqual((ROOT / PACKAGE / name).read_bytes(), + git(ROOT, 'show', ORIGINAL + ':' + PACKAGE + '/' + name)) + self.assertIn(HELPER, self.manifest['inputs']) + self.assertEqual(len(self.manifest['commands']), 3) + + def test_original_gate_rejects_but_correct_role_accepts_current_helper(self): + # The same harmless change demonstrates the old gate on any successor. + helper = self.root / HELPER + helper.write_bytes(helper.read_bytes() + b'\n# compatible fixture comment\n') + migrated = json.loads((self.root / PACKAGE / 'SOURCES.json').read_bytes()) + self.write_sources(self.original) + before = self.cli() + self.assertNotEqual(before.returncode, 0) + self.assertIn('working source drift: ' + HELPER, before.stderr) + self.write_sources(migrated) + after = self.cli() + self.assertEqual(after.returncode, 0, after.stderr) + self.assertIn('PASS_PACKAGE_METADATA_AND_PINS', after.stdout) + + def test_historical_blob_and_hash_corruption_still_rejects(self): + sources = json.loads((self.root / PACKAGE / 'SOURCES.json').read_bytes()) + for field, value, message in ( + ('git_blob', '0' * 40, 'source blob provenance mismatch'), + ('sha256', '0' * 64, 'source SHA256 provenance mismatch')): + with self.subTest(field=field): + corrupted = copy.deepcopy(sources) + next(row for row in corrupted['files'] if row['path'] == HELPER)[field] = value + self.write_sources(corrupted) + result = self.cli() + self.assertNotEqual(result.returncode, 0) + self.assertIn(message + ': ' + HELPER, result.stderr) + self.assertNotIn('PASS_PACKAGE', result.stdout) + + def test_unrelated_live_input_drift_still_rejects(self): + name = 'scripts/check_research_commit.py' + path = self.root / name + path.write_bytes(path.read_bytes() + b'\n# unpinned fixture change\n') + result = self.cli() + self.assertNotEqual(result.returncode, 0) + self.assertIn('working source drift: ' + name, result.stderr) + + def test_current_helper_remains_a_planner_and_publication_input(self): + base = git(self.root, 'rev-parse', 'HEAD').decode().strip() + helper = self.root / HELPER + helper.write_bytes(helper.read_bytes() + b'\n# current input change\n') + result = self.cli('check_research_commit.py', ['--package', PACKAGE]) + self.assertNotEqual(result.returncode, 0) + self.assertIn('working/commit bytes differ: ' + HELPER, result.stderr) + self.commit() + explanation = self.root / 'plan.json' + result = self.cli(arguments=['--changed-since', base, '--explain-plan', str(explanation)]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), {'include': [{'package': PACKAGE}]}) + selected = json.loads(explanation.read_bytes())['selected'] + self.assertEqual(selected[0]['causes']['declared_inputs'], [HELPER]) + + def test_broken_live_cache_isolation_fails_the_unchanged_registered_command(self): + helper = self.root / HELPER + source = helper.read_bytes() + protected = b'run_command(isolated_python_argv(argv, cache), cwd, timeout)' + self.assertEqual(source.count(protected), 1) + helper.write_bytes(source.replace(protected, b'run_command(argv, cwd, timeout)')) + command = self.manifest['commands'][2] + argv = [sys.executable if value == '{python}' else value.replace('{package}', PACKAGE) + for value in command['argv']] + env = os.environ.copy() + env.pop('PYTHONOPTIMIZE', None) + result = subprocess.run(argv, cwd=self.root, env=env, capture_output=True, text=True, + timeout=command['timeout_seconds']) + self.assertNotEqual(result.returncode, 0) + self.assertIn('Ran 8 tests', result.stderr) + self.assertIn('UNVERIFIED_BYTECODE_SENTINEL', result.stderr) + self.assertIn('FAILED', result.stderr) + + +if __name__ == '__main__': + unittest.main()