From 030f47d19ff6336b4a54ef8ccddac8b6bb85b1c5 Mon Sep 17 00:00:00 2001 From: psychologister <230141834+psychologister@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:24:17 +0800 Subject: [PATCH 1/2] reward_memory: single-source reference/digest shapes and recall modes utility_reducer borrows memory_utility's matchers; experiment borrows application's recall modes. Signed-off-by: psychologister <230141834+psychologister@users.noreply.github.com> --- .../capabilities/reward_memory/experiment.py | 9 ++- .../reward_memory/utility_reducer.py | 8 +- ...st_reward_memory_field_vocabulary_owner.py | 76 ++++++++++++++++++- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/loopx/capabilities/reward_memory/experiment.py b/loopx/capabilities/reward_memory/experiment.py index 2206941978..8ec03f4945 100644 --- a/loopx/capabilities/reward_memory/experiment.py +++ b/loopx/capabilities/reward_memory/experiment.py @@ -21,7 +21,11 @@ from ..context_providers import build_context_provider from ..context_providers.base import ContextProvider from ..context_providers.openviking import classify_openviking_scope -from .application import TOKEN_RE, normalize_reward_memory_provider_binding +from .application import ( + RECALL_MODES, + TOKEN_RE, + normalize_reward_memory_provider_binding, +) from .ingestion import normalize_reward_memory_standing_policy from .registry import MAX_CORPORA, normalize_reward_memory_corpus from .scoped_feedback import SCOPED_FEEDBACK_ADAPTER @@ -63,7 +67,6 @@ } _RECALL_PROFILE_FIELDS = {"profile_id", "mode", "max_queries", "limit"} _AUTOMATION_FIELDS = {"automatic_recall", "automatic_ingest", "fail_open"} -_RECALL_MODES = {"function_boundary", "bounded_agentic_search"} def canonical_reward_memory_actor_peer_id(*, goal_id: str, agent_id: str) -> str: @@ -589,7 +592,7 @@ def _normalize_v1(raw: Mapping[str, Any]) -> dict[str, Any]: allowed=_RECALL_PROFILE_FIELDS, ) mode = str(profile.get("mode") or "").strip() - if mode not in _RECALL_MODES: + if mode not in RECALL_MODES: raise ValueError("recall_profile.mode is unsupported") profile_id = str(profile.get("profile_id") or "").strip() if not profile_id: diff --git a/loopx/capabilities/reward_memory/utility_reducer.py b/loopx/capabilities/reward_memory/utility_reducer.py index 2685da6655..e50af76a4e 100644 --- a/loopx/capabilities/reward_memory/utility_reducer.py +++ b/loopx/capabilities/reward_memory/utility_reducer.py @@ -19,6 +19,8 @@ from .memory_utility import ( ATTRIBUTION_LEVELS, RewardMemoryUtilityObservationValidationError, + _MEMORY_REF_DIGEST_RE, + _OPAQUE_REF_RE, _SCOPE_FIELDS, validate_reward_memory_utility_observation, ) @@ -37,8 +39,6 @@ MAX_REVIEW_REASON_CODES = 12 MAX_HISTORY_EVIDENCE_REFS = 16 -_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$") -_MEMORY_DIGEST_RE = re.compile(r"^(?:[0-9a-f]{16}|sha256:[0-9a-f]{64})$") _OBSERVATION_ID_RE = re.compile(r"^muo_[0-9a-f]{64}$") _EVIDENCE_ORDER = ( "insufficient", @@ -91,7 +91,7 @@ def _ref(value: object, label: str) -> str: result = value.strip() if ( result != value - or not _REF_RE.fullmatch(result) + or not _OPAQUE_REF_RE.fullmatch(result) or "://" in result or PurePosixPath(result).is_absolute() or PureWindowsPath(result).is_absolute() @@ -113,7 +113,7 @@ def _memory_digests(value: object, label: str) -> list[str]: result = [] for item in value: digest = _ref(item, label) - if not _MEMORY_DIGEST_RE.fullmatch(digest): + if not _MEMORY_REF_DIGEST_RE.fullmatch(digest): raise ValueError(f"{label} must contain canonical memory digests") result.append(digest) if result != value or result != sorted(result) or len(set(result)) != len(result): diff --git a/tests/architecture/test_reward_memory_field_vocabulary_owner.py b/tests/architecture/test_reward_memory_field_vocabulary_owner.py index 91cec1c923..60f0f09357 100644 --- a/tests/architecture/test_reward_memory_field_vocabulary_owner.py +++ b/tests/architecture/test_reward_memory_field_vocabulary_owner.py @@ -41,6 +41,18 @@ between those two capability packages, so naming one of them the authority over the other is a domain decision for a maintainer, not a deduplication. +The same ``memory_utility`` -> ``utility_reducer`` pair also restated two compiled +matchers, now converged here: ``_OPAQUE_REF_RE`` (the bounded opaque reference) +and ``_MEMORY_REF_DIGEST_RE`` (a 16-hex or enveloped-64 memory digest). Two +same-value neighbours stay separate, each for a stated reason: +``control_plane/status/goal_source.py`` states the reference body unanchored and +inline for a ``todo_id`` in another package, with no import direction to make one +the authority (the same domain decision as ``CORPUS_ID_RE``); and +``reward_memory/architecture.py`` lists the recall modes in a fixed order inside a +descriptive manifest, so ``RECALL_MODES`` is pinned by identity -- ``experiment`` +borrows ``application``'s set -- rather than by the member census, which would +otherwise sweep that ordered list. + Compiled patterns are real objects, so the ``is`` assertions below carry information; the two field sets are compared by identity too, which only holds because both modules now import the same object rather than spelling it out. @@ -60,7 +72,13 @@ from loopx.capabilities.issue_fix import repository_snapshot from loopx.capabilities.issue_fix import reward_memory as issue_fix_reward_memory from loopx.capabilities.reward_memory import architecture as reward_architecture -from loopx.capabilities.reward_memory import memory_utility, scoped_feedback, utility_reducer +from loopx.capabilities.reward_memory import ( + application, + experiment, + memory_utility, + scoped_feedback, + utility_reducer, +) REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] PACKAGE_ROOT = REPO_ROOT / "loopx" @@ -111,6 +129,18 @@ ) ], ), + "_OPAQUE_REF_RE": ( + r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$", + "loopx/capabilities/reward_memory/memory_utility.py", + memory_utility, + [("loopx/capabilities/reward_memory/utility_reducer.py", utility_reducer)], + ), + "_MEMORY_REF_DIGEST_RE": ( + r"^(?:[0-9a-f]{16}|sha256:[0-9a-f]{64})$", + "loopx/capabilities/reward_memory/memory_utility.py", + memory_utility, + [("loopx/capabilities/reward_memory/utility_reducer.py", utility_reducer)], + ), } # Regex bodies that are legitimately embedded in a larger grammar elsewhere. @@ -325,6 +355,50 @@ def test_both_scope_validators_accept_the_same_four_keys() -> None: assert "missing=['corpus_id', 'project_id', 'surface_id']" in str(missing.value), label +def test_both_reference_validators_accept_the_same_opaque_refs() -> None: + """Wiring through each module's own reference reader, not just the imported name.""" + + for value in ("ref-1", "todo_2.3:4", "a" * 200): + assert memory_utility._opaque_ref(value, "ref") == value + assert utility_reducer._ref(value, "ref") == value + # The bound and the class are the rule. The path-separator and anchor cases + # are what separate this stricter shape from public_safe_text's slash-and-# + # allowing reference owner, so a fold into that owner would fail here. + for value in ("a" * 201, "has space", "a/b", "a#b", ""): + for label, validate in ( + ("owner", memory_utility._opaque_ref), + ("consumer", utility_reducer._ref), + ): + with pytest.raises(ValueError): + validate(value, "ref") + + +def test_both_digest_validators_accept_the_same_two_alternatives() -> None: + sixteen = "0123456789abcdef" + enveloped = "sha256:" + "a" * 64 + for value in ([sixteen], [enveloped], [sixteen, enveloped]): + assert memory_utility.normalize_reward_memory_ref_digests(value, "d") == value + assert utility_reducer._memory_digests(value, "d") == value + # A bare 64-hex is neither alternative; 15/17 hex and a short envelope fail. + for value in (["a" * 64], ["a" * 15], ["a" * 17], ["sha256:" + "a" * 63]): + for label, validate in ( + ("owner", memory_utility.normalize_reward_memory_ref_digests), + ("consumer", utility_reducer._memory_digests), + ): + with pytest.raises(ValueError): + validate(value, "d") + + +def test_experiment_borrows_the_application_recall_modes() -> None: + # Pinned by identity, not by the member census: architecture.py lists the + # same two modes in a fixed order inside a descriptive manifest, and folding + # that list from an unordered set would reorder it. The validator's set is + # the owner; the manifest's ordered list is a different question. + assert experiment.RECALL_MODES is application.RECALL_MODES + assert not hasattr(experiment, "_RECALL_MODES") + assert application.RECALL_MODES == {"function_boundary", "bounded_agentic_search"} + + def test_repo_shape_is_checked_by_the_producer_and_the_verifier() -> None: assert repository_snapshot._repo_parts("loopx-project/loopx") == ("loopx-project", "loopx") with pytest.raises(ValueError) as refused: From 0c2349e2a6dd9ba7dc6ef2e80fb1b39402e5ab31 Mon Sep 17 00:00:00 2001 From: psychologister <230141834+psychologister@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:38:36 +0800 Subject: [PATCH 2/2] reward_memory: scope the new matcher ownership guard to compiled rules The first revision routed _OPAQUE_REF_RE and _MEMORY_REF_DIGEST_RE through the package-wide body-substring census. As the review showed, that flags two things that are not this rule: an independent re.compile(, re.IGNORECASE) importer that accepts uppercase, and a plain help/display string that quotes the body but is never compiled. Gate these two reward-memory-internal matchers with a scoped compiled-rule census instead: walk only capabilities/reward_memory (the owner and its consumers), count only real re.compile sites, and compare (body, source flags) so a flag-different compile is a different rule. The repository and explore patterns keep the original package-wide embedded-body census. Production reuse is unchanged: utility_reducer still imports both matchers from memory_utility, and experiment still borrows application's RECALL_MODES. The accept/reject behaviour stays pinned through both modules' own validators. Add poison controls proving the flag-different importer and the never-compiled help string read as neighbours while a flagless recompilation is still caught, plus a scope-boundary test showing an out-of-package restatement is not scanned. Test-only change. Guard 59 passed; architecture suite net regression 0 (identical 96 pre-existing environment failures); reward_memory behaviour 158 passed; ruff clean. Signed-off-by: psychologister <230141834+psychologister@users.noreply.github.com> --- ...st_reward_memory_field_vocabulary_owner.py | 238 ++++++++++++++++-- 1 file changed, 219 insertions(+), 19 deletions(-) diff --git a/tests/architecture/test_reward_memory_field_vocabulary_owner.py b/tests/architecture/test_reward_memory_field_vocabulary_owner.py index 60f0f09357..447705da5e 100644 --- a/tests/architecture/test_reward_memory_field_vocabulary_owner.py +++ b/tests/architecture/test_reward_memory_field_vocabulary_owner.py @@ -23,12 +23,30 @@ ``repository_snapshot.py`` builds and stamps the ``repo`` field, while ``repository_commit_evidence.py`` only checks a declared one against it. -Two scan layers, because grouping module-level constants by name is not enough -to call a change complete: bindings are matched on the **folded value** (a set's -members, a compiled pattern's body) so a same-name-different-value neighbour is -never reported as a duplicate, and a separate census looks for any collection -literal with the same members or any string constant embedding the same regex -body. +Three scan layers, because grouping module-level constants by name is not enough +to call a change complete. The field sets and the two repository/explore patterns +(``TOKEN_PATTERN``, ``_REPO_PATTERN``) keep the original package-wide census: +bindings are matched on the **folded value** (a set's members, a compiled +pattern's body) so a same-name-different-value neighbour is never reported as a +duplicate, and a separate pass looks for any collection literal with the same +members or any string constant embedding the same regex body -- the latter is +what lets ``pr_description``'s three larger grammars be *declared* rather than +merged. + +The two reward-memory matchers this change converges (``_OPAQUE_REF_RE``, +``_MEMORY_REF_DIGEST_RE``) are gated by a **scoped, compiled-rule** census +instead, for the reason the review of the first revision gave: a body-only +substring sweep over the whole package flags two things that are not this rule -- +an independent ``re.compile(, re.IGNORECASE)`` importer that accepts +uppercase, and a plain help/display string that merely quotes the body and is +never compiled. So for these two the census (1) walks only +``capabilities/reward_memory`` -- the owner and its consumers, never an unrelated +package; (2) counts only real ``re.compile`` sites, not arbitrary string +constants; and (3) compares the **compiled rule** ``(body, source_flags)``, so a +flag-different compile is a different rule and is left alone. A true restatement +-- a reward-memory module that re-compiles the identical flagless body instead of +importing the owner -- is still caught, and the accept/reject behaviour is pinned +through both modules' own validators below. Neighbours this guard deliberately does not merge, each pinned below: the ``field_contracts`` table in ``reward_memory/architecture.py`` describes *which @@ -41,17 +59,14 @@ between those two capability packages, so naming one of them the authority over the other is a domain decision for a maintainer, not a deduplication. -The same ``memory_utility`` -> ``utility_reducer`` pair also restated two compiled -matchers, now converged here: ``_OPAQUE_REF_RE`` (the bounded opaque reference) -and ``_MEMORY_REF_DIGEST_RE`` (a 16-hex or enveloped-64 memory digest). Two -same-value neighbours stay separate, each for a stated reason: -``control_plane/status/goal_source.py`` states the reference body unanchored and -inline for a ``todo_id`` in another package, with no import direction to make one -the authority (the same domain decision as ``CORPUS_ID_RE``); and -``reward_memory/architecture.py`` lists the recall modes in a fixed order inside a -descriptive manifest, so ``RECALL_MODES`` is pinned by identity -- ``experiment`` -borrows ``application``'s set -- rather than by the member census, which would -otherwise sweep that ordered list. +``RECALL_MODES`` is pinned by identity -- ``experiment`` borrows ``application``'s +set -- rather than by the member census, because ``reward_memory/architecture.py`` +lists the same two modes in a fixed order inside a descriptive manifest, which an +unordered member fold would reorder. ``control_plane/status/goal_source.py`` +states the opaque-reference body unanchored and inline for a ``todo_id`` in +another package; it sits outside the reward-memory scope of the compiled-rule +census and is a different (unanchored) rule in any case, so it is neither scanned +nor merged -- the same domain decision as ``CORPUS_ID_RE``. Compiled patterns are real objects, so the ``is`` assertions below carry information; the two field sets are compared by identity too, which only holds @@ -62,6 +77,7 @@ import ast import pathlib +import re from types import ModuleType import pytest @@ -82,6 +98,12 @@ REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] PACKAGE_ROOT = REPO_ROOT / "loopx" +REWARD_MEMORY_ROOT = PACKAGE_ROOT / "capabilities" / "reward_memory" + +# These two matchers are reward-memory-internal rules. Their ownership census is +# scoped to REWARD_MEMORY_ROOT and compares compiled (body, flags) rules, not the +# package-wide body-substring sweep the repository/explore patterns below use. +SCOPED_COMPILED_MATCHERS = frozenset({"_OPAQUE_REF_RE", "_MEMORY_REF_DIGEST_RE"}) # name -> (kind, payload, owner path, owner module, [(consumer path, consumer module)]) SET_DECISIONS: dict[str, tuple[frozenset[str], str, ModuleType, list[tuple[str, ModuleType]]]] = { @@ -143,6 +165,12 @@ ), } +# The repository/explore patterns keep the package-wide embedded-body census; the +# reward-memory matchers are gated by the scoped compiled-rule census instead. +PACKAGE_WIDE_PATTERNS = sorted( + name for name in PATTERN_DECISIONS if name not in SCOPED_COMPILED_MATCHERS +) + # Regex bodies that are legitimately embedded in a larger grammar elsewhere. # file -> count of string constants carrying the body, with the reason recorded. DECLARED_EMBEDDED_BODIES: dict[str, dict[str, int]] = { @@ -243,11 +271,97 @@ def body_literal_sites(source: str, body: str) -> list[int]: return _sites(_analyse(source), "", "body", body)[1] +def _flag_value(node: ast.expr) -> int | None: + """Statically evaluate an ``re`` flag expression to an int, or None if unknown.""" + + if isinstance(node, ast.Constant) and isinstance(node.value, int): + return node.value + if isinstance(node, ast.Attribute): + value = getattr(re, node.attr, None) + return value if isinstance(value, int) else None + if isinstance(node, ast.Name): + value = getattr(re, node.id, None) + return value if isinstance(value, int) else None + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + left, right = _flag_value(node.left), _flag_value(node.right) + if left is not None and right is not None: + return left | right + return None + + +def _compile_flags(node: ast.Call) -> int | None: + """Source-level flags of an ``re.compile`` call: 0 when none are written. + + This is the flags argument *as written*, not the runtime ``Pattern.flags`` + (which always carries ``re.UNICODE`` for a str pattern). The owner compiles + both matchers with no explicit flags, so a duplicate must also write none; an + unresolvable flag expression returns None and is treated as a different rule. + """ + + flag_node = node.args[1] if len(node.args) >= 2 else None + for keyword in node.keywords: + if keyword.arg == "flags": + flag_node = keyword.value + if flag_node is None: + return 0 + return _flag_value(flag_node) + + +def compiled_rule(node: ast.expr) -> tuple[str, int | None] | None: + """Fold ``re.compile([, flags])`` to ``(body, source_flags)``. + + Unlike ``pattern_body`` this keeps the flags, so an ``re.IGNORECASE`` variant + of the same body folds to a different rule and is not counted as a duplicate. + """ + + if isinstance(node, ast.Call): + fname = getattr(node.func, "id", None) or getattr(node.func, "attr", None) + if fname == "compile" and node.args: + first = node.args[0] + if isinstance(first, ast.Constant) and isinstance(first.value, str): + return (first.value, _compile_flags(node)) + return None + + +def scoped_matcher_sites(source: str, body: str) -> list[int]: + """Lines that compile exactly the owner's flagless rule ``(body, 0)``. + + Compile-only (a bare help string is never counted), exact-body (a larger + grammar that merely embeds the body is a different rule), and flag-aware. + """ + + return [ + node.lineno + for node in ast.walk(ast.parse(source)) + if compiled_rule(node) == (body, 0) + ] + + +def _reward_memory_matcher_scan() -> dict[str, list[str]]: + """Census the reward-memory package for recompilations of each scoped rule.""" + + findings: dict[str, list[str]] = {} + for name in sorted(SCOPED_COMPILED_MATCHERS): + body, owner_path, _owner_module, _consumers = PATTERN_DECISIONS[name] + offenders: list[str] = [] + for path in sorted(REWARD_MEMORY_ROOT.rglob("*.py")): + rel = path.relative_to(REPO_ROOT).as_posix() + if rel == owner_path: + continue + offenders.extend( + f"{rel}:{line} recompiles the {name} rule instead of importing it" + for line in scoped_matcher_sites(path.read_text(encoding="utf-8"), body) + ) + findings[name] = offenders + return findings + + def _repository_scan(): queries = [(name, "members", spec[0], spec[1], {}) for name, spec in SET_DECISIONS.items()] queries += [(name, "body", spec[0], spec[1], DECLARED_EMBEDDED_BODIES.get(spec[0], {})) - for name, spec in PATTERN_DECISIONS.items()] + for name, spec in PATTERN_DECISIONS.items() + if name not in SCOPED_COMPILED_MATCHERS] bindings = {name: [] for name, *_ in queries} sites = {name: {} for name, *_ in queries} for path in sorted(PACKAGE_ROOT.rglob("*.py")): @@ -294,6 +408,11 @@ def repository_findings(): return _repository_reader() +@pytest.fixture(scope="module") +def reward_memory_matcher_findings(): + return _reward_memory_matcher_scan() + + @pytest.mark.parametrize("name", sorted(SET_DECISIONS)) def test_only_the_owner_module_binds_each_field_vocabulary(name: str, repository_findings) -> None: bindings, inline = repository_findings(name) @@ -301,13 +420,30 @@ def test_only_the_owner_module_binds_each_field_vocabulary(name: str, repository assert inline == [], f"{name} is still spelled out somewhere: " + "\n".join(inline) -@pytest.mark.parametrize("name", sorted(PATTERN_DECISIONS)) +@pytest.mark.parametrize("name", PACKAGE_WIDE_PATTERNS) def test_only_the_owner_module_compiles_each_pattern(name: str, repository_findings) -> None: bindings, embedded = repository_findings(name) assert bindings == [], "\n".join(bindings) assert embedded == [], f"{name} body appears undeclared: " + "\n".join(embedded) +@pytest.mark.parametrize("name", sorted(SCOPED_COMPILED_MATCHERS)) +def test_only_the_owner_compiles_each_reward_memory_matcher( + name: str, reward_memory_matcher_findings +) -> None: + offenders = reward_memory_matcher_findings[name] + assert offenders == [], f"{name} is recompiled outside its owner: " + "\n".join(offenders) + + +@pytest.mark.parametrize("name", sorted(SCOPED_COMPILED_MATCHERS)) +def test_the_owner_compiles_each_reward_memory_matcher_exactly_once(name: str) -> None: + # Without the owner exemption the scoped census would flag the owner itself; + # this pins that the exemption is load-bearing and there is one definition. + body, owner_path, owner_module, _consumers = PATTERN_DECISIONS[name] + assert getattr(owner_module, name).pattern == body + assert len(scoped_matcher_sites(module_source(owner_path), body)) == 1 + + @pytest.mark.parametrize(("name", "spec"), sorted(SET_DECISIONS.items())) def test_the_owner_states_the_recorded_members(name: str, spec) -> None: members, owner_path, owner_module, _consumers = spec @@ -551,3 +687,67 @@ def test_repository_findings_preserve_refusals_and_retry(tmp_path, monkeypatch, assert rejected == [ "loopx/capabilities/issue_fix/pr_description.py declares 3 embedded site(s), found 1" ] + + +@pytest.mark.parametrize("name", sorted(SCOPED_COMPILED_MATCHERS)) +@pytest.mark.parametrize( + ("label", "source", "expected"), + [ + ("renamed flagless restatement", '_X = re.compile(r"__BODY__")\n', 1), + ("ignorecase is a different rule", '_X = re.compile(r"__BODY__", re.IGNORECASE)\n', 0), + ("short flag is a different rule", '_X = re.compile(r"__BODY__", re.I)\n', 0), + ("keyword flag is a different rule", '_X = re.compile(r"__BODY__", flags=re.IGNORECASE)\n', 0), + ("multiline is a different rule", '_X = re.compile(r"__BODY__", re.MULTILINE)\n', 0), + ("combined flags are a different rule", '_X = re.compile(r"__BODY__", re.I | re.M)\n', 0), + ("help text quoting the body is not compiled", 'HELP = "match __BODY__ to validate"\n', 0), + ("larger grammar embedding the body", '_X = re.compile(r"^prefix__BODY__suffix$")\n', 0), + ("different body", '_X = re.compile(r"^[a-z]+$")\n', 0), + ("import from the owner", "from .memory_utility import _OPAQUE_REF_RE, _MEMORY_REF_DIGEST_RE\n", 0), + ], + ids=[ + "renamed-restatement", + "ignorecase-neighbour", + "short-flag-neighbour", + "keyword-flag-neighbour", + "multiline-neighbour", + "combined-flags-neighbour", + "help-text-neighbour", + "larger-grammar-neighbour", + "different-body", + "import", + ], +) +def test_the_scoped_census_bites_a_restatement_and_leaves_the_neighbours( + name: str, label: str, source: str, expected: int +) -> None: + # The two review counterexamples are the flag-different importer and the + # never-compiled help string; both must read as neighbours, while a flagless + # recompilation of the identical body -- even under a new name -- is caught. + body = PATTERN_DECISIONS[name][0] + sites = scoped_matcher_sites(source.replace("__BODY__", body), body) + assert len(sites) == expected, label + + +def test_the_scoped_census_never_leaves_the_reward_memory_package(tmp_path, monkeypatch) -> None: + # A restatement inside reward-memory is caught; the identical text in an + # unrelated package is out of scope and never scanned. This is the review's + # core ask: the new ownership constraint must not reach other packages. + monkeypatch.setitem(globals(), "REPO_ROOT", tmp_path) + monkeypatch.setitem( + globals(), "REWARD_MEMORY_ROOT", tmp_path / "loopx/capabilities/reward_memory" + ) + name = "_OPAQUE_REF_RE" + body, owner_path, _owner_module, _consumers = PATTERN_DECISIONS[name] + owner = tmp_path / owner_path + owner.parent.mkdir(parents=True) + owner.write_text(f'{name} = re.compile(r"{body}")\n') + consumer = tmp_path / "loopx/capabilities/reward_memory/utility_reducer.py" + consumer.write_text(f'{name} = re.compile(r"{body}")\n') # a true restatement + outside = tmp_path / "loopx/capabilities/control_plane/goal_source.py" + outside.parent.mkdir(parents=True) + outside.write_text(f'{name} = re.compile(r"{body}")\n') # same text, out of scope + findings = _reward_memory_matcher_scan() + assert findings[name] == [ + "loopx/capabilities/reward_memory/utility_reducer.py:1 recompiles the " + "_OPAQUE_REF_RE rule instead of importing it" + ]