Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions MANIFEST.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"hash_algorithm": "SHA-256",
"manifest_self_excluded": true,
"source_only": true,
"file_count": 709,
"file_count": 710,
"files": [
{
"path": ".github/workflows/test.yml",
Expand Down Expand Up @@ -747,8 +747,8 @@
},
{
"path": "docs/paper_qualification_v1.md",
"bytes": 17392,
"sha256": "d45717c9b667356beb85f69b6f5765c0d3f4440cf1412d48b993c43e469c63cf"
"bytes": 18238,
"sha256": "4ee585115505b6adc0890c1c25e72431529fb66d4b1e1e7b35b296de8b41c40d"
},
{
"path": "docs/paper_relevance_packets.md",
Expand Down Expand Up @@ -2312,8 +2312,8 @@
},
{
"path": "src/paper_qualification_v1.py",
"bytes": 86186,
"sha256": "18438e75da7d230e23863f541ca681f393f55e7e84c8bbc9170ed397a7edb327"
"bytes": 86234,
"sha256": "8b17c4c41fe9f8dbe37cfba6395604fadd3c5231d497888376d0f902a2f545be"
},
{
"path": "src/paper_relevance_packets.py",
Expand Down Expand Up @@ -2362,8 +2362,8 @@
},
{
"path": "src/parallel_source_v4/common.py",
"bytes": 7156,
"sha256": "61dc7718ea930175da49b45de6a30a64a67b4c24deb68c6700c52f6668e77c1d"
"bytes": 8034,
"sha256": "799f09c06a706a98e90b5f6e150d86f76004fb8b1abff8ae78f425fbe7f0de2e"
},
{
"path": "src/parallel_source_v4/extraction.py",
Expand Down Expand Up @@ -2590,6 +2590,11 @@
"bytes": 5407,
"sha256": "72fdd0bb250e88f05b37b8260acf507bf07b128803737d58cae6aeac069bf4d4"
},
{
"path": "tests/test_paper_evaluator_identity.py",
"bytes": 4735,
"sha256": "d90950ce0cbe541329fb4ab7607930d8010686c185ed0f2ccaf3d72eeb2f012b"
},
{
"path": "tests/test_paper_ingestion.py",
"bytes": 3741,
Expand Down
13 changes: 13 additions & 0 deletions docs/paper_qualification_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ cannot override these checks. The two primary bounds have at least 95% joint
coverage under the preregistered binomial model. The existing exact binomial
implementation is reused and its file is included in the evaluator identity.

The evaluator identity now expands the evaluator's own static local import
closure, including the qualification module's transitive helpers. Changing a
helper therefore changes the exact code map even when the evaluator entry file
is unchanged. Earlier code maps and receipts remain historical evidence; they
are not rebound to the new map. Evaluation requires a freshly approved
preregistration and execution identity matching the current source.

This is a static source identity, not an isolated execution loader. It does not
establish that dynamic imports, module caches, bytecode or external verifier
releases execute those exact bytes. The separate external-verifier closure gate
in issue #97 remains HOLD until those boundaries and its real prerequisites are
verified. Statistical criteria and the evaluation record schema are unchanged.

Primary selection takes the first 400 eligible recent work IDs by the frozen
`primary` rank. The next 600 form the recent screen; 600 historical works use
`historical-screen`. All 1,200 screen outcomes must exist. Source-only tags select
Expand Down
2 changes: 1 addition & 1 deletion src/paper_qualification_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ def _image_candidate(store, value, *, source, selected, original, reference, cod


def evaluator_identity():
code = method_hashes()
code = method_hashes(additional_roots=("src.paper_qualification_v1",))
path = Path(__file__).resolve()
code[path.relative_to(path.parents[1]).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
statistics = path.parents[1]/"trace_gc/qualification.py"
Expand Down
15 changes: 14 additions & 1 deletion src/parallel_source_v4/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,29 @@ def write_once(path: Path, value: object) -> str:
return hashlib.sha256(payload).hexdigest()


def method_hashes(repo: Path = REPO) -> dict:
def method_hashes(repo: Path = REPO, *, additional_roots: tuple[str, ...] = ()) -> dict:
"""Hash local import closure, including the dynamically loaded frozen replay.

Resolve imports statically so recording code never imports a worker/model or
adopts its import-time data root. All v4 modules are seeds, including newly
added metric modules. Function-local local imports are included as well.
Callers outside that default family must name their entry modules in
``additional_roots`` so their own transitive imports are included. Explicit
roots must exist locally; unresolved ordinary imports may be dependencies
supplied by the separately recorded runtime.
"""
seeds = ["trace_gc.pdf_source_parallel_v4", "trace_gc.pdf_structure_parallel_v4",
"src.abstract_validation_expanded.extraction"]
seeds += ["src.parallel_source_v4."+p.stem for p in (repo/"src/parallel_source_v4").glob("*.py")]
if not isinstance(additional_roots, (tuple, list)):
raise ValueError("expected_local_module_roots")
for module in additional_roots:
if not isinstance(module, str) or not all(part.isidentifier() for part in module.split(".")):
raise ValueError("expected_local_module_root")
target = repo.joinpath(*module.split("."))
if not target.with_suffix(".py").is_file() and not (target/"__init__.py").is_file():
raise ValueError("missing_local_module_root:"+module)
seeds += list(additional_roots)
pending, visited, snapshots = list(seeds), set(), {}
while pending:
module = pending.pop()
Expand Down
89 changes: 89 additions & 0 deletions tests/test_paper_evaluator_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Public source identity regressions; these are not empirical qualification."""
import hashlib
from pathlib import Path
import shutil
import tempfile
import unittest
from unittest.mock import patch

from src import paper_qualification_v1 as evaluator
from src.parallel_source_v4.common import REPO, method_hashes


class AdditionalRootTests(unittest.TestCase):
def test_default_roots_do_not_adopt_unrequested_entrypoint(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed = root / "src/parallel_source_v4/metric.py"
seed.parent.mkdir(parents=True)
seed.write_text("VALUE = 1\n", encoding="utf-8")
extra = root / "authored_verifier.py"
extra.write_text("VALUE = 'HOLD'\n", encoding="utf-8")
before = method_hashes(root)
self.assertEqual(before, method_hashes(root, additional_roots=()))
extra.write_text("VALUE = 'PASS'\n", encoding="utf-8")
self.assertEqual(before, method_hashes(root))
self.assertNotIn("authored_verifier.py", before)
selected = method_hashes(root, additional_roots=("authored_verifier",))
self.assertEqual(set(selected) - set(before), {"authored_verifier.py"})

def test_explicit_roots_fail_closed_when_missing_or_invalid(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for module in ("absent.verifier", "../outside", "", "os.path"):
with self.subTest(module=module), self.assertRaises(ValueError):
method_hashes(root, additional_roots=(module,))

def test_scan_includes_packages_and_local_imports_without_execution(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
sources = {
"authored_verifier/__init__.py": "raise RuntimeError('must not import package')\n",
"authored_verifier/entry.py": (
"from . import helper\n"
"def evaluate():\n from .nested import result\n"
"raise RuntimeError('must not execute verifier')\n"),
"authored_verifier/helper.py": "raise RuntimeError('must not import helper')\n",
"authored_verifier/nested.py": "raise RuntimeError('must not import nested helper')\n",
}
for relative, source in sources.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(source, encoding="utf-8")
hashes = method_hashes(root, additional_roots=("authored_verifier.entry",))
self.assertEqual(set(hashes), set(sources))
self.assertEqual(hashes, {
name: hashlib.sha256(source.encode()).hexdigest() for name, source in sources.items()})


class EvaluatorClosureTests(unittest.TestCase):
def test_real_qualifier_helpers_are_bound_without_entrypoint_changes(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for directory in ("src", "trace_gc"):
shutil.copytree(REPO / directory, root / directory,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
entry = root / "src/paper_qualification_v1.py"
with patch.object(evaluator, "__file__", str(entry)), patch.object(
evaluator, "method_hashes", side_effect=lambda **kwargs: method_hashes(root, **kwargs)):
before = evaluator.evaluator_identity()
helpers = ("trace_gc/constraints.py", "trace_gc/plans.py",
"trace_gc/policy.py", "trace_gc/resolver.py")
self.assertTrue(set(helpers) <= set(before))
self.assertIn("src/paper_qualification_v1.py", before)
for helper in helpers:
path = root / helper
original = path.read_bytes()
try:
path.write_bytes(original + b"\nraise RuntimeError('authored unexecuted mutation')\n")
after = evaluator.evaluator_identity()
self.assertEqual(before["src/paper_qualification_v1.py"],
after["src/paper_qualification_v1.py"])
self.assertNotEqual(before[helper], after[helper])
self.assertEqual({name for name in before if before[name] != after[name]}, {helper})
finally:
path.write_bytes(original)


if __name__ == "__main__":
unittest.main()
Loading