T08: give provisional defaults an owner, a date, and a visible age

Five scenarios encoded U-item defaults with no owner and no review date,
so they could shape the kernel indefinitely while looking handled.

Each now carries provisional_owner and provisional_raised, and the
runtime's ScenarioFile learned both fields (deny_unknown_fields meant
adding them to YAML alone would have failed every scenario -- the parser
had to agree).

make coverage reports every provisional item with its owner and age in
days, warns on any with no owner, and warns past 30 days. It WARNS
rather than breaking the build, on purpose: the ruling is ground-game's
to make and the kernel cannot make it for them. What the loop can
enforce is that evidence files list them, which is now stated in
GroundRules.

rule-coverage --self-test gained an assertion that every provisional
item has both fields, so the next one added without them fails a
command rather than passing quietly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-31 09:29:40 +02:00
parent 4580438f1c
commit e3d0df6690
9 changed files with 83 additions and 1 deletions

View file

@ -27,6 +27,10 @@ import sys
RULE_RE = r"\*\*(GR-[A-Z]+\d+)"
COVERS_RE = r"covers: \[(.*?)\]"
AGGREGATE = "games/ground/src/lib.rs"
# CB-WP-0003 T08: a provisional default with no expiry can shape the kernel
# indefinitely while looking handled. CI warns; it does not break the build,
# because the ruling is a ground-game decision we cannot make for them.
PROVISIONAL_WARN_DAYS = 30
ID_RE = r"GR-[A-Z]+\d+"
@ -39,6 +43,21 @@ def parse_code_ids(text):
return set(re.findall(ID_RE, text))
def provisional_items(paths):
"""(path, owner, raised) for every scenario encoding a U-item default."""
out = []
for path in paths:
text = open(path).read()
if not re.search(r"^provisional:\s*true", text, re.M):
continue
owner = re.search(r"^provisional_owner:\s*(\S+)", text, re.M)
raised = re.search(r"^provisional_raised:\s*(\S+)", text, re.M)
out.append((path,
owner.group(1) if owner else None,
raised.group(1) if raised else None))
return out
def parse_covers(text):
match = re.search(COVERS_RE, text, re.S)
if not match:
@ -71,6 +90,12 @@ def self_test():
== {"GR-R06", "GR-A12"})
check("code-id matcher finds none in unmarked source",
parse_code_ids("fn f() { let x = 1; }") == set())
# T08: every provisional scenario must carry an owner and a date.
import glob as _g
prov = provisional_items(sorted(_g.glob("scenarios/ground/*.yaml")))
check("every provisional item has an owner and a raised date",
bool(prov) and all(o and r for _, o, r in prov),
f"{len(prov)} provisional item(s)")
print("rule-coverage self-test (positive control)")
ok = True
@ -128,6 +153,34 @@ def main():
print(" ERROR — rule id in code that the spec does not define:",
" ".join(phantom), file=sys.stderr)
return 1
# T08: provisional items are reported with an owner and an age.
prov = provisional_items(paths)
if prov:
import datetime
today = datetime.date.today()
print(f"\nprovisional U-item defaults: {len(prov)}")
unowned, stale = [], []
for path, owner, raised in prov:
age = "?"
if raised:
try:
age = (today - datetime.date.fromisoformat(raised)).days
except ValueError:
age = "?"
name = path.split("/")[-1]
print(f" {name:<34} owner={owner or 'NONE':<12} age={age}d")
if not owner:
unowned.append(name)
if isinstance(age, int) and age > PROVISIONAL_WARN_DAYS:
stale.append(f"{name} ({age}d)")
if unowned:
print(" WARN — provisional with no owner:", " ".join(unowned))
if stale:
print(f" WARN — provisional for over {PROVISIONAL_WARN_DAYS} days:",
" ".join(stale))
print(" NOTE: evidence files must list these; a ruling flips the "
"scenario, not the kernel")
if missing:
print(" uncovered:", " ".join(missing))
if invented: