fix(scaffolding): scope matching is path-only — close the accidental wildcard

MEASURED GOVERNANCE DEFECT (priority 0). check_scope accepted a bare SUBSTRING
match ('s in f') alongside exact and directory-prefix matching. That is not a
path semantic. A contract whose scope contained the token '.' therefore matched
EVERY path: effectively allow-all while appearing bounded. The active T24
contract had exactly that token, so T24 was running under falsely bounded
authority — the worst of both worlds, since it looked governed.

FIX AT THE SINGLE CANONICAL POINT: matching is now exact path or directory
prefix, and non-string/blank entries are ignored. One rule, in check_scope, so
a malformed token cannot silently widen authority at any layer above. Nothing
is banned and no canonical whole-project representation is removed — path
semantics are simply restored.

  scope ['src/rune']        -> docs/x.md RED            (was RED)
  scope ['.']               -> docs/x.md RED, src/rune/a.py RED (was GREEN/GREEN)
  scope ['src/rune','docs/PLAN.md'] -> x/src/rune/y.py RED, docs/PLAN.md.bak RED
    (substrings that merely CONTAIN a scope entry are not in scope)

LIVE CONTRACT REPAIRED THROUGH THE CANONICAL AMENDMENT:
ConstructionInterface.amend_scope requires user authority (builders cannot
broaden their own scope — verified: a builder-authority attempt is refused),
is additive, and appends an auditable reason to ext.scope_amendments. The
junk tokens from the earlier prose-derived scope remain but are now inert;
they are NOT hand-edited out, because amend_scope is additive by canonical
design and silently editing the contract to make it pass is exactly what the
mandate forbids.

BEFORE: 30 entries incl. '.' and prose fragments, effectively allow-all
AMEND:  + ['config']  (reason recorded as the supervisory instruction)
AFTER : 31 entries, genuinely bounded

LIVE SCOPE PROOF: src/rune/... green · tests/... green · docs/... green ·
config/mimo-hooks/... green · store/scaffold/current.json RED · README.md RED.

Verification: 3 new tests (114 in the governance suite); 480 targeted
regression, 0 failures; ruff clean. LOCAL_MODEL_COMPUTE_EXECUTED = NO.
This commit is contained in:
conductor committed 2026-09-24 21:28:40 +01:00
1 parent d02aa56a1d
commit 4a2c7063db
2 files changed
+78 -2

No files matched your search

+15 -2
View File
@@ -622,7 +622,18 @@ class Scaffold:
def check_scope(self, doc: dict, contract_id: str, changed_files: list[str]) -> dict:
"""MECHANICAL SCOPE CHECK: changed files vs the contract's approved
scope paths. Out-of-scope = RED (verification failure) unless the
contract is amended. Deterministic; no code-ownership system."""
contract is amended. Deterministic; no code-ownership system.
MATCHING IS PATH SEMANTICS, in one place. A scope entry means either
the exact path, or a directory prefix. The previous implementation
also accepted `s in f` — a bare SUBSTRING test — which is not a path
semantic at all: it made any short or malformed entry match far more
than it names. MEASURED DEFECT (T24): a contract whose scope contained
the token "." was therefore effectively ALLOW-ALL while appearing
bounded, because "." occurs in every path. Enforcing path semantics
here is the single canonical rule, so a malformed token cannot
silently widen authority at any layer above.
"""
e = entity(doc, f"contract:{contract_id}")
if e is None:
raise ValueError(f"contract not found: {contract_id}")
@@ -639,7 +650,9 @@ class Scaffold:
}
in_scope, out_scope = [], []
for f in changed_files:
ok = any(f == s or f.startswith(s.rstrip("/") + "/") or s in f for s in scope)
ok = any(
f == s or f.startswith(s.rstrip("/") + "/")
for s in scope if isinstance(s, str) and s.strip())
(in_scope if ok else out_scope).append(f)
verdict = "green" if not out_scope else "red"
return {"verdict": verdict, "in_scope": sorted(in_scope), "out_of_scope": sorted(out_scope)}
+63
View File
@@ -170,3 +170,66 @@ def test_interrogatives_never_count_as_requirements():
"""An obligation word inside a QUESTION is still a question."""
assert has_construction_requirements("must I restart the workstream?") is False
assert has_construction_requirements("how must the contract be scoped?") is False
# ==========================================================================
# SCOPE AUTHORITY — measured defect: an accidental wildcard
# ==========================================================================
def test_dot_scope_no_longer_grants_allow_all(tmp_path):
"""MEASURED DEFECT. `check_scope` also matched on a bare SUBSTRING
(`s in f`), which is not a path semantic: a contract whose scope contained
the token "." matched EVERY path, so it was effectively allow-all while
appearing bounded. Matching is now path-only — exact or directory prefix.
"""
from rune.scaffold import Scaffold
s = Scaffold(str(tmp_path), "p")
doc = s.load()
s.new_contract(doc, contract_id="C1", need="n", origin="t", scope=["."])
s.save(doc)
v = s.check_scope(doc, "C1", ["docs/x.md", "src/rune/a.py"])
assert v["verdict"] == "red"
assert set(v["out_of_scope"]) == {"docs/x.md", "src/rune/a.py"}
def test_scope_matching_is_exact_or_directory_prefix(tmp_path):
from rune.scaffold import Scaffold
s = Scaffold(str(tmp_path), "p")
doc = s.load()
s.new_contract(doc, contract_id="C1", need="n", origin="t",
scope=["src/rune", "docs/PLAN.md"])
s.save(doc)
assert s.check_scope(doc, "C1", ["src/rune/a.py"])["verdict"] == "green"
assert s.check_scope(doc, "C1", ["docs/PLAN.md"])["verdict"] == "green"
# a substring that merely CONTAINS a scope entry is not in scope
assert s.check_scope(doc, "C1", ["x/src/rune/y.py"])["verdict"] == "red"
assert s.check_scope(doc, "C1", ["docs/PLAN.md.bak"])["verdict"] == "red"
def test_scope_amendment_is_additive_and_audited(store):
"""Canonical amendment records an auditable before/after and requires
user authority — builders cannot broaden their own scope."""
from rune.construction_interface import ConstructionInterface
from rune.scaffold import Scaffold
rec = turn("refactor src/rune/scaffold.py", store)
iface = ConstructionInterface(builder="t")
iface.scaffold = Scaffold(str(store), PROJECT)
doc = iface.scaffold.load()
e = next(x for x in doc["entities"] if x["id"].endswith(rec["contract_id"]))
refused = iface.amend_scope(rec["contract_id"], ["docs"],
authority="builder", reason="self-widen")
assert refused["success"] is False
ok = iface.amend_scope(rec["contract_id"], ["docs"], authority="user",
reason="supervisory instruction")
assert ok["success"] is True
doc2 = iface.scaffold.load()
e2 = next(x for x in doc2["entities"] if x["id"].endswith(rec["contract_id"]))
assert "docs" in e2["identities"]["scope"]
assert e2["ext"]["scope_amendments"][-1]["reason"].startswith("supervisory")