P04-01: 同時発生の更新規約

This commit is contained in:
Agent Control Runner
2026-10-16 06:46:41 +09:00
parent b543c26c95
commit 16207d353a
3 changed files with 236 additions and 2 deletions
@@ -0,0 +1,95 @@
# Repair21 — source submission; tests NOTRUN
P04-01 source-only performance repair. Changes are restricted to the checker,
the new `bulk_reader_equivalence.py` fixture, and this status file. No Git
command, test, fixture, collector, Runner, native/Godot run, agent, commit,
branch or integration operation was executed by the worker.
`read_blobs` validates full lowercase 40-hex OIDs before starting a process,
deduplicates/sorts them, and uses one `git cat-file --batch` process per nonempty
request. Empty requests start no process. `check_output` inherits stderr and
raises for nonzero exit status just like `git_bytes`. The separately injectable
byte parser verifies ordered OIDs, blob type, decimal size, exact framing and
complete output consumption. There is deliberately no blob cache, so no object
can be reused across `_GIT_ENV` namespaces. The two bulk call sites preserve
selected-path sorted manifests and every actual-file byte/mode comparison.
No existing require message or assertion was removed. The `NATIVE_RUNS` through
`verify_native` region, commit-type checks, ancestry, merge-tree isolation,
status/untracked/symlink/module checks, schema/encoder, six existing suites and
role case IDs were not edited. Checker self-blob reads remain unchanged.
The fixture source covers binary NUL/CR/LF/trailing LF, empty and large blobs,
unicode `ls-tree -z` path resolution, duplicates, empty input, malformed/short
OIDs, missing objects, tree/commit objects, invalid/truncated/oversized/garbage
responses, process failure, one-process sorted input, and private object
visibility inside versus outside and across isolated namespaces. It has not run.
Static validation: `ast.parse` and in-memory `compile(..., 'exec')` completed for
the checker and new fixture. Neither module was imported or executed. This is
syntax validation only. Tests: **NOTRUN**. Runtime equivalence and completion
within the registered 180 seconds remain for Root to measure.
The existing schema2 index at B/p04-evidence/index.json was read as data, along
with the existing builder and role fixture. Its registered/execution/tested
roles remain deb1bcb/9767b0a/5f6bc11 and original_credit remains 13/88.
The existing index and r20 collection are untouched. No new generation is
needed if Root confirms the recomputed payload is identical. If generation is
needed, use a new explicit destination, never overwrite existing evidence.
Original P04-01 remains incomplete; criteria and nativeWindows pending status
are unchanged. No original credit or acceptance is claimed.
## Root execution order — all NOTRUN
Use the clean new auto-submitted checkout, whose SHA is assigned after this
source submission. Do not use the registered base SHA as the new submission.
```sh
export B=/Users/korizatou114/Projects/TheTree-managed-worktrees/p04-01/.runtime/P04-01/build
export E=docs/plans/original-completion-2026-10-02/execution/p04-01
export BUILD_OUTPUT_DIR="$B"
export PYTHONDONTWRITEBYTECODE=1
python3 "$E/bulk_reader_equivalence.py"
python3 "$E/checker_negative_fixtures.py"
python3 "$E/checker_applicability_fixtures.py" --native-bundle "$B/audit-support/native-first-current"
python3 "$E/checker_native_fixtures.py" "$B/audit-support/native-first-current"
python3 "$E/checker_series_fixtures.py" --build "$B"
python3 "$E/checker_encoder_fixtures.py"
python3 "$E/checker_join_fixtures.py" --build "$B"
python3 "$E/checker_role_fixtures.py" --build "$B" --evidence "$B/p04-evidence"
```
Next run the registered validation on the **new auto-submitted SHA**, retaining
its existing 180-second timeout and actual timed receipt. The supplied formal
API call is exactly:
```python
runner.validate("P04-01")
```
The external Runner launcher/configuration is not present in the supplied task
context; use Root's already registered invocation, without substituting an
invented CLI, changing timeout, or treating direct checker timing as registered
validation. Record actual SHA, elapsed time and exit status from that invocation.
Then run direct real-candidate controls from actual clean submitted, merged and
final checkouts, preserving the formal review/integration state machine:
```sh
python3 tools/check_p04_order_evidence.py --verify-only
python3 "$P04_MERGED_CHECKOUT/tools/check_p04_order_evidence.py" --verify-only
python3 "$P04_FINAL_CHECKOUT/tools/check_p04_order_evidence.py" --verify-only
```
After required formal validation/review and exact final-source binding, run the
full178 product regression once, using a fresh output destination:
```sh
python3 "$P04_FINAL_CHECKOUT/tools/run_all_tests.py" \
--project-root "$P04_FINAL_CHECKOUT" --output "$B/p04-final-product-full-r21"
```
Do not regenerate raw collections, overwrite index/r20 evidence, alter existing
case semantics, or run an additional native/full suite to compensate for a
timeout. Any unrelated improvement is deferred. Pending Root execution is not
a worker specification gap and does not change this submission's scope.
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Root-only disposable repository controls; never original acceptance evidence."""
import importlib.util
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from unittest.mock import patch
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[5]
def rejected(operation):
try:
operation()
except (ValueError, subprocess.CalledProcessError):
return
raise AssertionError('invalid input/response accepted')
def run():
spec = importlib.util.spec_from_file_location('bulk_gate', ROOT / 'tools/check_p04_order_evidence.py')
gate = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gate)
with tempfile.TemporaryDirectory(prefix='p04-bulk-fixture-') as temporary:
base = Path(temporary)
repo = base / 'repo'
repo.mkdir()
gate.ROOT = repo
env = gate.git_environment()
env.update(GIT_AUTHOR_NAME='fixture', GIT_AUTHOR_EMAIL='fixture@invalid',
GIT_COMMITTER_NAME='fixture', GIT_COMMITTER_EMAIL='fixture@invalid')
def git(*args, data=None):
return subprocess.check_output(['git', '-c', 'core.hooksPath=/dev/null', *args],
input=data, cwd=repo, env=env)
git('init', '--quiet')
payloads = [b'\x00binary\r\nline\n', b'', bytes(range(256)) * 32768]
oids = [git('hash-object', '-w', '--stdin', data=data).decode().strip() for data in payloads]
name = '日本語の木.dat'
(repo / name).write_bytes(payloads[0])
git('add', '--', name)
git('commit', '--quiet', '-m', 'fixture')
entries = gate.tree_entries('HEAD') # ls-tree -z resolves the unicode path.
assert entries[name][1] == oids[0]
expected = {oid: git('cat-file', 'blob', oid) for oid in oids}
original_output = subprocess.check_output
with patch.object(gate.subprocess, 'check_output', wraps=original_output) as process:
assert gate.read_blobs(reversed(oids + [oids[0]])) == expected
assert process.call_count == 1
assert process.call_args.kwargs['input'] == ('\n'.join(sorted(set(oids))) + '\n').encode()
with patch.object(gate.subprocess, 'check_output', side_effect=AssertionError('process started')):
assert gate.read_blobs([]) == {}
for invalid in ('abc', 'g' * 40, 'a' * 39, 'a' * 41, 'HEAD', oids[0] + '\n', None):
rejected(lambda: gate.read_blobs([invalid]))
for oid in ('0' * 40, git('rev-parse', 'HEAD').decode().strip(),
git('rev-parse', 'HEAD^{tree}').decode().strip()):
rejected(lambda: gate.read_blobs([oid]))
oid = oids[0]
header = oid.encode() + b' blob '
valid = header + b'3\na\x00\n\n'
assert gate.parse_blob_batch(valid, [oid]) == {oid: b'a\x00\n'}
bad = [b'', header + b'3', header + b'3\na', valid[:-1],
header + b'999\na\n', header + b'1\nabc\n', valid + b'garbage',
oid.encode() + b' missing\n', oid.encode() + b' ambiguous\n',
header + b'-1\n', header + b'x\n', header + b'1.0\n',
b'f' * 40 + b' blob 0\n\n', oid.encode() + b' tree 0\n\n']
for response in bad:
rejected(lambda: gate.parse_blob_batch(response, [oid]))
with patch.object(gate.subprocess, 'check_output', return_value=response):
rejected(lambda: gate.read_blobs([oid]))
rejected(lambda: gate.parse_blob_batch(valid + valid, [oid]))
rejected(lambda: gate.parse_blob_batch(valid, [oid, oids[1]]))
with patch.object(gate.subprocess, 'check_output',
side_effect=subprocess.CalledProcessError(7, ['git'])):
rejected(lambda: gate.read_blobs([oid]))
with patch.dict(os.environ, BUILD_OUTPUT_DIR=str(base)):
with gate.isolated_objects():
private_data = b'isolated-only unique payload\x00\n'
private = subprocess.check_output(
['git', 'hash-object', '-w', '--stdin'], input=private_data,
cwd=repo, env=gate._GIT_ENV).decode().strip()
assert gate.read_blobs([private]) == {private: private_data}
rejected(lambda: gate.read_blobs([private]))
with gate.isolated_objects():
rejected(lambda: gate.read_blobs([private]))
assert gate.read_blobs(oids) == expected
assert gate._GIT_ENV is None
print('bulk reader equivalence controls completed')
if __name__ == '__main__':
run()
+43 -2
View File
@@ -141,6 +141,44 @@ def git_bytes(*args):
return subprocess.check_output(['git', '--no-replace-objects', *args], cwd=ROOT, env=env)
def parse_blob_batch(stdout, oids):
"""Decode length-framed output without splitting binary payloads into lines."""
result = {}
offset = 0
for oid in oids:
end = stdout.find(b'\n', offset)
require(end >= 0, 'truncated blob batch header')
header = stdout[offset:end].split(b' ')
require(len(header) == 3 and header[0] == oid.encode('ascii')
and header[1] == b'blob' and re.fullmatch(rb'[0-9]+', header[2]) is not None,
'invalid blob batch header: ' + oid)
size = int(header[2])
start = end + 1
end = start + size
require(end < len(stdout) and stdout[end:end + 1] == b'\n',
'truncated blob batch payload or missing LF: ' + oid)
result[oid] = stdout[start:end]
offset = end + 1
require(offset == len(stdout), 'extra blob batch output')
return result
def read_blobs(oids):
"""One process per request; no cache can cross object namespaces."""
requested = list(oids)
require(all(isinstance(oid, str) and re.fullmatch('[0-9a-f]{40}', oid) is not None
for oid in requested), 'invalid full blob OID')
requested = sorted(set(requested))
if not requested:
return {}
env = _GIT_ENV if _GIT_ENV is not None else git_environment()
# Inherit stderr and raise on nonzero status, exactly as git_bytes does.
stdout = subprocess.check_output(['git', '--no-replace-objects', 'cat-file', '--batch'],
input=('\n'.join(requested) + '\n').encode('ascii'),
cwd=ROOT, env=env)
return parse_blob_batch(stdout, requested)
def git_environment():
env = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')}
env.update(GIT_OPTIONAL_LOCKS='0', GIT_NO_REPLACE_OBJECTS='1', GIT_CONFIG_NOSYSTEM='1',
@@ -197,9 +235,11 @@ def allowed_path(path):
def product_manifest(entries):
result = {}
blobs = read_blobs(oid for path, (_, oid) in entries.items()
if selected_path(path) and path != CHECKER_PATH)
for path, (_, oid) in sorted(entries.items()):
if selected_path(path) and path != CHECKER_PATH:
data = git_bytes('cat-file', 'blob', oid)
data = blobs[oid]
result[path] = {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
return result
@@ -337,9 +377,10 @@ def actual_checkout():
require(not git_bytes('status', '--porcelain=v1', '--untracked-files=all',
'--ignore-submodules=none'), 'checkout is not clean')
require(not git_bytes('ls-files', '--others', '-z'), 'untracked/ignored substitution')
blobs = read_blobs(oid for _, oid in entries.values())
for path, (mode, oid) in entries.items():
actual = safe(ROOT, path)
require(actual.read_bytes() == git_bytes('cat-file', 'blob', oid), 'HEAD/tree/FS bytes: ' + path)
require(actual.read_bytes() == blobs[oid], 'HEAD/tree/FS bytes: ' + path)
require(('100755' if actual.stat().st_mode & 0o111 else '100644') == mode,
'HEAD/tree/FS mode: ' + path)
# Also reject untracked directory symlinks, including ones ignored by Git.